cylc-flow-8.6.4/0000775000175000017500000000000015202510312013603 5ustar alastairalastaircylc-flow-8.6.4/setup.cfg0000664000175000017500000001716215202510242015435 0ustar alastairalastair# 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 . [metadata] name = cylc-flow version = attr: cylc.flow.__version__ author = Hilary Oliver url=https://cylc.org/ description = A workflow engine for cycling systems long_description=file: README.md long_description_content_type=text/markdown project_urls = Documentation = https://cylc.github.io/cylc-doc/stable/html/index.html Source = https://github.com/cylc/cylc-flow Tracker = https://github.com/cylc/cylc-flow/issues keywords = cycling-workflows hpc job-scheduler metascheduler workflow-automation workflow-engine workflow-management scheduling license = GPL license_file = COPYING platforms = any classifiers = Environment :: Console Environment :: Web Environment Intended Audience :: Developers Intended Audience :: System Administrators Intended Audience :: Science/Research License :: OSI Approved :: GNU General Public License v3 (GPLv3) Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Topic :: Scientific/Engineering :: Atmospheric Science [options] packages = find_namespace: include_package_data = True python_requires = >=3.12 install_requires = ansimarkup>=1.0.0 colorama>=0.4,<1 graphql-core>=3.2,<3.3 graphene>=3.4.0,<3.5 # Note: can't pin jinja2 any higher than this until we give up on Cylc 7 back-compat jinja2==3.0.* metomi-isodatetime>=1!3.0.0,<1!3.2.0 # Constrain protobuf version for compatible Scheduler-UIS comms across hosts packaging protobuf>=5,<8 psutil>=5.6.0 pyzmq>=22 # NOTE: exclude two urwid versions that were not compatible with Tui urwid>=2.2,!=2.6.2,!=2.6.3,<4 [options.packages.find] include = cylc* [options.extras_require] graph = pillow main_loop-log_data_store = pympler matplotlib main_loop-log_main_loop = matplotlib main_loop-log_memory = pympler matplotlib main_loop-log_db = sqlparse report-timings = pandas==1.* matplotlib tests = aiosmtpd async_generator bandit>=1.7.0 coverage>=7.9.0 flake8-broken-line>=0.3.0 flake8-bugbear>=21.0.0 flake8-builtins>=1.5.0 flake8-comprehensions>=3.5.0 flake8-debugger>=4.0.0 flake8-implicit-str-concat>=0.4 flake8-mutable>=1.2.0 flake8-simplify>=0.14.0; python_version<"3.14" flake8-type-checking flake8>=3.0.0 mypy>=0.910 # https://github.com/pytest-dev/pytest-asyncio/issues/706 pytest-asyncio>=0.21.2,!=0.23.* pytest-cov>=2.8.0 pytest-xdist>=2 pytest-mock>=3.7 pytest>=6 testfixtures>=6.11.0 towncrier>=24.7.0 # Type annotation stubs # http://mypy-lang.blogspot.com/2021/05/the-upcoming-switch-to-modular-typeshed.html types-Jinja2>=0.1.3 types-protobuf>=0.1.10,!=5.29.1.20250402 tutorials = h5py requests all = %(graph)s %(main_loop-log_data_store)s %(main_loop-log_db)s %(main_loop-log_main_loop)s %(main_loop-log_memory)s %(tests)s %(tutorials)s [options.entry_points] # top level shell commands console_scripts = clyc = cylc.flow.scripts.cylc:main cylc = cylc.flow.scripts.cylc:main # cylc subcommands cylc.command = broadcast = cylc.flow.scripts.broadcast:main cat-log = cylc.flow.scripts.cat_log:main check-versions = cylc.flow.scripts.check_versions:main clean = cylc.flow.scripts.clean:main client = cylc.flow.scripts.client:main completion-server = cylc.flow.scripts.completion_server:main config = cylc.flow.scripts.config:main cycle-point = cylc.flow.scripts.cycle_point:main diff = cylc.flow.scripts.diff:main dump = cylc.flow.scripts.dump:main ext-trigger = cylc.flow.scripts.ext_trigger:main get-resources = cylc.flow.scripts.get_resources:main function-run = cylc.flow.scripts.function_run:main get-workflow-contact = cylc.flow.scripts.get_workflow_contact:main get-workflow-version = cylc.flow.scripts.get_workflow_version:main graph = cylc.flow.scripts.graph:main hold = cylc.flow.scripts.hold:main install = cylc.flow.scripts.install:main jobs-kill = cylc.flow.scripts.jobs_kill:main jobs-poll = cylc.flow.scripts.jobs_poll:main jobs-submit = cylc.flow.scripts.jobs_submit:main kill = cylc.flow.scripts.kill:main lint = cylc.flow.scripts.lint:main list = cylc.flow.scripts.list:main message = cylc.flow.scripts.message:main pause = cylc.flow.scripts.pause:main ping = cylc.flow.scripts.ping:main play = cylc.flow.scripts.play:main poll = cylc.flow.scripts.poll:main psutils = cylc.flow.scripts.psutil:main reinstall = cylc.flow.scripts.reinstall:main release = cylc.flow.scripts.release:main reload = cylc.flow.scripts.reload:main remote-init = cylc.flow.scripts.remote_init:main remote-tidy = cylc.flow.scripts.remote_tidy:main remove = cylc.flow.scripts.remove:main report-timings = cylc.flow.scripts.report_timings:main [report-timings] scan = cylc.flow.scripts.scan:cli show = cylc.flow.scripts.show:main set = cylc.flow.scripts.set:main stop = cylc.flow.scripts.stop:main subscribe = cylc.flow.scripts.subscribe:main verbosity = cylc.flow.scripts.verbosity:main workflow-state = cylc.flow.scripts.workflow_state:main tui = cylc.flow.scripts.tui:main trigger = cylc.flow.scripts.trigger:main validate = cylc.flow.scripts.validate:main view = cylc.flow.scripts.view:main vip = cylc.flow.scripts.validate_install_play:main vr = cylc.flow.scripts.validate_reinstall:main # async functions to run within the scheduler main loop cylc.main_loop = health_check = cylc.flow.main_loop.health_check auto_restart = cylc.flow.main_loop.auto_restart log_data_store = cylc.flow.main_loop.log_data_store [main_loop-log_data_store] log_db = cylc.flow.main_loop.log_db [main_loop-log_db] log_main_loop = cylc.flow.main_loop.log_main_loop [main_loop-log_main_loop] log_memory = cylc.flow.main_loop.log_memory [main_loop-log_memory] reset_bad_hosts = cylc.flow.main_loop.reset_bad_hosts # NOTE: all entry points should be listed here even if Cylc Flow does not # provide any implementations, to make entry point scraping easier cylc.pre_configure = cylc.post_install = log_vc_info = cylc.flow.install_plugins.log_vc_info:main # NOTE: Built-in xtrigger modules # - must contain a function (the xtrigger) with the same name as the module # - and may contain a "validate" function to check arguments cylc.xtriggers = echo = cylc.flow.xtriggers.echo wall_clock = cylc.flow.xtriggers.wall_clock workflow_state = cylc.flow.xtriggers.workflow_state suite_state = cylc.flow.xtriggers.suite_state xrandom = cylc.flow.xtriggers.xrandom [bdist_rpm] requires = python3-colorama python-isodatetime python3-jinja2 python3-MarkupSafe python3-zmq cylc-flow-8.6.4/tests/0000775000175000017500000000000015202510242014747 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/0000775000175000017500000000000015202510242017111 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/include-files/0000775000175000017500000000000015202510242021634 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/include-files/workflow/0000775000175000017500000000000015202510242023506 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/include-files/workflow/scheduling.cylc0000664000175000017500000000021715202510242026507 0ustar alastairalastair[scheduling] initial cycle point = 20130101T00 final cycle point = 20130101T00 [[graph]] R1 = foo => bar T00 = bar cylc-flow-8.6.4/tests/functional/include-files/workflow/ref-inlined.cylc0000664000175000017500000000045615202510242026563 0ustar alastairalastair[meta] title = "A test with nested include-files" [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20130101T00 final cycle point = 20130101T00 [[graph]] R1 = foo => bar T00 = bar [runtime] [[root]] script = "echo Hello World" cylc-flow-8.6.4/tests/functional/include-files/workflow/body.cylc0000664000175000017500000000005715202510242025321 0ustar alastairalastair%include scheduling.cylc %include runtime.cylc cylc-flow-8.6.4/tests/functional/include-files/workflow/runtime.cylc0000664000175000017500000000007315202510242026045 0ustar alastairalastair[runtime] [[root]] script = "echo Hello World" cylc-flow-8.6.4/tests/functional/include-files/workflow/flow.cylc0000664000175000017500000000016715202510242025335 0ustar alastairalastair[meta] title = "A test with nested include-files" [scheduler] allow implicit tasks = True %include body.cylc cylc-flow-8.6.4/tests/functional/include-files/test_header0000777000175000017500000000000015202510242030151 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/include-files/00-basic.t0000664000175000017500000000362615202510242023326 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test include-file inlining . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" workflow #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" # test raw workflow validates run_ok "${TEST_NAME}.1" cylc validate "${WORKFLOW_NAME}" # test workflow validates as inlined during editing mkdir inlined cylc view --inline "${WORKFLOW_NAME}" > inlined/flow.cylc run_ok "${TEST_NAME}.2" cylc validate ./inlined #------------------------------------------------------------------------------- # compare inlined workflow def with reference copy TEST_NAME=${TEST_NAME_BASE}-compare cmp_ok inlined/flow.cylc "${TEST_SOURCE_DIR}/workflow/ref-inlined.cylc" rm -rf inlined #------------------------------------------------------------------------------- purge #------------------------------------------------------------------------------- cylc-flow-8.6.4/tests/functional/remote/0000775000175000017500000000000015202510242020404 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/05-remote-init.t0000664000175000017500000000412315202510242023247 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote initialisation - when remote init fails for an install target, # check other platforms with same install target can be initialised. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 create_test_global_config "" " [platforms] [[belle]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} ssh command = garbage " #------------------------------------------------------------------------------- install_workflow run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" NAME='select-task-jobs.out' DB_FILE="${WORKFLOW_RUN_DIR}/log/db" sqlite3 "${DB_FILE}" \ 'SELECT name, submit_status, run_status, platform_name FROM task_jobs ORDER BY name' \ >"${NAME}" cmp_ok "${NAME}" <<__SELECT__ a|1||belle b|1||belle e|0|0|${CYLC_TEST_PLATFORM} f|0|0|${CYLC_TEST_PLATFORM} g|0|0|localhost __SELECT__ grep_ok "ERROR - Incomplete tasks:" "${TEST_NAME_BASE}-run.stderr" grep_ok "1/a did not complete the required outputs" "${TEST_NAME_BASE}-run.stderr" purge exit cylc-flow-8.6.4/tests/functional/remote/01-file-install.t0000664000175000017500000000677215202510242023406 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # test file installation to remote platforms export REQUIRE_PLATFORM='loc:remote comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 6 create_files () { # dump some files into the run dir for DIR in "bin" "ana" "app" "etc" "lib" "dir1" "dir2" "etc/share" "share" do mkdir -p "${WORKFLOW_RUN_DIR}/${DIR}" touch "${WORKFLOW_RUN_DIR}/${DIR}/moo" done for FILE in "file1" "file2" do touch "${WORKFLOW_RUN_DIR}/${FILE}" done } # Test configured files/directories along with default files/directories # (ana, app, bin, etc, lib) are correctly installed on the remote platform. TEST_NAME="${TEST_NAME_BASE}-default-paths" init_workflow "${TEST_NAME}" <<__FLOW_CONFIG__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] platform = $CYLC_TEST_PLATFORM __FLOW_CONFIG__ RUN_DIR_REL="${WORKFLOW_RUN_DIR#"${HOME}"/}" create_files # run the flow run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME}-run1" cylc play "${WORKFLOW_NAME}" \ --no-detach \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" # ensure these files get installed on the remote platform SSH="$(cylc config -d -i "[platforms][$CYLC_TEST_PLATFORM]ssh command")" ${SSH} "${CYLC_TEST_HOST}" \ find "${RUN_DIR_REL}/"{ana,app,bin,etc,lib} -type f | sort > 'find.out' cmp_ok 'find.out' <<__OUT__ ${RUN_DIR_REL}/ana/moo ${RUN_DIR_REL}/app/moo ${RUN_DIR_REL}/bin/moo ${RUN_DIR_REL}/etc/moo ${RUN_DIR_REL}/etc/share/moo ${RUN_DIR_REL}/lib/moo __OUT__ purge # ----------------------------------------------------------------------------- # Test the [scheduler]install configuration TEST_NAME="${TEST_NAME_BASE}-configured-paths" init_workflow "${TEST_NAME}" <<__FLOW_CONFIG__ [scheduler] install = dir1/, dir2/, file1, file2 [scheduling] [[graph]] R1 = foo [runtime] [[foo]] platform = $CYLC_TEST_PLATFORM __FLOW_CONFIG__ RUN_DIR_REL="${WORKFLOW_RUN_DIR#"${HOME}"/}" create_files run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME}-run2" cylc play "${WORKFLOW_NAME}" \ --no-detach \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" ${SSH} "${CYLC_TEST_HOST}" \ find "${RUN_DIR_REL}/"{ana,app,bin,dir1,dir2,file1,file2,etc,lib} -type f | sort > 'find.out' cmp_ok 'find.out' <<__OUT__ ${RUN_DIR_REL}/ana/moo ${RUN_DIR_REL}/app/moo ${RUN_DIR_REL}/bin/moo ${RUN_DIR_REL}/dir1/moo ${RUN_DIR_REL}/dir2/moo ${RUN_DIR_REL}/etc/moo ${RUN_DIR_REL}/etc/share/moo ${RUN_DIR_REL}/file1 ${RUN_DIR_REL}/file2 ${RUN_DIR_REL}/lib/moo __OUT__ purge exit cylc-flow-8.6.4/tests/functional/remote/basic/0000775000175000017500000000000015202510242021465 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/basic/reference.log0000664000175000017500000000007015202510242024123 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/remote/basic/flow.cylc0000664000175000017500000000032715202510242023312 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] stall timeout = PT0S [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = hostname -f platform = {{ environ['CYLC_TEST_PLATFORM'] }} cylc-flow-8.6.4/tests/functional/remote/03-polled-task-started/0000775000175000017500000000000015202510242024507 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/03-polled-task-started/reference.log0000664000175000017500000000024315202510242027147 0ustar alastairalastair1/picard -triggered off [] 1/worf -triggered off ['1/picard'] 1/riker -triggered off ['1/picard'] 1/janeway -triggered off [] 1/tuvok -triggered off ['1/janeway'] cylc-flow-8.6.4/tests/functional/remote/03-polled-task-started/flow.cylc0000664000175000017500000000157015202510242026335 0ustar alastairalastair#!Jinja2 [scheduler] allow implicit tasks = True [[events]] expected task failures = 1/janeway [scheduling] cycling mode = integer [[graph]] R1 = """ picard:start => worf picard => riker janeway:start => tuvok janeway:fail => !janeway # Makes test workflow shut down without a fuss """ [runtime] [[root]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[picard, janeway]] # Longer polling so that they finish before the first poll is done submission polling intervals = PT10S execution polling intervals = PT10S [[picard]] script = true [[[events]]] started handlers = echo "THERE ARE FOUR LIGHTS" [[janeway]] script = false [[[events]]] started handlers = echo "THERE'S COFFEE IN THAT NEBULA" cylc-flow-8.6.4/tests/functional/remote/03-polled-task-started.t0000664000175000017500000000320615202510242024675 0ustar alastairalastair#!/bin/bash # 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 . # ----------------------------------------------------------------------------- # Test that a quickly finishing task's `:start` trigger does not get missed # when using polling to get remote task status. export REQUIRE_PLATFORM='loc:remote comms:poll' . "$(dirname "$0")/test_header" set_test_number 4 install_workflow run_ok "${TEST_NAME_BASE}-validate" cylc validate "$WORKFLOW_NAME" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --reference-test --no-detach "$WORKFLOW_NAME" # Check 'started' event handlers ran PICARD_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/picard/01/job-activity.log" grep_ok "[(('event-handler-00', 'started'), 1) out] THERE ARE FOUR LIGHTS" "$PICARD_ACTIVITY_LOG" -F JANEWAY_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/janeway/01/job-activity.log" grep_ok "[(('event-handler-00', 'started'), 1) out] THERE'S COFFEE IN THAT NEBULA" "$JANEWAY_ACTIVITY_LOG" -F purge exit cylc-flow-8.6.4/tests/functional/remote/05-remote-init/0000775000175000017500000000000015202510242023062 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/05-remote-init/flow.cylc0000664000175000017500000000205615202510242024710 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = true stall timeout = PT0S abort on inactivity timeout = true [scheduling] [[graph]] R1 = """ a & b & g # a & b setup to submit-fail, g (localhost) does not require remote init b:submitted? => c b:start => d # c & d should not be triggered b:submit-fail? => e & f # e and f on a same install target but with a different platform name - these should execute """ [runtime] [[task]] script="sleep 1; echo hello" [[a]] inherit = task platform = belle [[b]] inherit = task platform = belle [[c]] inherit = task platform = belle [[d]] inherit=task platform = belle [[e]] inherit=task # ariel platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[f]] inherit=task # ariel platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[g]] inherit=task platform = localhost cylc-flow-8.6.4/tests/functional/remote/07-slow-file-install.t0000664000175000017500000000331215202510242024361 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test file install completes before dependent tasks are executed export REQUIRE_PLATFORM='loc:remote comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] rsync command = my-rsync.sh " TEST_NAME="${TEST_NAME_BASE}-installation-timing" install_workflow "${TEST_NAME}" "${TEST_NAME_BASE}" for DIR in "dir1" "dir2"; do mkdir -p "${WORKFLOW_RUN_DIR}/${DIR}" echo "hello" > "${WORKFLOW_RUN_DIR}/${DIR}/moo" done run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" export PATH="${WORKFLOW_RUN_DIR}/bin:$PATH" # shellcheck disable=SC2029 ssh -n "${CYLC_TEST_HOST}" "mkdir -p 'cylc-run/${WORKFLOW_NAME}/'" rsync -a 'bin' "${CYLC_TEST_HOST}:cylc-run/${WORKFLOW_NAME}/" workflow_run_ok "${TEST_NAME}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/remote/09-restart-running-file-install.t0000664000175000017500000000565515202510242026555 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test restart remote init, file install and task messaging works for running tasks # The test works as follows: # - Starts workflow off with a remote starter task which will not complete # until restarted. It greps for updated file which will be remote installed on # restart. # - On start of this starter task, update the file that is included in # remote file install for starter task. # - Stop the workflow - this will leave starter task orphaned # - Restart, starter task should remote install, authentication keys will be # updated which is verified by checking task messaging works (checks for # "(received)succeeded" in the logs). export REQUIRE_PLATFORM='loc:remote fs:indep comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] install = changing-file [[events]] abort on inactivity timeout = True inactivity timeout = PT1M [scheduling] [[graph]] R1 = """ starter:start => file-changer => stopper """ [runtime] [[starter]] platform = ${CYLC_TEST_PLATFORM} script = """ while ! grep 'Restart Play' "${CYLC_WORKFLOW_RUN_DIR}/changing-file"; do sleep 1 done """ [[stopper]] script = cylc stop --now "${CYLC_WORKFLOW_ID}" [[file-changer]] script = echo Restart Play > ${CYLC_WORKFLOW_RUN_DIR}/changing-file __FLOW_CONFIG__ echo "First Play" > "${WORKFLOW_RUN_DIR}/changing-file" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-start" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" grep_ok "remote file install complete" "${LOG}" grep_ok "\[1/starter/01:running\] (received)succeeded" "${LOG}" ls "${WORKFLOW_RUN_DIR}/log/remote-install" > 'ls.out' cmp_ok ls.out <<__RLOGS__ 01-start-${CYLC_TEST_INSTALL_TARGET}.log 02-restart-${CYLC_TEST_INSTALL_TARGET}.log __RLOGS__ purge exit cylc-flow-8.6.4/tests/functional/remote/04-symlink-dirs/0000775000175000017500000000000015202510242023252 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/04-symlink-dirs/flow.cylc0000664000175000017500000000030515202510242025073 0ustar alastairalastair#!jinja2 [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = holder => held [runtime] [[holder]] script = true platform = {{CYLC_TEST_PLATFORM}} cylc-flow-8.6.4/tests/functional/remote/test_header0000777000175000017500000000000015202510242026721 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/02-install-target.t0000664000175000017500000000331415202510242023743 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote installation only happens when appropriate export REQUIRE_PLATFORM='loc:remote fs:shared comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' #!jinja2 [scheduling] [[graph]] graph = remote [runtime] [[remote]] # this should not require remote-init because the platform # has a shared filesystem (same install target) script = true platform = {{CYLC_TEST_PLATFORM}} __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --debug \ --no-detach \ "${WORKFLOW_NAME}" -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" grep_ok "REMOTE INIT NOT REQUIRED for localhost" "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge exit cylc-flow-8.6.4/tests/functional/remote/07-slow-file-install/0000775000175000017500000000000015202510242024175 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/07-slow-file-install/bin/0000775000175000017500000000000015202510242024745 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/remote/07-slow-file-install/bin/my-rsync.sh0000775000175000017500000000005515202510242027065 0ustar alastairalastair#!/usr/bin/env bash sleep 30 exec rsync "$@" cylc-flow-8.6.4/tests/functional/remote/07-slow-file-install/flow.cylc0000664000175000017500000000122015202510242026013 0ustar alastairalastair[scheduler] install = dir1/, dir2/ [[events]] abort on stall timeout = true stall timeout = PT0S abort on inactivity timeout = true [scheduling] [[graph]] R1 = olaf => sven [runtime] [[olaf]] # task dependent on file install already being complete script = """ cat ${CYLC_WORKFLOW_RUN_DIR}/dir1/moo """ platform = $CYLC_TEST_PLATFORM [[sven]] # task dependent on file install already being complete script = """ rm -r ${CYLC_WORKFLOW_RUN_DIR}/dir1 ${CYLC_WORKFLOW_RUN_DIR}/dir2 """ platform = $CYLC_TEST_PLATFORM cylc-flow-8.6.4/tests/functional/remote/04-symlink-dirs.t0000664000175000017500000001203315202510242023436 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Checks configured symlinks are created for run, work, share, share/cycle, log # # directories on localhost and the remote platform. export REQUIRE_PLATFORM='loc:remote comms:tcp fs:indep' . "$(dirname "$0")/test_header" if [[ -z ${TMPDIR:-} || -z ${USER:-} || $TMPDIR/$USER == "$HOME" ]]; then skip_all '"TMPDIR" or "USER" not defined or "TMPDIR"/"USER" is "HOME"' fi set_test_number 14 create_test_global_config "" " [install] [[symlink dirs]] [[[localhost]]] run = \$TMPDIR/\$USER/cylctb_tmp_run_dir share = \$TMPDIR/\$USER log = \$TMPDIR/\$USER log/job = \$TMPDIR/\$USER/cylctb_tmp_log_job_dir share/cycle = \$TMPDIR/\$USER/cylctb_tmp_share_dir work = \$TMPDIR/\$USER [[[$CYLC_TEST_INSTALL_TARGET]]] run = \$TMPDIR/\$USER/test_cylc_symlink/ctb_tmp_run_dir share = \$TMPDIR/\$USER/test_cylc_symlink/ log = \$TMPDIR/\$USER/test_cylc_symlink/ log/job = \$TMPDIR/\$USER/cylctb_tmp_log_job_dir share/cycle = \$TMPDIR/\$USER/test_cylc_symlink/ctb_tmp_share_dir work = \$TMPDIR/\$USER/test_cylc_symlink/ " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run-ok" cylc play "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" --debug poll_grep_workflow_log 'remote file install complete' TEST_SYM="${TEST_NAME_BASE}-run-symlink-exists-ok" if [[ $(readlink "$HOME/cylc-run/${WORKFLOW_NAME}") == \ "$TMPDIR/$USER/cylctb_tmp_run_dir/cylc-run/${WORKFLOW_NAME}" ]]; then ok "$TEST_SYM.localhost" else fail "$TEST_SYM.localhost" fi TEST_SYM="${TEST_NAME_BASE}-share/cycle-symlink-exists-ok" if [[ $(readlink "$HOME/cylc-run/${WORKFLOW_NAME}/share/cycle") == \ "$TMPDIR/$USER/cylctb_tmp_share_dir/cylc-run/${WORKFLOW_NAME}/share/cycle" ]]; then ok "$TEST_SYM.localhost" else fail "$TEST_SYM.localhost" fi TEST_SYM="${TEST_NAME_BASE}-log/job-symlink-exists-ok" if [[ $(readlink "$HOME/cylc-run/${WORKFLOW_NAME}/log/job") == \ "$TMPDIR/$USER/cylctb_tmp_log_job_dir/cylc-run/${WORKFLOW_NAME}/log/job" ]]; then ok "$TEST_SYM.localhost" else fail "$TEST_SYM.localhost" fi for DIR in 'work' 'share' 'log'; do TEST_SYM="${TEST_NAME_BASE}-${DIR}-symlink-exists-ok" if [[ $(readlink "$HOME/cylc-run/${WORKFLOW_NAME}/${DIR}") == \ "$TMPDIR/$USER/cylc-run/${WORKFLOW_NAME}/${DIR}" ]]; then ok "$TEST_SYM.localhost" else fail "$TEST_SYM.localhost" fi done SSH="$(cylc config -d -i "[platforms][$CYLC_TEST_PLATFORM]ssh command")" # shellcheck disable=SC2016 LINK="$(${SSH} "${CYLC_TEST_HOST}" 'readlink "$HOME/cylc-run/'"$WORKFLOW_NAME"'"')" if [[ "$LINK" == *"/test_cylc_symlink/ctb_tmp_run_dir/cylc-run/${WORKFLOW_NAME}" ]]; then ok "${TEST_NAME_BASE}-run-symlink-exists-ok.remotehost" else fail "${TEST_NAME_BASE}-run-symlink-exists-ok.remotehost" fi # shellcheck disable=SC2016 LINK="$(${SSH} "${CYLC_TEST_HOST}" 'readlink "$HOME/cylc-run/'"$WORKFLOW_NAME"/share/cycle'"')" if [[ "$LINK" == *"/test_cylc_symlink/ctb_tmp_share_dir/cylc-run/${WORKFLOW_NAME}/share/cycle" ]]; then ok "${TEST_NAME_BASE}-share/cycle-symlink-exists-ok.remotehost" else fail "${TEST_NAME_BASE}-share/cycle-symlink-exists-ok.remotehost" fi # shellcheck disable=SC2016 LINK="$(${SSH} "${CYLC_TEST_HOST}" 'readlink "$HOME/cylc-run/'"$WORKFLOW_NAME"/log/job'"')" if [[ "$LINK" == *"/cylctb_tmp_log_job_dir/cylc-run/${WORKFLOW_NAME}/log/job" ]]; then ok "${TEST_NAME_BASE}-log/job-symlink-exists-ok.remotehost" else fail "${TEST_NAME_BASE}-log/job-symlink-exists-ok.remotehost" fi for DIR in 'work' 'share' 'log'; do # shellcheck disable=SC2016 LINK="$(${SSH} "${CYLC_TEST_HOST}" 'readlink "$HOME/cylc-run/'"$WORKFLOW_NAME"/$DIR'"')" if [[ "$LINK" == *"/test_cylc_symlink/cylc-run/${WORKFLOW_NAME}/${DIR}" ]]; then ok "${TEST_NAME_BASE}-${DIR}-symlink-exists-ok.remotehost" else fail "${TEST_NAME_BASE}-${DIR}-symlink-exists-ok.remotehost" fi done # clean up remote ${SSH} "${CYLC_TEST_HOST}" rm -rf "${TMPDIR}/${USER}/test_cylc_symlink/" purge exit cylc-flow-8.6.4/tests/functional/remote/06-poll.t0000664000175000017500000000367115202510242021771 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote host settings. export REQUIRE_PLATFORM='loc:remote comms:poll' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] retrieve job logs = True " #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" <<__HERE__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = cylc message -- foo platform = $CYLC_TEST_PLATFORM [[[outputs]]] foo = foo __HERE__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play "${WORKFLOW_NAME}" \ --debug \ --no-detach log_scan \ "${TEST_NAME_BASE}-poll" \ "$(cylc cat-log -m p "$WORKFLOW_NAME")" \ 10 \ 1 \ '\[1/foo.* (polled)foo' \ '\[1/foo.* (polled)succeeded' purge exit cylc-flow-8.6.4/tests/functional/remote/08-symlink-dir-target-exist.t0000664000175000017500000000402015202510242025672 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test remote init fails if symlink dir target already exists export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" set_test_number 5 SSH_CMD="$(cylc config -d -i "[platforms][${CYLC_TEST_PLATFORM}]ssh command")" create_test_global_config "" " [install] [[symlink dirs]] [[[${CYLC_TEST_INSTALL_TARGET}]]] run = \$TMPDIR/\$USER/sym-run " install_workflow "${TEST_NAME_BASE}" basic run_ok "${TEST_NAME_BASE}-val" cylc validate "$WORKFLOW_NAME" # Run once to setup symlink dirs on remote install target workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "$WORKFLOW_NAME" # Remove remote run dir symlink (but not its target) $SSH_CMD "$CYLC_TEST_HOST" "rm -rf ~/cylc-run/${WORKFLOW_NAME}" # New run should abort delete_db TEST_NAME="${TEST_NAME_BASE}-run-again" workflow_run_fail "$TEST_NAME" cylc play --no-detach "$WORKFLOW_NAME" grep_ok "ERROR - platform: .* initialisation did not complete" "${TEST_NAME}.stderr" grep_ok "WorkflowFilesError: Symlink dir target already exists" "${TEST_NAME}.stderr" # Clean up remote symlink dir target # shellcheck disable=SC2016 $SSH_CMD "$CYLC_TEST_HOST" 'rm -rf "${TMPDIR}/${USER}/sym-run"' purge cylc-flow-8.6.4/tests/functional/remote/00-basic.t0000664000175000017500000000460515202510242022074 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote host settings. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] retrieve job logs = True " #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" basic #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-platform sqlite3 "${WORKFLOW_RUN_DIR}/log/db" \ "SELECT platform_name FROM task_jobs WHERE name=='foo'" >'foo-host.txt' cmp_ok 'foo-host.txt' <<<"${CYLC_TEST_PLATFORM}" #------------------------------------------------------------------------------- # Check that the remote job has actually been run on the correct remote by # checking it's job.out file for @CYLC_TEST_HOST REMOTE_HOST_FQDN="$(ssh "${CYLC_TEST_HOST}" hostname -f)" TEST_NAME=${TEST_NAME_BASE}-ensure-remote-run grep_ok \ "^$REMOTE_HOST_FQDN" \ "${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job.out" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-remove/0000775000175000017500000000000015202510242021336 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-remove/00-simple.t0000775000175000017500000000164715202510242023244 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc remove . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-remove/02-cycling/0000775000175000017500000000000015202510242023205 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-remove/02-cycling/reference.log0000664000175000017500000000045615202510242025653 0ustar alastairalastairRun mode: live Initial point: 2020 Final point: 2021 Cold Start 2020 2020/remover -triggered off [] 2020/foo -triggered off [] 2021/foo -triggered off [] 2020/bar -triggered off ['2020/foo'] 2021/bar -triggered off ['2021/foo'] 2020/baz -triggered off ['2020/foo'] 2021/baz -triggered off ['2021/foo'] cylc-flow-8.6.4/tests/functional/cylc-remove/02-cycling/flow.cylc0000664000175000017500000000205315202510242025030 0ustar alastairalastair# Abort on stall timeout unless we successfully remove some failed and waiting tasks. [scheduler] UTC mode = True cycle point format = %Y [[events]] stall timeout = PT30S abort on stall timeout = True expected task failures = 2020/bar, 2021/baz [scheduling] initial cycle point = 2020 final cycle point = 2021 [[graph]] R1 = remover P1Y = foo => bar & baz => waz [runtime] [[remover]] script = """ cylc__job__poll_grep_workflow_log -E '2020/bar/01.* failed' cylc__job__poll_grep_workflow_log -E '2021/baz/01.* failed' # Remove the two unhandled failed tasks. cylc remove "$CYLC_WORKFLOW_ID//*/ba*:failed" # Remove the two unsatisfied waiting tasks. cylc remove "$CYLC_WORKFLOW_ID//*/waz" # Exit so workflow can shut down. """ [[foo, waz]] script = true [[bar]] script = [[ $CYLC_TASK_CYCLE_POINT != 2020 ]] [[baz]] script = [[ $CYLC_TASK_CYCLE_POINT != 2021 ]] cylc-flow-8.6.4/tests/functional/cylc-remove/04-kill/0000775000175000017500000000000015202510242022512 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-remove/04-kill/reference.log0000664000175000017500000000016415202510242025154 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/remover -triggered off ['1/a', '1/b'] cylc-flow-8.6.4/tests/functional/cylc-remove/04-kill/flow.cylc0000664000175000017500000000221015202510242024330 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] expected task failures = 1/a, 1/b stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = """ a:started => remover a:failed => u b:submitted? => remover b:submit-failed? => v """ [runtime] [[a, b]] script = sleep 40 [[[events]]] submitted handlers = echo %(event)s failed handlers = echo %(event)s submission failed handlers = echo %(event)s [[b]] platform = old_street [[remover]] script = """ cylc remove "$CYLC_WORKFLOW_ID//1/a" "$CYLC_WORKFLOW_ID//1/b" # Task proxies become "transient" on removal (it means, not in the # task pool), after which the job kill will be logged but the # state change to "failed" or "sumbit-failed" will not (we don't # care about the state of removed tasks). cylc__job__poll_grep_workflow_log -E '1\/a.* job killed' cylc__job__poll_grep_workflow_log -E '1\/b.* job killed' """ cylc-flow-8.6.4/tests/functional/cylc-remove/04-kill.t0000664000175000017500000000502215202510242022676 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test that removing submited/running tasks causes them to be killed. # Any downstream tasks that depend on the `:submit-fail`/`:fail` outputs # should NOT run. # Handlers for the `submission failed`/`failed` events should not run either. export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 10 # Create platform that ensures job b will be in submitted state for long enough create_test_global_config '' " [platforms] [[old_street]] job runner = at job runner command template = at now + 5 minutes hosts = localhost install target = localhost " install_and_validate reftest_run grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a" \ "[1/a/01(flows=none):failed(held)] job killed" -F J_LOG_A="${WORKFLOW_RUN_DIR}/log/job/1/a/NN/job-activity.log" # Failed handler should not run: grep_fail "[(('event-handler-00', 'failed'), 1) out]" "$J_LOG_A" -F # (Check submitted handler as a control): grep_ok "[(('event-handler-00', 'submitted'), 1) out]" "$J_LOG_A" -F grep_workflow_log_ok "${TEST_NAME_BASE}-grep-b" \ "[1/b/01(flows=none):submit-failed(held)] job killed" -F J_LOG_B="${WORKFLOW_RUN_DIR}/log/job/1/b/NN/job-activity.log" grep_fail "[(('event-handler-00', 'submission failed'), 1) out]" "$J_LOG_B" -F grep_ok "[(('event-handler-00', 'submitted'), 1) out]" "$J_LOG_B" -F # Check task state updated in DB despite removal from task pool: sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" \ "SELECT status, flow_nums FROM task_states WHERE name='a';" > task_states.out cmp_ok task_states.out - <<< "failed|[]" # Check job updated in DB: sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" \ "SELECT run_status, time_run_exit FROM task_jobs WHERE cycle='1' AND name='a';" > task_jobs.out cmp_ok_re task_jobs.out - <<< "1\|[\w:+-]+" purge cylc-flow-8.6.4/tests/functional/cylc-remove/03-flow.t0000664000175000017500000000317515202510242022720 0ustar alastairalastair#!/usr/bin/env bash # 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 . # High-level test of `cylc remove --flow` option. # Integration tests exist for more comprehensive coverage. . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" <<'__EOF__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo __EOF__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --pause run_ok "${TEST_NAME_BASE}-remove" cylc remove "${WORKFLOW_NAME}//1/foo" --flow 1 --flow 2 cylc stop "${WORKFLOW_NAME}" poll_workflow_stopped grep_workflow_log_ok "${TEST_NAME_BASE}-grep" "Removed tasks: 1/foo (flows=1)" # Simple additional test of DB: TEST_NAME="${TEST_NAME_BASE}-workflow-state" run_ok "$TEST_NAME" cylc workflow-state "$WORKFLOW_NAME" cmp_ok "${TEST_NAME}.stdout" <<__EOF__ 1/foo:waiting(flows=none) __EOF__ purge cylc-flow-8.6.4/tests/functional/cylc-remove/test_header0000777000175000017500000000000015202510242027653 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-remove/02-cycling.t0000775000175000017500000000173615202510242023404 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc remove in a cycling workflow (spawn before remove, and not). . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-remove/00-simple/0000775000175000017500000000000015202510242023044 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-remove/00-simple/reference.log0000664000175000017500000000015515202510242025506 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/cleaner -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-remove/00-simple/flow.cylc0000664000175000017500000000141115202510242024664 0ustar alastairalastair# Abort on stall timeout unless we remove unhandled failed and waiting task. [scheduler] [[events]] stall timeout = PT30S abort on stall timeout = True expected task failures = 1/b [scheduling] [[graph]] R1 = """ a => b => c cleaner """ [runtime] [[a,c]] script = true [[b]] script = false [[cleaner]] script = """ cylc__job__poll_grep_workflow_log -E '1/b/01.* failed' # Remove the unhandled failed task cylc remove "$CYLC_WORKFLOW_ID//1/b" # Remove waiting 1/c # (not auto-removed because parent 1/b, an unhandled fail, is not finished.) cylc remove "$CYLC_WORKFLOW_ID//1/c:waiting" """ cylc-flow-8.6.4/tests/functional/cylc-ping/0000775000175000017500000000000015202510242020776 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-ping/00-simple.t0000664000175000017500000000165715202510242022702 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc ping behaviour . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-ping/03-check-keys-local.t0000664000175000017500000000337215202510242024526 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Checks ZMQ keys are created and deleted on shutdown - local. . "$(dirname "$0")/test_header" set_test_number 10 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] cycle point format = %Y allow implicit tasks = True [scheduling] initial cycle point = 2020 [[graph]] R1 = t1 __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" SRVD="${WORKFLOW_RUN_DIR}/.service" workflow_run_ok "${TEST_NAME_BASE}-run-pause" cylc play --pause "${WORKFLOW_NAME}" exists_ok "${SRVD}/client.key_secret" exists_ok "${SRVD}/server.key_secret" exists_ok "${SRVD}/server.key" exists_ok "${SRVD}/client_public_keys/client_localhost.key" cylc stop --max-polls=60 --interval=1 "${WORKFLOW_NAME}" exists_fail "${SRVD}/client.key_secret" exists_fail "${SRVD}/server.key_secret" exists_fail "${SRVD}/server.key" exists_fail "${SRVD}/client_public_keys/client_localhost.key" purge exit cylc-flow-8.6.4/tests/functional/cylc-ping/04-check-keys-remote.t0000664000175000017500000000521115202510242024722 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Checks remote ZMQ keys are created and deleted on shutdown. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config '' " [platforms] [[$CYLC_TEST_PLATFORM]] retrieve job logs = True " init_workflow "${TEST_NAME_BASE}" <<__FLOW_CONFIG__ [scheduling] [[graph]] R1 = keys [runtime] [[keys]] platform = $CYLC_TEST_PLATFORM script = """ find \ "\${CYLC_WORKFLOW_RUN_DIR}" \ -type f \ -name "*key*" \ | awk -F/ '{print \$NF}'|sort > "\${CYLC_TASK_LOG_ROOT}-find-out" """ [[[environment]]] LANG = C __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate \ "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play \ "${WORKFLOW_NAME}" \ --no-detach KEYS_FILE="$(cylc cat-log -m p "$WORKFLOW_NAME//1/keys" -f job-find-out)" if [[ "$CYLC_TEST_PLATFORM" == *shared* ]]; then cmp_ok "$KEYS_FILE" <<__OUT__ client.key_secret client_${CYLC_TEST_INSTALL_TARGET}.key server.key server.key_secret __OUT__ else cmp_ok "$KEYS_FILE" <<__OUT__ client.key_secret client_${CYLC_TEST_INSTALL_TARGET}.key server.key __OUT__ fi if [[ "$CYLC_TEST_PLATFORM" == *shared* ]]; then skip 1 else # NOTE: remote tidy happens on a random platform picked from the install # target so might not be $CYLC_TEST_PLATFORM grep_ok \ "platform: .* - remote tidy (on $CYLC_TEST_HOST)" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" fi # ensure the keys got removed again afterwards SSH='ssh -n -oBatchMode=yes -oConnectTimeout=5' ${SSH} "${CYLC_TEST_HOST}" \ LANG=C find "cylc-run/${WORKFLOW_NAME}" -type f -name "*key*"|awk -F/ '{print $NF}'|sort >'find.out' cmp_ok 'find.out' inheritor [runtime] [[FAM1]] script = false [[FAM2]] script = true [[reloader]] script = """ # change the inheritance of inheritor: perl -pi -e 's/(inherit = )FAM1( # marker)/\1FAM2\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload $CYLC_WORKFLOW_ID sleep 5 """ [[inheritor]] inherit = FAM1 # marker cylc-flow-8.6.4/tests/functional/reload/24-reload-file-install/0000775000175000017500000000000015202510242024431 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/24-reload-file-install/flow.cylc0000664000175000017500000000067615202510242026265 0ustar alastairalastair[scheduler] install = changing-file [scheduling] [[graph]] R1 = "a => b => c" [runtime] [[a]] script = "cat ${CYLC_WORKFLOW_RUN_DIR}/changing-file" platform = $CYLC_TEST_PLATFORM [[b]] script = """echo goodbye > "${CYLC_WORKFLOW_RUN_DIR}/changing-file"; cylc reload $CYLC_WORKFLOW_ID""" [[c]] script = "cat ${CYLC_WORKFLOW_RUN_DIR}/changing-file" platform = $CYLC_TEST_PLATFORM cylc-flow-8.6.4/tests/functional/reload/13-add-task/0000775000175000017500000000000015202510242022270 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/13-add-task/reference.log0000664000175000017500000000020315202510242024724 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/foo -triggered off ['1/reloader'] 1/add_me -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/reload/13-add-task/flow.cylc0000664000175000017500000000067315202510242024121 0ustar alastairalastair[meta] title = "Test insertion of a task added by a reload." # Don't run this workflow in-place: it modifies itself. [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = reloader => foo [runtime] [[root]] script = true [[reloader]] script = """ sed -i "s/\(R1 = reloader => foo\)\s*$/\1 => add_me/" $CYLC_WORKFLOW_RUN_DIR/flow.cylc cylc reload $CYLC_WORKFLOW_ID sleep 10 """ cylc-flow-8.6.4/tests/functional/reload/10-runahead.t0000664000175000017500000000353515202510242022557 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test runahead limit is being correctly reloaded . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" runahead #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-check-fail DB_FILE="$RUN_DIR/${WORKFLOW_NAME}/log/db" QUERY="SELECT COUNT(*) FROM task_states WHERE status == 'failed'" cmp_ok <(sqlite3 "$DB_FILE" "$QUERY") <<< "4" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/reload/00-simple.t0000664000175000017500000000166715202510242022264 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reloading a simple workflow . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/02-content.t0000664000175000017500000000171715202510242022443 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reloads have actually updated environment variables . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/01-startup/0000775000175000017500000000000015202510242022277 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/01-startup/reference.log0000664000175000017500000000106415202510242024741 0ustar alastairalastairInitial point: 20100101T00 Final point: 20100102T00 20100101T00/start -triggered off [] 20100101T00/a -triggered off ['20091231T18/c', '20100101T00/start'] 20100101T00/b -triggered off ['20100101T00/a'] 20100101T00/c -triggered off ['20100101T00/b'] 20100101T06/a -triggered off ['20100101T00/c'] 20100101T06/b -triggered off ['20100101T06/a'] 20100101T06/c -triggered off ['20100101T06/b'] 20100101T18/c -triggered off [] 20100102T00/a -triggered off ['20100101T18/c'] 20100102T00/b -triggered off ['20100102T00/a'] 20100102T00/c -triggered off ['20100102T00/b'] cylc-flow-8.6.4/tests/functional/reload/01-startup/flow.cylc0000664000175000017500000000063615202510242024127 0ustar alastairalastair[scheduler] cycle point format = "%Y%m%dT%H" [scheduling] initial cycle point = 20100101T00 final cycle point = 20100102T00 [[graph]] R1 = "start => a" T00, T06 = "c[-PT6H] => a => b => c" T18 = "c" [runtime] [[a,c,start]] script = true [[b]] script = """ cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' """ cylc-flow-8.6.4/tests/functional/reload/21-submit-fail/0000775000175000017500000000000015202510242023013 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/21-submit-fail/flow.cylc0000664000175000017500000000066115202510242024641 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 1/t1 [scheduling] [[graph]] R1=""" t1:submit-fail => stopper reloader => stopper """ [runtime] [[t1]] script=true platform = platypus [[reloader]] script=cylc reload "${CYLC_WORKFLOW_ID}" [[stopper]] script=cylc stop "${CYLC_WORKFLOW_ID}" cylc-flow-8.6.4/tests/functional/reload/30-global-rollback/0000775000175000017500000000000015202510242023626 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/30-global-rollback/flow.cylc0000664000175000017500000000166115202510242025455 0ustar alastairalastair[scheduling] cycling mode = integer [[graph]] R1 = """ a => reload-global-error-workflow? => b & c """ [runtime] [[root]] script = true [[a,b]] platform = localhost [[c]] platform = $CYLC_TEST_PLATFORM [[reload-global-error-workflow]] platform = localhost script = """ # Append to global config cat >> "$CYLC_CONF_PATH/global.cylc" <> "$CYLC_WORKFLOW_RUN_DIR/flow.cylc" <. #------------------------------------------------------------------------------- # Test changing cycle times (and dependencies) . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger/0000775000175000017500000000000015202510242023714 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger/bin/0000775000175000017500000000000015202510242024464 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger/bin/stall-handler.sh0000775000175000017500000000105215202510242027553 0ustar alastairalastair#!/bin/bash # Change "script = false" -> "true" in 1/foo, then reload and retrigger it. if grep "\[command\] reload_workflow" "${CYLC_WORKFLOW_LOG_DIR}/log" >/dev/null; then # Abort if not the first call (avoid an endless loop if the reload does not # have the intended effect). >&2 echo "ERROR (stall-handler.sh): should only be called once" cylc stop --now --now "${CYLC_WORKFLOW_ID}" exit 1 fi sed -i "s/false/true/" "${CYLC_WORKFLOW_RUN_DIR}/suite.rc" cylc reload "${CYLC_WORKFLOW_ID}" cylc trigger "${CYLC_WORKFLOW_ID}//1/foo" cylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger/suite.rc0000664000175000017500000000055315202510242025376 0ustar alastairalastair# Use a stall handler to fix and reload the workflow config, then retrigger the # failed task, which should run successfully with the new settings. [scheduler] [[events]] stall handlers = stall-handler.sh expected task failures = 1/foo [scheduling] [[graph]] R1 = "foo => bar" [runtime] [[foo]] script = false [[bar]] cylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger/reference.log0000664000175000017500000000015515202510242026356 0ustar alastairalastair1/foo -triggered off [] in flow 1 1/foo -triggered off [] in flow 1 1/bar -triggered off ['1/foo'] in flow 1 cylc-flow-8.6.4/tests/functional/reload/17-graphing-change.t0000775000175000017500000000633115202510242024021 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that removing a task from the graph works OK. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 12 # shellcheck disable=SC2317 disable=SC2329 grep_workflow_log_n_times() { TEXT="$1" N_TIMES="$2" [[ $(grep -c "$TEXT" "${WORKFLOW_RUN_DIR}/log/scheduler/log") == "$N_TIMES" ]] } #------------------------------------------------------------------------------- # test reporting of added tasks # install workflow install_workflow "${TEST_NAME_BASE}" 'graphing-change' LOG_FILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # start workflow in paused mode run_ok "${TEST_NAME_BASE}-add-run" cylc play --debug --pause "${WORKFLOW_NAME}" # change the flow.cylc file cp "${TEST_SOURCE_DIR}/graphing-change/flow-1.cylc" \ "${RUN_DIR}/${WORKFLOW_NAME}/flow.cylc" # reload workflow run_ok "${TEST_NAME_BASE}-add-reload" cylc reload "${WORKFLOW_NAME}" poll grep_workflow_log_n_times 'Reload completed' 1 # check workflow log grep_ok "Added task: 'one'" "${LOG_FILE}" #------------------------------------------------------------------------------- # test reporting or removed tasks # change the flow.cylc file cp "${TEST_SOURCE_DIR}/graphing-change/flow.cylc" \ "${RUN_DIR}/${WORKFLOW_NAME}/flow.cylc" # reload workflow run_ok "${TEST_NAME_BASE}-remove-reload" cylc reload "${WORKFLOW_NAME}" poll grep_workflow_log_n_times 'Reload completed' 2 # check workflow log grep_ok "Removed task: 'one'" "${LOG_FILE}" #------------------------------------------------------------------------------- # test reporting of adding / removing / swapping tasks # change the flow.cylc file cp "${TEST_SOURCE_DIR}/graphing-change/flow-2.cylc" \ "${RUN_DIR}/${WORKFLOW_NAME}/flow.cylc" # Spawn a couple of task proxies, to get "task definition removed" message. cylc set "${WORKFLOW_NAME}//1/foo" cylc set "${WORKFLOW_NAME}//1/baz" # reload workflow run_ok "${TEST_NAME_BASE}-swap-reload" cylc reload "${WORKFLOW_NAME}" poll grep_workflow_log_n_times 'Reload completed' 3 # check workflow log grep_ok "Added task: 'one'" "${LOG_FILE}" grep_ok "Added task: 'add'" "${LOG_FILE}" grep_ok "Added task: 'boo'" "${LOG_FILE}" grep_ok "\\[1/bar.*\\].*task definition removed" "${LOG_FILE}" grep_ok "\\[1/bol.*\\].*task definition removed" "${LOG_FILE}" run_ok "${TEST_NAME_BASE}-stop" \ cylc stop --max-polls=10 --interval=2 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/reload/08-cycle/0000775000175000017500000000000015202510242021703 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/08-cycle/reference.log0000664000175000017500000000052315202510242024344 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100101T1800Z 20100101T0000Z/reloader -triggered off [] 20100101T0000Z/a -triggered off ['20091231T1800Z/a', '20100101T0000Z/reloader'] 20100101T0600Z/a -triggered off ['20100101T0000Z/a'] 20100101T1200Z/a -triggered off ['20100101T0600Z/a'] 20100101T1800Z/a -triggered off ['20100101T1200Z/a'] cylc-flow-8.6.4/tests/functional/reload/08-cycle/flow.cylc0000664000175000017500000000146715202510242023536 0ustar alastairalastair[meta] title = cycling period change description = """change cycle points""" [scheduler] UTC mode = True [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100101T18 [[graph]] T00 = reloader => a T00,T12 = a[-PT12H] => a [runtime] [[reloader]] script = """ # I should only run once. if ((CYLC_TASK_SUBMIT_NUMBER != 1)); then exit 1 fi sed -i 's/T00,T12 = a\[-PT12H\]/T00,T06,T12,T18 = a[-PT6H]/'\ "${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[a]] script = true cylc-flow-8.6.4/tests/functional/reload/16-remove-add-alter-task/0000775000175000017500000000000015202510242024673 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/16-remove-add-alter-task/reference.log0000664000175000017500000000022415202510242027332 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/inter -triggered off ['1/reloader'] 1/remove_add_alter_me -triggered off ['1/inter'] cylc-flow-8.6.4/tests/functional/reload/16-remove-add-alter-task/flow.cylc0000664000175000017500000000157315202510242026524 0ustar alastairalastair[meta] title = "Test reloading of a task removed then added by a reload." # Don't run this workflow in-place: it modifies itself. [scheduler] UTC mode = True allow implicit tasks = True [scheduling] [[graph]] R1 = reloader => inter => remove_add_alter_me [runtime] [[remove_add_alter_me]] script = false [[reloader]] script = """ do_reload() { cylc reload "${CYLC_WORKFLOW_ID}" while test "$(grep -cF 'Reload completed' "${CYLC_WORKFLOW_LOG_DIR}/log")" -ne "$1" do sleep 1 done } sed -i "s/\(R1 = reloader => inter\).*/\1/" "${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" do_reload 1 sed -i "s/\(R1 = reloader => inter\)/\1 => remove_add_alter_me/" \ "${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" do_reload 2 cat >>"${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" <<'__RUNTIME__' [[remove_add_alter_me]] script = true __RUNTIME__ do_reload 3 """ cylc-flow-8.6.4/tests/functional/reload/18-broadcast-insert/0000775000175000017500000000000015202510242024051 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/18-broadcast-insert/flow-2.cylc0000664000175000017500000000013515202510242026032 0ustar alastairalastair[scheduling] [[graph]] R1=foo&bar [runtime] [[foo,bar]] script=true cylc-flow-8.6.4/tests/functional/reload/18-broadcast-insert/reference.log0000664000175000017500000000012015202510242026503 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off [] cylc-flow-8.6.4/tests/functional/reload/18-broadcast-insert/flow.cylc0000664000175000017500000000067715202510242025706 0ustar alastairalastair[scheduling] [[graph]] R1=foo [runtime] [[foo]] script=""" cylc broadcast "${CYLC_WORKFLOW_ID}" \ -s '[environment]CYLC_TEST_VAR=1' cp -p \ "${CYLC_WORKFLOW_RUN_DIR}/flow-2.cylc" \ "${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" cylc reload "${CYLC_WORKFLOW_ID}" sleep 5 cylc trigger "${CYLC_WORKFLOW_ID}//1/bar" """ cylc-flow-8.6.4/tests/functional/reload/05-graphing-simple.t0000664000175000017500000000170015202510242024052 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test changing basic graphing via a reload . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/18-broadcast-insert.t0000775000175000017500000000171615202510242024246 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reload with new task + broadcast + insert new task. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/graphing-change/0000775000175000017500000000000015202510242023401 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/graphing-change/flow-1.cylc0000664000175000017500000000040515202510242025361 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 1 [[graph]] R1 = start => one P1 = foo => bar => baz => bol [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/reload/graphing-change/flow-2.cylc0000664000175000017500000000040515202510242025362 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 1 [[graph]] R1 = start => one P1 = add => foo => boo => baz [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/reload/graphing-change/flow.cylc0000664000175000017500000000037615202510242025232 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 1 [[graph]] R1 = start P1 = foo => bar => baz => bol [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/reload/29-global/0000775000175000017500000000000015202510242022047 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/29-global/flow.cylc0000664000175000017500000000155415202510242023677 0ustar alastairalastair[scheduling] cycling mode = integer [[graph]] R1 = a => reload-global => b & c [runtime] [[root]] script = true [[a,b]] platform = localhost [[c]] platform = $CYLC_TEST_PLATFORM [[reload-global]] platform = localhost script = """ # Append to global config cat >> "$CYLC_CONF_PATH/global.cylc" < waiter sleeping-waiter => waiter sleeping-waiter:start => reloader """ [runtime] [[sleeping-waiter, starter]] script = """ touch 'file' while [[ -e 'file' ]]; do sleep 1 done """ [[waiter]] script = true [[reloader]] script = """ cylc__job__wait_cylc_message_started cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -E '1/waiter.* reloaded task definition' rm -f "${CYLC_WORKFLOW_WORK_DIR}/1/sleeping-waiter/file" rm -f "${CYLC_WORKFLOW_WORK_DIR}/1/starter/file" """ cylc-flow-8.6.4/tests/functional/reload/03-queues.t0000664000175000017500000000172115202510242022274 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test changing queue size via reload doesn't break anything . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/04-inheritance.t0000664000175000017500000000167515202510242023267 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test changing inheritance via a reload . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/02-content/0000775000175000017500000000000015202510242022250 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/02-content/reference.log0000664000175000017500000000014115202510242024705 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/foo -triggered off ['1/reloader'] cylc-flow-8.6.4/tests/functional/reload/02-content/flow.cylc0000664000175000017500000000111415202510242024070 0ustar alastairalastair[meta] title = content reload test description = """two tasks: the second will fail, causing the test to fail, unless the first reloads the workflow definition after modifying it.""" [scheduling] [[graph]] R1 = "reloader => foo" [runtime] [[reloader]] script = """ # change the value of $FALSE to "true" in foo's environment: perl -pi -e 's/(FALSE = )false( # marker)/\1true\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload $CYLC_WORKFLOW_ID """ [[foo]] script = "$FALSE" [[[environment]]] FALSE = false # marker cylc-flow-8.6.4/tests/functional/reload/12-remove-task.t0000664000175000017500000000171115202510242023221 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that removing a task from the graph works OK. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/13-add-task.t0000664000175000017500000000170515202510242022460 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that adding a task to the graph works OK. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/06-graphing-fam.t0000664000175000017500000000170315202510242023330 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test changing family triggering via a reload . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/garbage/0000775000175000017500000000000015202510242021747 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/garbage/reference.log0000664000175000017500000000014115202510242024404 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/foo -triggered off ['1/reloader'] cylc-flow-8.6.4/tests/functional/reload/garbage/flow.cylc0000664000175000017500000000052215202510242023571 0ustar alastairalastair[scheduling] [[graph]] # marker R1 = reloader => foo [runtime] [[reloader]] script = """ sleep 5 # change the dependencies section name to garbage: perl -pi -e 's/(\[\[)graph(\]\] # marker)/\1garbage\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload $CYLC_WORKFLOW_ID """ [[foo]] script = true cylc-flow-8.6.4/tests/functional/reload/20-stop-point/0000775000175000017500000000000015202510242022712 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/20-stop-point/reference.log0000664000175000017500000000032315202510242025351 0ustar alastairalastairInitial point: 1 Final point: 5 1/set-stop-point -triggered off [] 1/reload -triggered off ['1/set-stop-point'] 1/t1 -triggered off ['0/t1', '1/reload'] 2/t1 -triggered off ['1/t1'] 3/t1 -triggered off ['2/t1'] cylc-flow-8.6.4/tests/functional/reload/20-stop-point/flow.cylc0000664000175000017500000000124215202510242024534 0ustar alastairalastair#!Jinja2 # [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 5 [[graph]] R1 = "set-stop-point => reload => t1" P1 = "t1[-P1] => t1" [runtime] [[set-stop-point]] script = cylc stop "${CYLC_WORKFLOW_ID}//3" [[reload]] script = """ cylc__job__wait_cylc_message_started cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[[job]]] execution time limit = PT1M [[t1]] script = true cylc-flow-8.6.4/tests/functional/reload/27-stall-retrigger.t0000664000175000017500000000207515202510242024105 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test retriggering a failed task after fixing the bug and reloading. # It should run correctly with the updated settings. # https://github.com/cylc/cylc-flow/issues/5103 . "$(dirname "$0")/test_header" set_test_number 2 reftest cylc-flow-8.6.4/tests/functional/reload/15-state-summary.t0000664000175000017500000000433315202510242023605 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that the state summary updates immediately when a reload finishes. # (SoD: the original test contrived to get a succeeded and a failed task in the # pool, and no active tasks. That's not possible under SoD, and it seems to me # a trivial paused workflow should do to test that the state summary updates after a # reload when nothing else is happening). # See https://github.com/cylc/cylc-flow/pull/1756 . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = true __FLOW_CONFIG__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- cylc play --pause "${WORKFLOW_NAME}" > /dev/null 2>&1 sleep 5 cylc reload "${WORKFLOW_NAME}" sleep 5 cylc dump "${WORKFLOW_NAME}" > dump.out TEST_NAME=${TEST_NAME_BASE}-grep # State summary should not say "reloaded = True" grep_ok "reloaded=False" dump.out #------------------------------------------------------------------------------- cylc stop --now --now "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/reload/07-final-cycle/0000775000175000017500000000000015202510242022771 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/07-final-cycle/reference.log0000664000175000017500000000043615202510242025435 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100102T0000Z 20100101T0000Z/reloader -triggered off [] 20100101T0000Z/a -triggered off ['20091231T1800Z/a', '20100101T0000Z/reloader'] 20100101T0600Z/a -triggered off ['20100101T0000Z/a'] 20100101T1200Z/a -triggered off ['20100101T0600Z/a'] cylc-flow-8.6.4/tests/functional/reload/07-final-cycle/flow.cylc0000664000175000017500000000113715202510242024616 0ustar alastairalastair[meta] title = final cycle reload test description = """change final cycle.""" [scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100102T00 # marker [[graph]] R1 = "reloader => a" PT6H = "a[-PT6H] => a" [runtime] [[reloader]] script = """ # change the final cycle: perl -pi -e 's/(final cycle point = )20100102T00( # marker)/\1 20100101T12\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload $CYLC_WORKFLOW_ID cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[a]] script = true cylc-flow-8.6.4/tests/functional/reload/06-graphing-fam/0000775000175000017500000000000015202510242023142 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/06-graphing-fam/reference.log0000664000175000017500000000063715202510242025611 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/inter -triggered off ['1/reloader'] 1/a -triggered off ['1/inter'] 1/c -triggered off ['1/inter'] 1/b -triggered off ['1/inter'] 1/d -triggered off ['1/inter'] 1/e -triggered off ['1/a', '1/b', '1/c', '1/d'] 1/g -triggered off ['1/a', '1/b', '1/c', '1/d'] 1/f -triggered off ['1/a', '1/b', '1/c', '1/d'] 1/h -triggered off ['1/a', '1/b', '1/c', '1/d'] cylc-flow-8.6.4/tests/functional/reload/06-graphing-fam/flow.cylc0000664000175000017500000000150215202510242024763 0ustar alastairalastair[meta] title = queue size reload test description = """change family triggering order via a reload.""" [scheduling] [[graph]] R1 = """ reloader => inter => BAR? # marker1 BAR:finish-all => FOO # marker2 """ [runtime] [[inter]] [[reloader]] script = """ # change the order of FOO and BAR in the graphing section: perl -pi -e 's/(reloader => inter => )BAR\?( # marker1)/\1FOO?\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc perl -pi -e 's/( )BAR:finish-all => FOO( # marker2)/\1FOO:finish-all => BAR\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[FOO, BAR]] script = true [[a,b,c,d]] inherit = FOO [[e,f,g,h]] inherit = BAR cylc-flow-8.6.4/tests/functional/reload/19-remote-kill/0000775000175000017500000000000015202510242023032 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/19-remote-kill/reference.log0000664000175000017500000000012715202510242025473 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/reload/19-remote-kill/flow.cylc0000664000175000017500000000127115202510242024656 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 1/foo [scheduling] [[graph]] R1 = foo:start => bar [runtime] [[bar]] script = """ cylc__job__wait_cylc_message_started cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' cylc kill "${CYLC_WORKFLOW_ID}//1/foo" cylc__job__poll_grep_workflow_log -E '1/foo/01:failed\(held\).* job killed' """ [[[job]]] execution time limit = PT1M [[foo]] script=sleep 61 platform = {{ CYLC_TEST_PLATFORM }} cylc-flow-8.6.4/tests/functional/reload/11-retrying/0000775000175000017500000000000015202510242022441 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/11-retrying/reference.log0000664000175000017500000000016515202510242025104 0ustar alastairalastairInitial point: 1 Final point: 1 1/retrier -triggered off [] 1/reloader -triggered off [] 1/retrier -triggered off [] cylc-flow-8.6.4/tests/functional/reload/11-retrying/flow.cylc0000664000175000017500000000210215202510242024257 0ustar alastairalastair[meta] title = "test that a reloaded retrying task does retry" description = """ this requires some state vars to be carried over to the new task proxy; ref github #945 """ [scheduling] [[graph]] R1 = retrier & reloader [runtime] [[retrier]] script = """ cylc__job__wait_cylc_message_started sleep 1 if ((CYLC_TASK_TRY_NUMBER == 1)); then # Kill the job, so task will go into waiting (held) cylc kill "${CYLC_WORKFLOW_ID}//1/retrier" sleep 120 # Does not matter how long as the job will be killed fi """ [[[job]]] execution retry delays = PT0S [[reloader]] script = """ cylc__job__poll_grep_workflow_log -E '1/retrier/01:running\(held\).* => waiting\(held\)' cylc reload "${CYLC_WORKFLOW_ID}" cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' cylc release "${CYLC_WORKFLOW_ID}//1/retrier" """ cylc-flow-8.6.4/tests/functional/reload/11-retrying.t0000664000175000017500000000172515202510242022633 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that a reloaded retrying does does retry; ref github #945 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/test_header0000777000175000017500000000000015202510242026674 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/01-startup.t0000664000175000017500000000171315202510242022466 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reloading a workflow containing a startup task. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/26-stalled.t0000664000175000017500000000355415202510242022430 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ---------------------------------------------------------------------------- # Test reloading a stalled workflow: it should not stall again # https://github.com/cylc/cylc-flow/issues/5103 . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__FLOW__' [scheduler] [[events]] stall handlers = cylc reload %(workflow)s stall timeout = PT30S abort on stall timeout = True # Prevent infinite loop if the bug resurfaces workflow timeout = PT3M abort on workflow timeout = True [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = false __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach LOG_FILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Should only stall once count_ok "CRITICAL - Workflow stalled" "$LOG_FILE" 1 # Stall event handler should only run once count_ok "INFO - Reload completed" "$LOG_FILE" 1 # Stall timer should not stop at any point grep_fail "stall timer stopped" "$LOG_FILE" purge cylc-flow-8.6.4/tests/functional/reload/05-graphing-simple/0000775000175000017500000000000015202510242023667 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/05-graphing-simple/reference.log0000664000175000017500000000024315202510242026327 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/inter -triggered off ['1/reloader'] 1/foo -triggered off ['1/inter'] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/reload/05-graphing-simple/flow.cylc0000664000175000017500000000112015202510242025504 0ustar alastairalastair[meta] title = queue size reload test description = """change graphing order via a reload.""" [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = reloader => inter => bar => foo # marker [runtime] [[reloader]] script = """ # change the order of foo and bar in the graphing section: perl -pi -e 's/(R1 = reloader => inter => )bar => foo( # marker)/\1foo => bar\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[foo, bar]] script = true cylc-flow-8.6.4/tests/functional/reload/07-final-cycle.t0000664000175000017500000000166615202510242023167 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test changing final cycle point . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/23-cycle-point-time-zone.t0000664000175000017500000000351715202510242025127 0ustar alastairalastair#!/bin/bash # 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 . #------------------------------------------------------------------------------- # Test saving and loading of cycle point time zone to/from database on a run # followed by a reload. Important for reloading a workflow after a system # time zone change. . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" << '__FLOW__' [scheduler] cycle point time zone = +0100 allow implicit tasks = True [scheduling] initial cycle point = now [[graph]] R1 = foo __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Set time zone to +01:00 export TZ=BST-1 workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --pause poll_grep_workflow_log "Paused on start up" # Simulate DST change export TZ=UTC run_ok "${TEST_NAME_BASE}-reload" cylc reload "${WORKFLOW_NAME}" poll_grep_workflow_log "Reload completed" cylc stop --now --now "${WORKFLOW_NAME}" log_scan "${TEST_NAME_BASE}-log-scan" "${WORKFLOW_RUN_DIR}/log/scheduler/log" 1 0 \ 'LOADING saved workflow parameters' \ '+ cycle point time zone = +0100' purge cylc-flow-8.6.4/tests/functional/reload/16-remove-add-alter-task.t0000664000175000017500000000173615202510242025067 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that removing, adding, then altering a task in the graph works OK. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/reload/29-global.t0000664000175000017500000000372215202510242022240 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reloading global configuration . "$(dirname "$0")/test_header" set_test_number 9 create_test_global_config "" "" TEST_NAME="${TEST_NAME_BASE}" install_workflow "${TEST_NAME}" "${TEST_NAME_BASE}" # Validate the config run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" # Run the workflow workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach -v # Reload happened grep_ok "Reloading the global configuration" "${WORKFLOW_RUN_DIR}/log/scheduler/01-start-01.log" # Reload hasn't happened in job a, has happened in b and c grep_fail "global init-script reloaded!" "${WORKFLOW_RUN_DIR}/log/job/1/a/01/job.out" grep_ok "global init-script reloaded!" "${WORKFLOW_RUN_DIR}/log/job/1/b/01/job.out" grep_ok "global init-script reloaded!" "${WORKFLOW_RUN_DIR}/log/job/1/c/01/job.out" # Events are original in job a, updated in b and c grep_fail "!!EVENT!! succeeded 1/a" "${WORKFLOW_RUN_DIR}/log/scheduler/01-start-01.log" grep_ok "!!EVENT!! succeeded 1/b" "${WORKFLOW_RUN_DIR}/log/scheduler/01-start-01.log" grep_ok "!!EVENT!! succeeded 1/c" "${WORKFLOW_RUN_DIR}/log/scheduler/01-start-01.log" purge exit cylc-flow-8.6.4/tests/functional/reload/09-garbage.t0000664000175000017500000000350615202510242022366 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test trying to reload a file with invalid dependencies section header . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'garbage' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- grep_ok \ 'Reload failed - IllegalItemError: \[scheduling\]garbage' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/reload/runahead/0000775000175000017500000000000015202510242022146 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/runahead/flow.cylc0000664000175000017500000000137215202510242023774 0ustar alastairalastair[scheduler] UTC mode = True [[events]] stall timeout = PT0.2M abort on stall timeout = True [scheduling] initial cycle point = 2010-01-01 final cycle point = 2010-01-05 runahead limit = P1 # marker [[graph]] # oops is stuck waiting task to hold back runahead R1/T00 = "foo & reloader => oops" T00/PT6H = "foo => bar" [runtime] [[foo]] script = false [[bar, oops]] script = true [[reloader]] script = """ cylc__job__poll_grep_workflow_log -E "${CYLC_TASK_CYCLE_POINT}/foo/01:running.*failed" perl -pi -e 's/(runahead limit = )P1( # marker)/\1 P3\2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc cylc reload $CYLC_WORKFLOW_ID """ cylc-flow-8.6.4/tests/functional/reload/12-remove-task/0000775000175000017500000000000015202510242023034 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/12-remove-task/reference.log0000664000175000017500000000020415202510242025471 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/inter -triggered off ['1/reloader'] 1/foo -triggered off ['1/inter'] cylc-flow-8.6.4/tests/functional/reload/12-remove-task/flow.cylc0000664000175000017500000000076215202510242024664 0ustar alastairalastair[meta] title = "test reloading after manually removing a task." [scheduling] [[graph]] R1 = """ reloader => inter => remove_me => foo """ [runtime] [[reloader]] script = """ sed -i "s/remove_me =>//g" $CYLC_WORKFLOW_RUN_DIR/flow.cylc cylc reload $CYLC_WORKFLOW_ID cylc__job__poll_grep_workflow_log -F 'Reload completed' """ [[remove_me]] script = false [[foo, inter]] script = true cylc-flow-8.6.4/tests/functional/reload/00-simple/0000775000175000017500000000000015202510242022065 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/00-simple/reference.log0000664000175000017500000000015415202510242024526 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/c -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/reload/00-simple/flow.cylc0000664000175000017500000000025015202510242023705 0ustar alastairalastair[scheduling] [[graph]] R1 = "a => b => c" [runtime] [[a,c]] script = "true" [[b]] script = "cylc reload $CYLC_WORKFLOW_ID; sleep 5" cylc-flow-8.6.4/tests/functional/reload/22-remove-task-cycling.t0000664000175000017500000000532215202510242024652 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # Test orphaned tasks do not stall the workflow after reload - GitHub #3306. . "$(dirname "$0")/test_header" set_test_number 3 # A workflow designed to orphan a single copy of a task 'bar' on self-reload, # or stall and abort if the orphaned task triggers the #3306 bug. init_workflow "${TEST_NAME_BASE}" <<__FLOW_CONFIG__ [scheduler] [[events]] inactivity timeout = PT25S abort on inactivity timeout = True [scheduling] initial cycle point = 1 final cycle point = 3 cycling mode = integer runahead limit = P1 [[dependencies]] [[[R/^/P1]]] graph = """foo[-P1] => foo bar[-P1] => bar # remove bar:start => foo # remove """ [runtime] [[foo]] script = """ # Use poll function from test_header. $(declare -f poll) $(declare -f poll_grep) # Remove bar and tell the server to reload. if (( CYLC_TASK_CYCLE_POINT == CYLC_WORKFLOW_INITIAL_CYCLE_POINT )); then sed -i 's/^.*remove*$//g' "\${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" cylc reload "\${CYLC_WORKFLOW_ID}" poll_grep -F 'Reload complete' "\${CYLC_WORKFLOW_RUN_DIR}/log/scheduler/log" # kill the long-running orphaned bar task. kill "\$(cat "\${CYLC_WORKFLOW_RUN_DIR}/work/1/bar/pid")" fi """ [[bar]] script = """ # Long sleep to ensure that bar does not finish before the reload. # Store long sleep PID to enable kill after the reload. sleep 1000 & echo \$! > pid wait""" __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-result" cylc workflow-state --old-format "${WORKFLOW_NAME}" > workflow-state.log contains_ok workflow-state.log << __END__ foo, 1, succeeded bar, 1, succeeded foo, 2, succeeded foo, 3, succeeded __END__ purge cylc-flow-8.6.4/tests/functional/reload/20-stop-point.t0000775000175000017500000000320115202510242023076 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test set stop point then reload. Reload should not reset stop point. # https://github.com/cylc/cylc-flow/issues/2964 export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config " [platforms] [[$CYLC_TEST_PLATFORM]] job runner command template = sleep 5 submission retry delays = 3*PT5S " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" \ 'SELECT cycle,name,run_status FROM task_jobs' | sort >'db.out' cmp_ok 'db.out' <<'__OUT__' 1|reload|0 1|set-stop-point|0 1|t1|0 2|t1|0 3|t1|0 __OUT__ purge exit cylc-flow-8.6.4/tests/functional/reload/30-global-rollback.t0000664000175000017500000000333315202510242024015 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reloading global configuration . "$(dirname "$0")/test_header" set_test_number 6 create_test_global_config "" "" TEST_NAME="${TEST_NAME_BASE}" install_workflow "${TEST_NAME}" "${TEST_NAME_BASE}" # Validate the config run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" # Run the workflow workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach -v # Reload happened grep_ok "Reloading the global configuration" "${WORKFLOW_RUN_DIR}/log/scheduler/01-start-01.log" # But there was an error in the workflow config, so the change rolled back grep_ok "Reload failed - IllegalItemError: garbage" "${WORKFLOW_RUN_DIR}/log/scheduler/log" # Reload has rolled back in all tasks grep_fail "global init-script reloaded!" "${WORKFLOW_RUN_DIR}/log/job/1/b/01/job.out" grep_fail "global init-script reloaded!" "${WORKFLOW_RUN_DIR}/log/job/1/c/01/job.out" purge exit cylc-flow-8.6.4/tests/functional/reload/03-queues/0000775000175000017500000000000015202510242022106 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/reload/03-queues/reference.log0000664000175000017500000000077515202510242024560 0ustar alastairalastairInitial point: 1 Final point: 1 1/reloader -triggered off [] 1/a -triggered off ['1/reloader'] 1/c -triggered off ['1/reloader'] 1/b -triggered off ['1/reloader'] 1/e -triggered off ['1/reloader'] 1/monitor -triggered off ['1/reloader'] 1/d -triggered off ['1/reloader'] 1/g -triggered off ['1/reloader'] 1/f -triggered off ['1/reloader'] 1/i -triggered off ['1/reloader'] 1/h -triggered off ['1/reloader'] 1/k -triggered off ['1/reloader'] 1/j -triggered off ['1/reloader'] 1/l -triggered off ['1/reloader'] cylc-flow-8.6.4/tests/functional/reload/03-queues/flow.cylc0000664000175000017500000000307715202510242023740 0ustar alastairalastair# Change queue limit by reload and check number of running tasks to confirm # that the new limit is being applied. This test is potentially flaky because # it checks task status using the "cylc workflow-state" DB command, and DB task # states (like the datastore too) only get updated once per main loop. [scheduling] [[ queues ]] [[[ q1 ]]] limit = 5 # marker members = reloader, FAM [[graph]] R1 = """reloader:start => FAM reloader => monitor""" [runtime] [[FAM]] script = sleep 10 [[a,b,c,d,e,f,g,h,i,j,k,l]] inherit = FAM [[reloader]] script = """ # change the limit from 5 to 3: perl -pi -e 's/(limit = )5( # marker)/\1 3 \2/' $CYLC_WORKFLOW_RUN_DIR/flow.cylc # reload: cylc reload "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log 'Reload completed' """ [[monitor]] script = """ cylc__job__wait_cylc_message_started while true; do RUNNING=$(cylc dump -l -t "${CYLC_WORKFLOW_ID}" | grep running | wc -l) # Should be max of: monitor plus 3 members of q1 echo "RUNNING $RUNNING" if ((RUNNING > 4)); then break fi sleep 1 SUCCEEDED=$(cylc workflow-state "${CYLC_WORKFLOW_ID}//*/*:succeeded" --max-polls=1 | wc -l) echo "SUCCEEDED $SUCCEEDED" if ((SUCCEEDED==13)); then break fi sleep 1 done if ((RUNNING > 4)); then false else true fi """ cylc-flow-8.6.4/tests/functional/reload/24-reload-file-install.t0000664000175000017500000000325515202510242024623 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reload triggers a fresh file install export REQUIRE_PLATFORM='loc:remote fs:indep comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True " TEST_NAME="${TEST_NAME_BASE}" install_workflow "${TEST_NAME}" "${TEST_NAME_BASE}" echo "hello" > "${WORKFLOW_RUN_DIR}/changing-file" run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" # If new remote file install has completed task c job.out should have goodbye in it grep_ok "goodbye" "${WORKFLOW_RUN_DIR}/log/job/1/c/01/job.out" find "${WORKFLOW_RUN_DIR}/log/remote-install" -type f -name "*log" | wc -l >'find-remote-install-log' cmp_ok 'find-remote-install-log' <<< '2' purge exit cylc-flow-8.6.4/tests/functional/reload/19-remote-kill.t0000775000175000017500000000276415202510242023233 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reload then kill remote running task. export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate --set="CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test \ --set="CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" \ "${WORKFLOW_NAME}" sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" \ 'SELECT cycle,name,run_status FROM task_jobs' >'db.out' cmp_ok 'db.out' <<'__OUT__' 1|foo|1 1|bar|0 __OUT__ purge exit cylc-flow-8.6.4/tests/functional/reload/14-waiting.t0000775000175000017500000000301515202510242022432 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test reload a waiting task does not cause DB integrity error. # cylc/cylc-flow#1221 . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" run_fail "${TEST_NAME_BASE}-database-integrity-error" \ grep -q 'Database Integrity Error' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/reload/28-preparing.t0000664000175000017500000000332715202510242022767 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test for duplicate job submissions when preparing tasks get flushed # prior to reload - see https://github.com/cylc/cylc-flow/pull/6345 . "$(dirname "$0")/test_header" set_test_number 4 # Strap the process pool size down to 1, so that the first task is stuck # in the preparing state until the startup event handler finishes. create_test_global_config "" " [scheduler] process pool size = 1 " # install and play the workflow, then reload it and wait for it to finish. install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-vip" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-reload" cylc play "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-reload" cylc reload "${WORKFLOW_NAME}" poll_grep_workflow_log -F 'INFO - DONE' # check that task `foo` was only submitted once. count_ok "1/foo.*submitted to" "${WORKFLOW_RUN_DIR}/log/scheduler/log" 1 purge cylc-flow-8.6.4/tests/functional/reload/25-xtriggers.t0000664000175000017500000000513615202510242023013 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Ensure that xtriggers are preserved after reloads # See https://github.com/cylc/cylc-flow/issues/4866 . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = """ broken reload """ [runtime] [[broken]] script = false # should be long enough for the reload to complete # (annoyingly we can't make this event driven) execution retry delays = PT1M # NOTE: "execution retry delays" is implemented as an xtrigger [[reload]] script = """ # wait for "broken" to fail cylc__job__poll_grep_workflow_log -E '1/broken/01.*failed/ERR' # fix "broken" to allow it to pass sed -i 's/false/true/' "${CYLC_WORKFLOW_RUN_DIR}/flow.cylc" # reload the workflow cylc reload "${CYLC_WORKFLOW_ID}" """ __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach -v # ensure the following order of events # 1. "1/broken" fails # 2. workflow is reloaded (by "1/reload") # 3. the retry xtrigger for "1/broken" is succeeded (after the reload) # (thus proving that the xtrigger survived the reload) # 4. "1/broken" succeeds log_scan "${TEST_NAME_BASE}-scan" \ "$(cylc cat-log -m p "${WORKFLOW_NAME}")" \ 1 1 \ '1/broken/01.*failed/ERR' log_scan "${TEST_NAME_BASE}-scan" \ "$(cylc cat-log -m p "${WORKFLOW_NAME}")" 1 1 \ 'Command "reload_workflow" actioned' \ log_scan "${TEST_NAME_BASE}-scan" \ "$(cylc cat-log -m p "${WORKFLOW_NAME}")" \ 1 1 \ 'xtrigger succeeded: _cylc_retry_1_broken' \ '1/broken.* => succeeded' purge cylc-flow-8.6.4/tests/functional/cylc-cat-log/0000775000175000017500000000000015202510242021367 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/13-remote-out-err-tailer/0000775000175000017500000000000015202510242025754 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/13-remote-out-err-tailer/flow.cylc0000664000175000017500000000125515202510242027602 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT2M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ # wait for the started message to be received cylc__job__poll_grep_workflow_log -E 'foo.*running' # remove the out/err files rm "${CYLC_TASK_LOG_DIR}/job.out" rm "${CYLC_TASK_LOG_DIR}/job.err" # stop the workflow, orphaning this job cylc stop --now --now "${CYLC_WORKFLOW_ID}" 2>/dev/null >/dev/null # suppress any subsequent messages rm "${CYLC_WORKFLOW_RUN_DIR}/.service/contact" """ cylc-flow-8.6.4/tests/functional/cylc-cat-log/01-remote.t0000775000175000017500000001262415202510242023275 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" for remote tasks. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 16 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = False" install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --debug --no-detach \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-out cylc cat-log -f o "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok '^the quick brown fox$' "${TEST_NAME}.out" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-job cylc cat-log -f j "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" contains_ok "${TEST_NAME}.out" - << __END__ # SCRIPT: # Write to task stdout log echo "the quick brown fox" # Write to task stderr log echo "jumped over the lazy dog" >&2 # Write to a custom log file echo "drugs and money" > \${CYLC_TASK_LOG_ROOT}.custom-log __END__ #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-err cylc cat-log -f e "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "jumped over the lazy dog" "${TEST_NAME}.out" #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-status cylc cat-log -f s "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "CYLC_JOB_RUNNER_NAME=$CYLC_TEST_JOB_RUNNER" "${TEST_NAME}.out" #------------------------------------------------------------------------------- # local TEST_NAME=${TEST_NAME_BASE}-task-activity cylc cat-log -f a "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok '\[jobs-submit ret_code\] 0' "${TEST_NAME}.out" #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-custom cylc cat-log -f 'job.custom-log' "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "drugs and money" "${TEST_NAME}.out" #------------------------------------------------------------------------------- # local TEST_NAME=${TEST_NAME_BASE}-task-list-local-NN cylc cat-log -f a -m l "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" contains_ok "${TEST_NAME}.out" <<__END__ job job-activity.log __END__ #------------------------------------------------------------------------------- # local TEST_NAME=${TEST_NAME_BASE}-task-list-local-01 cylc cat-log -f a -m l -s 1 "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" contains_ok "${TEST_NAME}.out" <<__END__ job job-activity.log __END__ #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-list-remote-NN cylc cat-log -f j -m l "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" contains_ok "${TEST_NAME}.out" <<__END__ job job-activity.log job.custom-log job.err job.out job.status job.xtrace __END__ #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-log-dir-NN cylc cat-log -f j -m d "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/NN$" "${TEST_NAME}.out" #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-log-dir-01 cylc cat-log -m d -f j -s 1 "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/01$" "${TEST_NAME}.out" #------------------------------------------------------------------------------- # remote TEST_NAME=${TEST_NAME_BASE}-task-job-path cylc cat-log -m p -f j "${WORKFLOW_NAME}//1/a-task" >"${TEST_NAME}.out" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/NN/job$" "${TEST_NAME}.out" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-un-norm-path run_fail "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/a-task" \ --remote-arg=j/../02/j \ --remote-arg=cat \ --remote-arg='tail -f' grep_ok 'InputError' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- # Clean up the task host. purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/02-remote-custom-runtime-viewer-pbs/0000775000175000017500000000000015202510242030153 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/02-remote-custom-runtime-viewer-pbs/reference.log0000664000175000017500000000014015202510242032607 0ustar alastairalastairInitial point: 1 Final point: 1 1/a-task -triggered off [] 1/b-task -triggered off ['1/a-task'] cylc-flow-8.6.4/tests/functional/cylc-cat-log/02-remote-custom-runtime-viewer-pbs/flow.cylc0000664000175000017500000000116015202510242031774 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = a-task:echo => b-task [runtime] [[a-task]] script = """ echo rubbish echo garbage >&2 cylc message 'echo done' sleep 60 """ platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[outputs]]] echo = "echo done" [[b-task]] script = """ sleep 10 # wait for buffer to flush? cylc cat-log --debug -f o "${CYLC_WORKFLOW_ID}//1/a-task" | grep 'rubbish' cylc cat-log --debug -f e "${CYLC_WORKFLOW_ID}//1/a-task" | grep 'garbage' """ cylc-flow-8.6.4/tests/functional/cylc-cat-log/09-cat-running/0000775000175000017500000000000015202510242024042 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/09-cat-running/reference.log0000664000175000017500000000023215202510242026500 0ustar alastairalastairInitial point: 1 Final point: 1 1/local-task -triggered off [] 1/remote-task -triggered off [] 1/cat-log -triggered off ['1/local-task', '1/remote-task'] cylc-flow-8.6.4/tests/functional/cylc-cat-log/09-cat-running/flow.cylc0000664000175000017500000000211415202510242025663 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = local-task:echo & remote-task:echo => cat-log [runtime] [[ECHO]] script = """ cylc__job__wait_cylc_message_started echo rubbish echo garbage >&2 cylc message 'echo done' """ [[[outputs]]] echo = "echo done" [[local-task]] inherit = ECHO [[remote-task]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} inherit = ECHO [[cat-log]] script = """ cylc__job__wait_cylc_message_started for TASK in '1/local-task' '1/remote-task'; do cylc cat-log --debug -f o \ "${CYLC_WORKFLOW_ID}//${TASK}" \ | grep 'rubbish' cylc cat-log --debug -f e \ "${CYLC_WORKFLOW_ID}//${TASK}" \ | grep 'garbage' done """ cylc-flow-8.6.4/tests/functional/cylc-cat-log/06-log-rotation.t0000775000175000017500000000276415202510242024431 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Tests that cylc cat-log correctly handles log rotation. . "$(dirname "$0")/test_header" set_test_number 1 init_workflow "${TEST_NAME_BASE}" '/dev/null' # Populate its cylc-run dir with empty log files. LOG_DIR="$HOME/cylc-run/$WORKFLOW_NAME/log/scheduler" mkdir -p "${LOG_DIR}" touch -t '201001011200.00' "${LOG_DIR}/01-start-01.log" touch -t '201001011200.01' "${LOG_DIR}/02-start-01.log" touch -t '201001011200.02' "${LOG_DIR}/03-restart-02.log" # Test log rotation. for I in {0..2}; do basename "$(cylc cat-log "${WORKFLOW_NAME}" -m p -r "${I}")" done >'result' cmp_ok 'result' <<'__CMP__' 03-restart-02.log 02-start-01.log 01-start-01.log __CMP__ purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/05-remote-tail.t0000775000175000017500000000543115202510242024226 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" with a custom remote tail command. export REQUIRE_PLATFORM='loc:remote comms:tcp runner:background' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" set -eu SSH='ssh -oBatchMode=yes -oConnectTimeout=5' SCP='scp -oBatchMode=yes -oConnectTimeout=5' $SSH -n "${CYLC_TEST_HOST}" "mkdir -p cylc-run/.bin" # shellcheck disable=SC2016 create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] tail command template = \$HOME/cylc-run/.bin/my-tailer.sh %(filename)s" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- $SCP "${PWD}/bin/my-tailer.sh" \ "${CYLC_TEST_HOST}:cylc-run/.bin/my-tailer.sh" #------------------------------------------------------------------------------- # Run detached. workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- poll_grep_workflow_log -E '1/foo/01:preparing.* => submitted' # cylc cat-log -m 't' tail-follows a file, so needs to be killed. # Send interrupt signal to tail command after 15 seconds. TEST_NAME="${TEST_NAME_BASE}-cat-log" timeout -s 'INT' 15 \ cylc cat-log "${WORKFLOW_NAME}//1/foo" -f 'o' -m 't' --force-remote \ >"${TEST_NAME}.out" 2>"${TEST_NAME}.err" || true grep_ok "HELLO from foo 1" "${TEST_NAME}.out" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-stop run_ok "${TEST_NAME}" cylc stop --kill --max-polls=20 --interval=1 "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- $SSH -n "${CYLC_TEST_HOST}" "rm -rf cylc-run/.bin/" purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/02-remote-custom-runtime-viewer-pbs.t0000775000175000017500000000244715202510242030352 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" for viewing PBS runtime STDOUT/STDERR by a custom command export REQUIRE_PLATFORM='runner:pbs' . "$(dirname "$0")/test_header" OUT_VIEWER="$(cylc config -d -i "[platforms][$CYLC_TEST_PLATFORM]out viewer")" ERR_VIEWER="$(cylc config -d -i "[platforms][$CYLC_TEST_PLATFORM]err viewer")" if [[ -z "${ERR_VIEWER}" || -z "${OUT_VIEWER}" ]]; then skip_all 'remote viewers not configured for this platform' fi set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/04-local-tail.t0000775000175000017500000000365415202510242024031 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" with a custom tail /command . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" create_test_global_config "" " [platforms] [[localhost]] tail command template = $PWD/bin/my-tailer.sh %(filename)s " #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" run_ok "wait-for-task-foo-start" \ cylc workflow-state "${WORKFLOW_NAME}//1/foo:started" --interval=1 --triggers TEST_NAME=${TEST_NAME_BASE}-cat-log cylc cat-log "${WORKFLOW_NAME}//1/foo" -f o -m t > "${TEST_NAME}.out" grep_ok "HELLO from foo 1" "${TEST_NAME}.out" #------------------------------------------------------------------------------- cylc stop --kill --max-polls=20 --interval=1 "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cylc-cat-log/05-remote-tail/0000775000175000017500000000000015202510242024033 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/05-remote-tail/bin/0000775000175000017500000000000015202510242024603 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/05-remote-tail/bin/my-tailer.sh0000775000175000017500000000032415202510242027044 0ustar alastairalastair#!/usr/bin/env bash # Modify 'tail' output to prove that cylc used the custom tailer. # Exit immediately, for the test (i.e. don't 'tail -F') FILE="$1" tail -n +1 "${FILE}" | awk '{print "HELLO", $0; fflush() }' cylc-flow-8.6.4/tests/functional/cylc-cat-log/05-remote-tail/flow.cylc0000664000175000017500000000052615202510242025661 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT2M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ for I in $(seq 1 100); do echo "from $CYLC_TASK_NAME $I" sleep 1 done""" platform={{environ['CYLC_TEST_PLATFORM'] | default("localhost")}} cylc-flow-8.6.4/tests/functional/cylc-cat-log/01-remote/0000775000175000017500000000000015202510242023100 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/01-remote/flow.cylc0000664000175000017500000000072215202510242024724 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT1M [scheduling] [[graph]] R1 = a-task [runtime] [[a-task]] script = """ # Write to task stdout log echo "the quick brown fox" # Write to task stderr log echo "jumped over the lazy dog" >&2 # Write to a custom log file echo "drugs and money" > ${CYLC_TASK_LOG_ROOT}.custom-log """ platform = {{CYLC_TEST_PLATFORM | default("localhost")}} cylc-flow-8.6.4/tests/functional/cylc-cat-log/10-remote-no-retrieve.t0000775000175000017500000000330515202510242025526 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" for remote tasks with no auto-retrieval. export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = False" install_workflow "${TEST_NAME_BASE}" remote-simple TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" # Local job.out should not exist (not retrieved). LOCAL_JOB_DIR=$(cylc cat-log -f a -m d "${WORKFLOW_NAME}//1/a-task") exists_fail "${LOCAL_JOB_DIR}/job.out" # Cat the remote one. TEST_NAME=${TEST_NAME_BASE}-task-out run_ok "${TEST_NAME}" cylc cat-log -f o "${WORKFLOW_NAME}//1/a-task" grep_ok '^the quick brown fox$' "${TEST_NAME}.stdout" purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/test_header0000777000175000017500000000000015202510242027704 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/12-delete-kill.t0000664000175000017500000000365115202510242024174 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" exits when the log file is deleted # or when tail is killed. . "$(dirname "$0")/test_header" set_test_number 2 # Get PID of tail cmd given the parent cat-log PPID get_tail_pid() { pgrep -P "$1" tail } init_workflow "${TEST_NAME_BASE}" << __EOF__ # whatever __EOF__ log_file="${WORKFLOW_RUN_DIR}/log/foo.log" echo "Hello, Mr. Thompson" > "$log_file" TEST_NAME="${TEST_NAME_BASE}-delete" cylc cat-log --mode=tail "$WORKFLOW_NAME" -f foo.log 2>&1 & cat_log_pid="$!" # Wait for tail to start poll get_tail_pid "$cat_log_pid" # We should be able to delete the log file run_ok "$TEST_NAME" rm "$log_file" # cat-log should exit (but exit code does not seem to be consistent across systems) poll_pid_done "$cat_log_pid" echo "Hello, Mr. Thompson" > "$log_file" TEST_NAME="${TEST_NAME_BASE}-kill" cylc cat-log --mode=tail "$WORKFLOW_NAME" -f foo.log 2>&1 & cat_log_pid="$!" # Wait for tail to start poll get_tail_pid "$cat_log_pid" kill "$(get_tail_pid "$cat_log_pid")" # cat-log should exit non-zero poll_pid_done "$cat_log_pid" run_fail "${TEST_NAME}_ret" wait "$cat_log_pid" purge cylc-flow-8.6.4/tests/functional/cylc-cat-log/04-local-tail/0000775000175000017500000000000015202510242023631 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/04-local-tail/bin/0000775000175000017500000000000015202510242024401 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/04-local-tail/bin/my-tailer.sh0000775000175000017500000000032415202510242026642 0ustar alastairalastair#!/usr/bin/env bash # Modify 'tail' output to prove that cylc used the custom tailer. # Exit immediately, for the test (i.e. don't 'tail -F') FILE="$1" tail -n +1 "${FILE}" | awk '{print "HELLO", $0; fflush() }' cylc-flow-8.6.4/tests/functional/cylc-cat-log/04-local-tail/flow.cylc0000664000175000017500000000047415202510242025461 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT2M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ for I in $(seq 1 100); do echo "from $CYLC_TASK_NAME $I" sleep 1 done """ cylc-flow-8.6.4/tests/functional/cylc-cat-log/11-remote-retrieve.t0000775000175000017500000000372515202510242025123 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" for remote tasks with auto-retrieval. export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" set_test_number 7 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True" install_workflow "${TEST_NAME_BASE}" remote-simple TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" # Local job.out should exist (retrieved). LOCAL_JOB_OUT=$(cylc cat-log -f a -m d "${WORKFLOW_NAME}//1/a-task")/job.out exists_ok "${LOCAL_JOB_OUT}" # Distinguish local from remote job.out perl -pi -e 's/fox/FOX/' "${LOCAL_JOB_OUT}" # Cat the remote one. TEST_NAME=${TEST_NAME_BASE}-out-rem run_ok "${TEST_NAME}" cylc cat-log --force-remote -f o \ "${WORKFLOW_NAME}//1/a-task" grep_ok '^the quick brown fox$' "${TEST_NAME}.stdout" # Cat the local one. TEST_NAME=${TEST_NAME_BASE}-out-loc run_ok "${TEST_NAME}" cylc cat-log -f o "${WORKFLOW_NAME}//1/a-task" grep_ok '^the quick brown FOX$' "${TEST_NAME}.stdout" purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/13-remote-out-err-tailer.t0000664000175000017500000000714515202510242026150 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" with custom out/err tailers export REQUIRE_PLATFORM='loc:remote runner:background fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 12 #------------------------------------------------------------------------------- # run the workflow TEST_NAME="${TEST_NAME_BASE}-validate" install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play -N "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # change the platform the task ran on to the remote platform sqlite3 "${HOME}/cylc-run/${WORKFLOW_NAME}/log/db" " UPDATE task_jobs SET platform_name = '${CYLC_TEST_PLATFORM}', run_status = null WHERE name = 'foo' AND cycle = '1' ;" #------------------------------------------------------------------------------- # test cylc cat-log --mode=list-dir will not list job.out / err # (no tailer / viewer configured) create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] out tailer = err tailer = out viewer = err viewer = " TEST_NAME="${TEST_NAME_BASE}-list-dir-no-tailers" # NOTE: command will fail due to missing remote directory (this tests remote # error code is preserved) run_fail "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/foo" -m 'list-dir' # the job.out and job.err filees grep_fail "job.out" "${TEST_NAME}.stdout" grep_fail "job.err" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- # test cylc cat-log --mode=list-dir lists the tailed files # (both tailer and viewer configured) create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] out tailer = echo OUT err tailer = echo ERR out viewer = echo OUT err viewer = echo ERR " # test cylc cat-log --mode=list-dir lists the tailed files TEST_NAME="${TEST_NAME_BASE}-list-dir-with-tailers" # NOTE: command will fail due to missing remote directory (this tests remote # error code is preserved) run_fail "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/foo" -m 'list-dir' # the job.out and job.err filees grep_ok "job.out" "${TEST_NAME}.stdout" grep_ok "job.err" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- # test cylc cat-log runs the custom tailers TEST_NAME="${TEST_NAME_BASE}-cat-out" run_ok "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/foo" -f o -m t grep_ok "OUT" "${TEST_NAME}.stdout" run_ok "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/foo" -f e -m t grep_ok "ERR" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/09-cat-running.t0000775000175000017500000000201215202510242024225 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" of currently-running local and remote jobs. export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/00-local/0000775000175000017500000000000015202510242022676 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/00-local/reference.log0000664000175000017500000000014215202510242025334 0ustar alastairalastair1/submit-failed -triggered off [] in flow 1 1/a-task -triggered off ['1/submit-failed'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-cat-log/00-local/flow.cylc0000664000175000017500000000175615202510242024532 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = submit-failed:submit-failed? => a-task [runtime] [[a-task]] script = """ # Write to task stdout log echo "the quick brown fox" # Write to task stderr log echo "jumped over the lazy dog" >&2 # Write to a custom log file echo "drugs, money & whitespace" > "${CYLC_TASK_LOG_ROOT} custom.log" # Generate a warning message in the workflow log. cylc message -p WARNING 'marmite and squashed bananas' # Remove the submit-failed task. cylc remove "${CYLC_WORKFLOW_NAME}//1/submit-failed" """ [[submit-failed]] # This causes a submission failure due to Bash syntax check. # In this case the job file isn't even written remotely. script = if cylc-flow-8.6.4/tests/functional/cylc-cat-log/00-local.t0000775000175000017500000001717415202510242023100 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc cat-log" on the workflow host. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 43 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --no-detach "${WORKFLOW_NAME}" --reference-test #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-workflow-log-log run_ok "${TEST_NAME}" cylc cat-log "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-workflow-log-ok LOG_DIR="$(dirname "$(cylc cat-log -m p "${WORKFLOW_NAME}")")" echo "This is file 02-restart-02.log" > "${LOG_DIR}/02-restart-02.log" echo "This is file 03-restart-02.log" > "${LOG_DIR}/03-restart-02.log" # it should accept file paths relative to the scheduler log directory run_ok "${TEST_NAME}" cylc cat-log -f scheduler/03-restart-02.log "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" - << __END__ This is file 03-restart-02.log __END__ # it should pick the latest scheduler log file if no rotation number is provided run_ok "${TEST_NAME}" cylc cat-log --file s "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" - << __END__ This is file 03-restart-02.log __END__ # it should apply rotation number to scheduler log files run_ok "${TEST_NAME}" cylc cat-log -f s -r 1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" - << __END__ This is file 02-restart-02.log __END__ # it should list scheduler log files run_ok "${TEST_NAME}" cylc cat-log -m l "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" - << __END__ config/01-start-01.cylc config/flow-processed.cylc install/01-install.log scheduler/01-start-01.log scheduler/02-restart-02.log scheduler/03-restart-02.log scheduler/reftest.log __END__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-out run_ok "${TEST_NAME}" cylc cat-log -f o "${WORKFLOW_NAME}//1/a-task" grep_ok '^the quick brown fox$' "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-job run_ok "${TEST_NAME}" cylc cat-log -f j "${WORKFLOW_NAME}//1/a-task" contains_ok "${TEST_NAME}.stdout" - << __END__ # SCRIPT: # Write to task stdout log echo "the quick brown fox" # Write to task stderr log echo "jumped over the lazy dog" >&2 # Write to a custom log file echo "drugs, money & whitespace" > "\${CYLC_TASK_LOG_ROOT} custom.log" # Generate a warning message in the workflow log. cylc message -p WARNING 'marmite and squashed bananas' __END__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-err run_ok "${TEST_NAME}" cylc cat-log -f e "${WORKFLOW_NAME}//1/a-task" grep_ok "jumped over the lazy dog" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-status run_ok "${TEST_NAME}" cylc cat-log -f s "${WORKFLOW_NAME}//1/a-task" grep_ok "CYLC_JOB_RUNNER_NAME=background" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-activity run_ok "${TEST_NAME}" cylc cat-log -f a "${WORKFLOW_NAME}//1/a-task" grep_ok '\[jobs-submit ret_code\] 0' "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-custom run_ok "${TEST_NAME}" cylc cat-log -f 'job custom.log' "${WORKFLOW_NAME}//1/a-task" grep_ok "drugs, money & whitespace" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-list-local-NN run_ok "${TEST_NAME}" cylc cat-log -f a -m l "${WORKFLOW_NAME}//1/a-task" contains_ok "${TEST_NAME}.stdout" <<__END__ job job-activity.log job custom.log job.err job.out job.status __END__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-list-local-01 run_ok "${TEST_NAME}" cylc cat-log -f a -m l -s 1 "${WORKFLOW_NAME}//1/a-task" contains_ok "${TEST_NAME}.stdout" <<__END__ job job-activity.log job custom.log job.err job.out job.status __END__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-list-local-02 run_fail cylc cat-log -f j -m l -s 2 "${WORKFLOW_NAME}//1/a-task" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-log-dir-NN run_ok "${TEST_NAME}" cylc cat-log -f j -m d "${WORKFLOW_NAME}//1/a-task" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/NN$" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-log-dir-01 run_ok "${TEST_NAME}" cylc cat-log -f j -m d -s 1 "${WORKFLOW_NAME}//1/a-task" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/01$" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-task-job-path run_ok "${TEST_NAME}" cylc cat-log -f j -m p "${WORKFLOW_NAME}//1/a-task" grep_ok "${WORKFLOW_NAME}/log/job/1/a-task/NN/job$" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- # it shouldn't let you modify the file path to access other resources # use the dedicated options TEST_NAME=${TEST_NAME_BASE}-un-norm-path run_fail "${TEST_NAME}" cylc cat-log -f j/../02/j "${WORKFLOW_NAME}//1/a-task" grep_ok 'InputError' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-prepend-path run_ok "${TEST_NAME}-get-path" cylc cat-log -m p "${WORKFLOW_NAME}//1/a-task" run_ok "${TEST_NAME}" cylc cat-log --prepend-path "${WORKFLOW_NAME}//1/a-task" grep_ok "$(cat "#.*${TEST_NAME}-get-path.stdout")" "${TEST_NAME}.stdout" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-submit-failed run_ok "${TEST_NAME}" cylc cat-log -m l "${WORKFLOW_NAME}//1/submit-failed" contains_ok "${TEST_NAME}.stdout" <<__END__ job.tmp job-activity.log __END__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-list-no-install-dir rm -r "${WORKFLOW_RUN_DIR}/log/install" run_ok "${TEST_NAME}-get-path" cylc cat-log -m l "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-cat-log/remote-simple/0000775000175000017500000000000015202510242024151 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-cat-log/remote-simple/flow.cylc0000664000175000017500000000041715202510242025776 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT1M [scheduling] [[graph]] R1 = a-task [runtime] [[a-task]] script = echo "the quick brown fox" platform = {{ environ['CYLC_TEST_PLATFORM'] }} cylc-flow-8.6.4/tests/functional/triggering/0000775000175000017500000000000015202510242021252 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/14-submit-fail/0000775000175000017500000000000015202510242023710 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/14-submit-fail/reference.log0000664000175000017500000000012715202510242026351 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/triggering/14-submit-fail/flow.cylc0000664000175000017500000000044315202510242025534 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/foo [scheduling] [[graph]] R1 = """ foo:submit-fail? => bar bar => !foo """ [runtime] [[foo]] script = true platform = idontexist [[bar]] script = true cylc-flow-8.6.4/tests/functional/triggering/02-fam-start-all/0000775000175000017500000000000015202510242024135 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/02-fam-start-all/reference.log0000664000175000017500000000021515202510242026574 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/a', '1/b', '1/c'] cylc-flow-8.6.4/tests/functional/triggering/02-fam-start-all/flow.cylc0000664000175000017500000000032715202510242025762 0ustar alastairalastair[scheduling] [[graph]] R1 = """FAM:start-all => foo""" [runtime] [[FAM]] script = cylc__job__wait_cylc_message_started [[a,b,c]] inherit = FAM [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/fam-expansion/0000775000175000017500000000000015202510242024017 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/fam-expansion/flow.cylc0000664000175000017500000000041015202510242025635 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = "(FOO:finish-all & FOO:fail-any?) => bar" [runtime] [[FOO]] script = false [[foo1,foo2,foo3]] inherit = FOO [[bar]] script = cylc show "${CYLC_WORKFLOW_ID}//1/bar" > {{SHOW_OUT}} cylc-flow-8.6.4/tests/functional/triggering/01-or-conditional.t0000664000175000017500000000165415202510242024604 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "or" conditionals . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/19-and-suicide.t0000664000175000017500000000262115202510242024054 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "and" outputs from 2 tasks triggering suicide. # https://github.com/cylc/cylc-flow/issues/2655 . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" DBFILE="$RUN_DIR/${WORKFLOW_NAME}/log/db" sqlite3 "${DBFILE}" 'SELECT cycle, name, status FROM task_pool ORDER BY name;' >'sqlite3.out' cmp_ok 'sqlite3.out' <'/dev/null' purge exit cylc-flow-8.6.4/tests/functional/triggering/20-and-outputs-suicide/0000775000175000017500000000000015202510242025377 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/20-and-outputs-suicide/reference.log0000664000175000017500000000074415202510242030045 0ustar alastairalastairInitial point: 1 Final point: 3 1/showdown -triggered off ['0/fin'] 1/bad -triggered off ['1/showdown'] 1/ugly -triggered off ['1/showdown'] 1/fin -triggered off ['1/bad', '1/ugly'] 2/showdown -triggered off ['1/fin'] 2/good -triggered off ['2/showdown'] 2/ugly -triggered off ['2/showdown'] 2/fin -triggered off ['2/good', '2/ugly'] 3/showdown -triggered off ['2/fin'] 3/bad -triggered off ['3/showdown'] 3/good -triggered off ['3/showdown'] 3/fin -triggered off ['3/bad', '3/good'] cylc-flow-8.6.4/tests/functional/triggering/20-and-outputs-suicide/flow.cylc0000664000175000017500000000242715202510242027227 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 3 [[graph]] P1 = """ fin[-P1] => showdown showdown:good? => good & ! bad & ! ugly showdown:bad? => bad & ! good & ! ugly showdown:ugly? => ugly & ! good & ! bad # Note: The above implies: # showdown:good => good # showdown:bad => bad # showdown:ugly => ugly # showdown:good & showdown:bad => ! ugly # showdown:good & showdown:ugly => ! bad # showdown:bad & showdown:ugly => ! good (good & bad) | (bad & ugly) | (ugly & good) => fin """ [runtime] [[showdown]] script = """ if ((${CYLC_TASK_CYCLE_POINT} == 1)); then cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'bad' 'ugly' elif ((${CYLC_TASK_CYCLE_POINT} == 2)); then cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'good' 'ugly' else cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'good' 'bad' fi """ [[[outputs]]] good = good bad = bad ugly = ugly [[good, bad, ugly, fin]] script = true cylc-flow-8.6.4/tests/functional/triggering/05-fam-finish-all/0000775000175000017500000000000015202510242024263 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/05-fam-finish-all/reference.log0000664000175000017500000000021515202510242026722 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/a', '1/b', '1/c'] cylc-flow-8.6.4/tests/functional/triggering/05-fam-finish-all/flow.cylc0000664000175000017500000000046315202510242026111 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/a, 1/c [scheduling] [[graph]] R1 = "FAM:finish-all => foo" [runtime] [[FAM]] script = "false" [[a,c]] inherit = FAM [[b]] inherit = FAM script = "true" [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/04-fam-fail-all/0000775000175000017500000000000015202510242023715 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/04-fam-fail-all/reference.log0000664000175000017500000000021515202510242026354 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/a', '1/b', '1/c'] cylc-flow-8.6.4/tests/functional/triggering/04-fam-fail-all/flow.cylc0000664000175000017500000000040015202510242025532 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/a, 1/b, 1/c [scheduling] [[graph]] R1 = "FAM:fail-all => foo" [runtime] [[FAM]] script = "false" [[a,b,c]] inherit = FAM [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/06-fam-succeed-any/0000775000175000017500000000000015202510242024436 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/06-fam-succeed-any/reference.log0000664000175000017500000000024715202510242027102 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/b'] 1/handled -triggered off ['1/a', '1/c'] cylc-flow-8.6.4/tests/functional/triggering/06-fam-succeed-any/flow.cylc0000664000175000017500000000060115202510242026256 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/a, 1/c [scheduling] [[graph]] R1 = """ FAM:succeed-any? => foo a:fail? & c:fail? => handled """ [runtime] [[FAM]] script = "false" [[a,c]] inherit = FAM [[b]] inherit = FAM script = "true" [[foo, handled]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/17-suicide-multi.t0000664000175000017500000000256115202510242024445 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "or" outputs from same task triggering suicide triggering . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" DBFILE="$RUN_DIR/${WORKFLOW_NAME}/log/db" sqlite3 "${DBFILE}" 'SELECT cycle, name, status FROM task_pool ORDER BY cycle, name;' \ >'sqlite3.out' cmp_ok 'sqlite3.out' <'/dev/null' purge exit cylc-flow-8.6.4/tests/functional/triggering/00-recovery/0000775000175000017500000000000015202510242023325 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/00-recovery/reference.log0000664000175000017500000000076615202510242025777 0ustar alastairalastairInitial point: 20110101T0000Z Final point: 20110101T1200Z 20110101T0000Z/pre -triggered off [] 20110101T1200Z/pre -triggered off [] 20110101T0000Z/model -triggered off ['20110101T0000Z/pre'] 20110101T1200Z/model -triggered off ['20110101T1200Z/pre'] 20110101T0000Z/post -triggered off ['20110101T0000Z/model'] 20110101T1200Z/diagnose -triggered off ['20110101T1200Z/model'] 20110101T1200Z/recover -triggered off ['20110101T1200Z/diagnose'] 20110101T1200Z/post -triggered off ['20110101T1200Z/recover'] cylc-flow-8.6.4/tests/functional/triggering/00-recovery/flow.cylc0000664000175000017500000000252015202510242025147 0ustar alastairalastair[meta] title = "automated failure recovery example" description = """ Model task failure triggers diagnosis and recovery tasks, which otherwise take themselves out of the workflow if model succeeds. Model post processing triggers off model or recovery tasks. """ [scheduler] UTC mode = True allow implicit tasks = True [[events]] expected task failures = 20110101T1200Z/model [scheduling] initial cycle point = 20110101T00 final cycle point = 20110101T12 [[graph]] T00,T12 = """ pre:finish => model? # finish trigger model:fail? => diagnose => recover # fail trigger model? => !diagnose & !recover # explicit success trigger model:succeed? | recover => post # conditional and explicit success post => !model # removes failed model to allow workflow to auto finish """ [runtime] [[root]] script = "true" # fast [[model]] script = """ echo Hello from $CYLC_TASK_ID if [[ $(cylc cycletime --print-hour) == 12 ]]; then echo "FAILING NOW!" false else echo "Succeeded" true fi """ [[[meta]]] description = A task that succeeds at 0 UTC and fails at 12 UTC cylc-flow-8.6.4/tests/functional/triggering/19-and-suicide/0000775000175000017500000000000015202510242023666 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/19-and-suicide/reference.log0000664000175000017500000000015315202510242026326 0ustar alastairalastairInitial point: 1 Final point: 1 1/t0 -triggered off [] 1/t1 -triggered off [] 1/t2 -triggered off ['1/t0'] cylc-flow-8.6.4/tests/functional/triggering/19-and-suicide/flow.cylc0000664000175000017500000000114615202510242025513 0ustar alastairalastair# This is an explicit test of suicide triggers. # Under SoD it isn't really a useful test. [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 1/t1 [scheduling] [[graph]] R1 = """ t0:fail? & t1:fail? => !t2 t0? | t1? => t2 """ [runtime] [[t0]] # https://github.com/cylc/cylc-flow/issues/2655 # "1/t2" should not suicide on "1/t1:failed" script = cylc__job__poll_grep_workflow_log -E '1/t1.* failed' [[t1]] script = false [[t2]] script = true cylc-flow-8.6.4/tests/functional/triggering/13-submit/0000775000175000017500000000000015202510242022776 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/13-submit/reference.log0000664000175000017500000000012715202510242025437 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/triggering/13-submit/flow.cylc0000664000175000017500000000015615202510242024623 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo:submit => bar" [runtime] [[foo,bar]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/02-fam-start-all.t0000664000175000017500000000167615202510242024334 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of FAM:start-all . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/20-and-outputs-suicide.t0000664000175000017500000000256015202510242025567 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "and" outputs from same task triggering suicide. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" DBFILE="$RUN_DIR/${WORKFLOW_NAME}/log/db" sqlite3 "${DBFILE}" 'SELECT cycle, name, status FROM task_pool ORDER BY cycle, name;' \ >'sqlite3.out' cmp_ok 'sqlite3.out' <'/dev/null' purge exit cylc-flow-8.6.4/tests/functional/triggering/08-fam-finish-any.t0000664000175000017500000000172615202510242024500 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of 'FAM:finish-any' . "$(dirname "$0")/test_header" skip_macos_gh_actions set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/06-fam-succeed-any.t0000664000175000017500000000170015202510242024621 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of FAM:succeed-any . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/16-fam-expansion.t0000664000175000017500000000402515202510242024431 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of (FOO:finish-all & FOO:fail-any) . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" fam-expansion SHOW_OUT="$PWD/show.out" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate --set="SHOW_OUT='$SHOW_OUT'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --debug --no-detach --set="SHOW_OUT='$SHOW_OUT'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- contains_ok "$SHOW_OUT" <<'__SHOW_DUMP__' ✓ (((1 | 0) & (3 | 2) & (5 | 4)) & (0 | 2 | 4)) ✓ 0 = 1/foo1 failed ⨯ 1 = 1/foo1 succeeded ✓ 2 = 1/foo2 failed ⨯ 3 = 1/foo2 succeeded ✓ 4 = 1/foo3 failed ⨯ 5 = 1/foo3 succeeded __SHOW_DUMP__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/triggering/07-fam-fail-any/0000775000175000017500000000000015202510242023737 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/07-fam-fail-any/reference.log0000664000175000017500000000017715202510242026405 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/triggering/07-fam-fail-any/flow.cylc0000664000175000017500000000045515202510242025566 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/b [scheduling] [[graph]] R1 = "FAM:fail-any? => foo" [runtime] [[FAM]] script = "true" [[a,c]] inherit = FAM [[b]] inherit = FAM script = "false" [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/12-succeed/0000775000175000017500000000000015202510242023105 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/12-succeed/reference.log0000664000175000017500000000012715202510242025546 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/triggering/12-succeed/flow.cylc0000664000175000017500000000015715202510242024733 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo:succeed => bar" [runtime] [[foo,bar]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/11-start/0000775000175000017500000000000015202510242022626 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/11-start/reference.log0000664000175000017500000000012715202510242025267 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/triggering/11-start/flow.cylc0000664000175000017500000000015515202510242024452 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo:start => bar" [runtime] [[foo,bar]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/00-recovery.t0000664000175000017500000000167615202510242023524 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test automated failure recovery example . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/01-or-conditional/0000775000175000017500000000000015202510242024411 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/01-or-conditional/reference.log0000664000175000017500000000023515202510242027052 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/c -triggered off ['1/a'] 1/d -triggered off ['1/a'] 1/e -triggered off ['1/d'] cylc-flow-8.6.4/tests/functional/triggering/01-or-conditional/flow.cylc0000664000175000017500000000047415202510242026241 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/b, 1/c [scheduling] [[graph]] R1 = """ a | b? => c? & d c? | d => e b:failed? => !b c:failed? => !c """ [runtime] [[b,c]] script = false [[a,d, e]] script = true cylc-flow-8.6.4/tests/functional/triggering/03-fam-succeed-all.t0000664000175000017500000000170015202510242024577 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of FAM:succeed-all . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/21-expire.t0000664000175000017500000000165515202510242023162 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test expire triggering . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/03-fam-succeed-all/0000775000175000017500000000000015202510242024414 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/03-fam-succeed-all/reference.log0000664000175000017500000000021515202510242027053 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/a', '1/b', '1/c'] cylc-flow-8.6.4/tests/functional/triggering/03-fam-succeed-all/flow.cylc0000664000175000017500000000027315202510242026241 0ustar alastairalastair[scheduling] [[graph]] R1 = """FAM:succeed-all => foo""" [runtime] [[FAM]] script = "true" [[a,b,c]] inherit = FAM [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/test_header0000777000175000017500000000000015202510242027567 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/17-suicide-multi/0000775000175000017500000000000015202510242024254 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/17-suicide-multi/reference.log0000664000175000017500000000053115202510242026714 0ustar alastairalastairInitial point: 1 Final point: 3 1/showdown -triggered off ['0/fin'] 1/ugly -triggered off ['1/showdown'] 1/fin -triggered off ['1/ugly'] 2/showdown -triggered off ['1/fin'] 2/bad -triggered off ['2/showdown'] 2/fin -triggered off ['2/bad'] 3/showdown -triggered off ['2/fin'] 3/good -triggered off ['3/showdown'] 3/fin -triggered off ['3/good'] cylc-flow-8.6.4/tests/functional/triggering/17-suicide-multi/flow.cylc0000664000175000017500000000227415202510242026104 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 3 [[graph]] P1 = """ fin[-P1] => showdown showdown:good? => good showdown:bad? => bad showdown:ugly? => ugly showdown:good? | showdown:bad? => ! ugly showdown:good? | showdown:ugly? => ! bad showdown:bad? | showdown:ugly? => ! good good | bad | ugly => fin """ [runtime] [[root]] script = true [[showdown]] script = """ if ! (( ${CYLC_TASK_CYCLE_POINT} % 3 )); then cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'The-Good' elif ! (( ( ${CYLC_TASK_CYCLE_POINT} + 1 ) % 3 )); then cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'The-Bad' else cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'The-Ugly' fi """ [[[outputs]]] good = 'The-Good' bad = 'The-Bad' ugly = 'The-Ugly' cylc-flow-8.6.4/tests/functional/triggering/13-submit.t0000664000175000017500000000165515202510242023172 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test submit triggering . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/08-fam-finish-any/0000775000175000017500000000000015202510242024305 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/08-fam-finish-any/reference.log0000664000175000017500000000017715202510242026753 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/c -triggered off [] 1/b -triggered off [] 1/foo -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/triggering/08-fam-finish-any/flow.cylc0000664000175000017500000000065715202510242026140 0ustar alastairalastair[scheduling] [[graph]] R1 = """FAM:finish-any => foo""" [runtime] [[root]] script = true [[FAM]] [[a]] inherit = FAM script = """ cylc__job__poll_grep_workflow_log -E "1/b.*succeeded" """ [[b]] inherit = FAM [[c]] inherit = FAM script = """ cylc__job__poll_grep_workflow_log -E "1/b.*succeeded" """ [[foo]] cylc-flow-8.6.4/tests/functional/triggering/07-fam-fail-any.t0000664000175000017500000000167515202510242024135 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of FAM:fail-any . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/00a-recovery/0000775000175000017500000000000015202510242023466 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/00a-recovery/reference.log0000664000175000017500000000076615202510242026140 0ustar alastairalastairInitial point: 20110101T0000Z Final point: 20110101T1200Z 20110101T0000Z/pre -triggered off [] 20110101T1200Z/pre -triggered off [] 20110101T0000Z/model -triggered off ['20110101T0000Z/pre'] 20110101T1200Z/model -triggered off ['20110101T1200Z/pre'] 20110101T0000Z/post -triggered off ['20110101T0000Z/model'] 20110101T1200Z/diagnose -triggered off ['20110101T1200Z/model'] 20110101T1200Z/recover -triggered off ['20110101T1200Z/diagnose'] 20110101T1200Z/post -triggered off ['20110101T1200Z/recover'] cylc-flow-8.6.4/tests/functional/triggering/00a-recovery/flow.cylc0000664000175000017500000000240115202510242025306 0ustar alastairalastair# Spawn on Demand variant of 00-recovery # No need for suicide triggers. [meta] title = "automated failure recovery example" description = """ Model task failure triggers diagnosis and recovery tasks, which otherwise take themselves out of the workflow if model succeeds. Model post processing triggers off model or recovery tasks. """ [scheduler] UTC mode = True allow implicit tasks = True [[events]] expected task failures = 20110101T1200Z/model [scheduling] initial cycle point = 20110101T00 final cycle point = 20110101T12 [[graph]] T00,T12 = """ pre:finish => model? # finish trigger model:fail? => diagnose => recover # fail trigger model:succeed? | recover => post # conditional and explicit success """ [runtime] [[root]] script = "true" # fast [[model]] script = """ echo Hello from $CYLC_TASK_ID if [[ $(cylc cycletime --print-hour) == 12 ]]; then echo "FAILING NOW!" false else echo "Succeeded" true fi """ [[[meta]]] description = A task that succeeds at 0 UTC and fails at 12 UTC cylc-flow-8.6.4/tests/functional/triggering/00a-recovery.t0000664000175000017500000000167615202510242023665 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test automated failure recovery example . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/05-fam-finish-all.t0000664000175000017500000000170115202510242024447 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of 'FAM:finish-all' . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/14-submit-fail.t0000664000175000017500000000176115202510242024102 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test submit-fail triggering . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config '' ' [platforms] [[idontexist]] ' reftest exit cylc-flow-8.6.4/tests/functional/triggering/04-fam-fail-all.t0000664000175000017500000000167515202510242024113 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test correct expansion of FAM:fail-all . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/10-finish.t0000664000175000017500000000165515202510242023144 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test finish triggering . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/11-start.t0000664000175000017500000000165415202510242023021 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test start triggering . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/12-succeed.t0000664000175000017500000000165615202510242023302 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test succeed triggering . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/triggering/10-finish/0000775000175000017500000000000015202510242022750 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/10-finish/reference.log0000664000175000017500000000021615202510242025410 0ustar alastairalastairInitial point: 1 Final point: 1 1/baz -triggered off [] 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/qux -triggered off ['1/baz'] cylc-flow-8.6.4/tests/functional/triggering/10-finish/flow.cylc0000664000175000017500000000037615202510242024601 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/foo [scheduling] [[graph]] R1 = """foo:finish => bar baz:finish => qux""" [runtime] [[foo]] script = false [[bar,baz,qux]] script = "true" cylc-flow-8.6.4/tests/functional/triggering/21-expire/0000775000175000017500000000000015202510242022766 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/triggering/21-expire/reference.log0000664000175000017500000000041615202510242025430 0ustar alastairalastair19990101T0000Z/bar2 -triggered off [] in flow 1 19990101T0000Z/baz -triggered off ['19990101T0000Z/foo1', '19990101T0000Z/foo2'] in flow 1 19990101T0000Z/qux -triggered off ['19990101T0000Z/bar1'] in flow 1 19990101T0000Z/y -triggered off ['19990101T0000Z/x'] in flow 1 cylc-flow-8.6.4/tests/functional/triggering/21-expire/flow.cylc0000664000175000017500000000074615202510242024620 0ustar alastairalastair[scheduling] initial cycle point = 1999 [[special tasks]] clock-expire = foo1(PT0S), foo2(PT0S), bar1(PT0S), x(PT0S) [[graph]] # Expire: foo1, foo2, bar1, x # Run: y, bar2, baz, qux R1 = """ x? FOO? BAR? x:expire? => y FOO:expire-all? => baz BAR:expire-any? => qux """ [runtime] [[FOO, BAR]] [[foo1, foo2]] inherit = FOO [[bar1, bar2]] inherit = BAR [[x, y, baz, qux]] cylc-flow-8.6.4/tests/functional/rnd/0000775000175000017500000000000015202510242017674 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/00-run-funtional-tests.t0000664000175000017500000000564315202510242024247 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test the run-functional-tests --chunk option . "$(dirname "$0")/test_header" set_test_number 16 #------------------------------------------------------------------------------- CTB="${CYLC_REPO_DIR}/etc/bin/run-functional-tests" unset CHUNK # ensure that 'tests/f' is used as the default test base TEST_NAME="${TEST_NAME_BASE}-base" run_ok "${TEST_NAME}-1" "$CTB" --dry run_ok "${TEST_NAME}-2" "$CTB" --dry tests/functional sort -o "${TEST_NAME}-1.stdout" "${TEST_NAME}-1.stdout" sort -o "${TEST_NAME}-2.stdout" "${TEST_NAME}-2.stdout" cmp_ok "${TEST_NAME}-1.stdout" "${TEST_NAME}-2.stdout" TEST_NAME="${TEST_NAME_BASE}-chunk-base" run_ok "${TEST_NAME}-1" env CHUNK="1/4" "$CTB" --dry run_ok "${TEST_NAME}-2" env CHUNK="1/4" "$CTB" --dry tests/functional sort -o "${TEST_NAME}-1.stdout" "${TEST_NAME}-1.stdout" sort -o "${TEST_NAME}-2.stdout" "${TEST_NAME}-2.stdout" cmp_ok "${TEST_NAME}-1.stdout" "${TEST_NAME}-2.stdout" # ensure that mixing test bases works correctly TEST_NAME="${TEST_NAME_BASE}-testbase" run_ok "${TEST_NAME}-1" "$CTB" --dry tests/f run_ok "${TEST_NAME}-2" "$CTB" --dry tests/k run_ok "${TEST_NAME}-3" "$CTB" --dry tests/f tests/k cat "${TEST_NAME}-2.stdout" >> "${TEST_NAME}-1.stdout" sort -o "${TEST_NAME}-1.stdout" "${TEST_NAME}-1.stdout" sort -o "${TEST_NAME}-3.stdout" "${TEST_NAME}-3.stdout" cmp_ok "${TEST_NAME}-1.stdout" "${TEST_NAME}-3.stdout" # ensure that the whole is equal to the sum of its parts N_CHUNKS=4 DRY_TEST_NAME="${TEST_NAME_BASE}-all" run_ok "${DRY_TEST_NAME}" "${CTB}" --dry 'tests/f' 'tests/k' # list tests for each chunk (from prove not run-functional-tests) for i_chunk in $(seq "${N_CHUNKS}"); do TEST_NAME="${TEST_NAME_BASE}-chunk_n-${i_chunk}" run_ok "${TEST_NAME}" env CHUNK="${i_chunk}/${N_CHUNKS}" "${CTB}" --dry 'tests/f' 'tests/k' cat "${TEST_NAME}.stdout" >>'chunks.out' done # sort files ($CYLC_REPO_DIR/etc/bin/run-functional-tests uses --shuffle) sort -o "${DRY_TEST_NAME}.stdout" "${DRY_TEST_NAME}.stdout" sort -o 'chunks.out' 'chunks.out' # compare test plan for the full and chunked versions cmp_ok "${DRY_TEST_NAME}.stdout" 'chunks.out' exit cylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/0000775000175000017500000000000015202510242023354 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/lib/0000775000175000017500000000000015202510242024122 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/lib/python/0000775000175000017500000000000015202510242025443 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/lib/python/pub/0000775000175000017500000000000015202510242026231 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/lib/python/pub/beer.py0000664000175000017500000000153315202510242027522 0ustar alastairalastair# 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 . BOTTLES = [99] def drink(): BOTTLES[0] -= 1 return f'{BOTTLES[0]} bottles of beer on the wall.' cylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job/flow.cylc0000664000175000017500000000106615202510242025202 0ustar alastairalastair[scheduler] [[events]] [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ # check PYTHONPATH is set correctly grep -q "${CYLC_WORKFLOW_RUN_DIR}/lib/python" <<< "${PYTHONPATH}" # run a toy example python3 -c ' from pub import beer assert beer.drink() == "98 bottles of beer on the wall." assert beer.drink() == "97 bottles of beer on the wall." assert beer.drink() == "96 bottles of beer on the wall." ' """ cylc-flow-8.6.4/tests/functional/rnd/08-cylc-list.t0000664000175000017500000000325015202510242022211 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Functional test of the cylc-list command # (see integration tests for more comprehensive tests) . "$(dirname "$0")/test_header" set_test_number 4 cat > flow.cylc <<__FLOW__ [scheduling] [[graph]] R1 = c [runtime] [[A]] [[[meta]]] title = Aaa [[B]] inherit = A [[[meta]]] title = Bbb [[c]] inherit = B [[[meta]]] title = Ccc __FLOW__ # test signals on a detached scheduler TEST_NAME="${TEST_NAME_BASE}-list" run_ok "$TEST_NAME" cylc list '.' --all-namespaces --with-titles cmp_ok "${TEST_NAME}.stdout" << __HERE__ A Aaa B Bbb c Ccc root __HERE__ TEST_NAME="${TEST_NAME_BASE}-tree" run_ok "$TEST_NAME" cylc list '.' --tree --with-titles cmp_ok "${TEST_NAME}.stdout" << '__HERE__' root `-A `-B `-c Ccc __HERE__ cylc-flow-8.6.4/tests/functional/rnd/02-lib-python-in-job.t0000664000175000017500000000311615202510242023542 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test ${CYLC_WORKFLOW_DIR} in task PYTHONPATH. . "$(dirname "$0")/test_header" set_test_number 2 #------------------------------------------------------------------------------- CHOSEN_WORKFLOW="$(basename "$0" | sed 's/\..*//')" install_workflow "${TEST_NAME_BASE}" "$CHOSEN_WORKFLOW" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/rnd/05-main-loop.t0000664000175000017500000000430515202510242022200 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test the development main-loop plugins to ensure they can run to completion . "$(dirname "$0")/test_header" expected_log_files=( # these are the files they should produce cylc.flow.main_loop.log_data_store.json cylc.flow.main_loop.log_data_store.pdf cylc.flow.main_loop.log_db.sql cylc.flow.main_loop.log_main_loop.json cylc.flow.main_loop.log_main_loop.pdf cylc.flow.main_loop.log_memory.json cylc.flow.main_loop.log_memory.pdf ) set_test_number $(( 1 + ${#expected_log_files[@]} )) init_workflow "${TEST_NAME_BASE}" <<__FLOW_CYLC__ [scheduler] # make sure periodic plugins actually run [[main loop]] [[[log data store]]] interval = PT1S [[[log main loop]]] interval = PT1S [[[log memory]]] interval = PT1S [scheduling] [[graph]] R1 = a [runtime] [[a]] script = sleep 5 __FLOW_CYLC__ # run a workflow with all the development main-loop plugins turned on run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" \ --no-detach \ --debug \ --main-loop 'log data store' \ --main-loop 'log db' \ --main-loop 'log main loop' \ --main-loop 'log memory' # check the expected files are generated for log_file in "${expected_log_files[@]}"; do file_path="${HOME}/cylc-run/${WORKFLOW_NAME}/${log_file}" run_ok "${TEST_NAME_BASE}.${log_file}" \ stat "${file_path}" done purge cylc-flow-8.6.4/tests/functional/rnd/01-signals.t0000664000175000017500000000374715202510242021752 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test scheduler signal handling # # See https://github.com/cylc/cylc-flow/issues/6438 . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" <<__FLOW__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] __FLOW__ # test signals on a detached scheduler TEST_NAME="${TEST_NAME_BASE}-detatch" cylc play "${WORKFLOW_NAME}" --pause poll_workflow_running PID="$(sed -n 's/CYLC_WORKFLOW_PID=//p' "$HOME/cylc-run/$WORKFLOW_NAME/.service/contact")" kill -s SIGINT "$PID" poll_workflow_stopped log_scan "${TEST_NAME}" "$(cylc cat-log -m p "${WORKFLOW_NAME}")" 10 1 \ 'Signal SIGINT received' \ 'Workflow shutting down - REQUEST(NOW)' \ 'DONE' # test signals on a non-detached scheduler TEST_NAME="${TEST_NAME_BASE}-no-detach" cylc play "${WORKFLOW_NAME}" --pause --no-detach 2>/dev/null & poll_workflow_running PID="$(sed -n 's/CYLC_WORKFLOW_PID=//p' "$HOME/cylc-run/$WORKFLOW_NAME/.service/contact")" kill -s SIGTERM "$PID" poll_workflow_stopped log_scan "${TEST_NAME}" "$(cylc cat-log -m p "${WORKFLOW_NAME}")" 10 1 \ 'Signal SIGTERM received' \ 'Workflow shutting down - REQUEST(NOW)' \ 'DONE' purge cylc-flow-8.6.4/tests/functional/rnd/test_header0000777000175000017500000000000015202510242026211 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/rnd/04-conditions.t0000664000175000017500000000251115202510242022452 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- # check `cylc help license` run_ok "${TEST_NAME_BASE}" cylc help license # remove trailing newlines sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "${TEST_NAME_BASE}.stdout" # check `cylc help license` output matches the COPYING file cmp_ok "${CYLC_REPO_DIR}/COPYING" "${TEST_NAME_BASE}.stdout" cylc-flow-8.6.4/tests/functional/rnd/07-completion-server.t0000664000175000017500000000356315202510242023771 0ustar alastairalastair#!/usr/bin/env bash # 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 . . "$(dirname "$0")/test_header" set_test_number 6 # NOTE: The completion server is heavily unit-tested # Run a couple of quick functional tests to make sure the CLI interface / reading # from stdin is working correctly. # $ cylc t # tui # trigger TEST_NAME="${TEST_NAME_BASE}-t" run_ok "${TEST_NAME}" cylc completion-server --once <<< 'cylc|t' grep_ok trigger "${TEST_NAME}.stdout" grep_ok tui "${TEST_NAME}.stdout" # $ cylc trigg # trigger TEST_NAME="${TEST_NAME_BASE}-trigg" run_ok "${TEST_NAME}" cylc completion-server --once <<< 'cylc|trigg' cmp_ok "${TEST_NAME}.stdout" << __HERE__ trigger __HERE__ # Make sure the server exits timeout when trying to read from stdin # (Note the completion server exits 0 on timeout) TEST_NAME="${TEST_NAME_BASE}-timeout" # shellcheck disable=SC2162 if ! read -t 0 -N 0; then # If stdin is not a terminal or contains readable data this test will # fail. run_ok "${TEST_NAME}" timeout 5 cylc completion-server --timeout=1 else # This should happen in non-interactive environments e.g. CI skip 1 'Test requires an stdin stream (cannot be faked)' fi exit cylc-flow-8.6.4/tests/functional/rnd/06-unicode.t0000664000175000017500000000276615202510242021745 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Ensure that UnicodeDecodeError's are caught and handled elegantly # See: https://github.com/cylc/cylc-flow/pull/4947 . "$(dirname "$0")/test_header" if [[ -n ${CI:-} ]]; then # test requires a real terminal to write to skip_all fi set_test_number 4 # this command should work where UTF-8 is supported run_ok "${TEST_NAME_BASE}-good" env LANG=en_GB.UTF-8 cylc scan --help # but fail where it is not run_fail "${TEST_NAME_BASE}-bad" env LANG=en_GB cylc scan --help # we should raise a sensible error message grep_ok \ 'A UTF-8 compatible terminal is required for this command' \ "${TEST_NAME_BASE}-bad.stderr" # and provide some helpful advice grep_ok \ 'LANG=C.UTF-8 cylc scan --help' \ "${TEST_NAME_BASE}-bad.stderr" exit cylc-flow-8.6.4/tests/functional/rnd/03-check-versions.t0000664000175000017500000000333415202510242023227 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc check-versions # WARNING: This is testing your remote platform setup, if you do not # have the exactly same version of Cylc installed locally and # remotely the test will fail. export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" set_test_number 3 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" <<__FLOW__ [scheduler] [[events]] abort on stall timeout = True stall timeout=PT30S [scheduling] [[graph]] R1 = foo [runtime] [[foo]] platform = $CYLC_TEST_PLATFORM __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-check" run_ok "${TEST_NAME}" cylc check-versions "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__HERE__ $CYLC_TEST_PLATFORM: $(cylc version) __HERE__ purge cylc-flow-8.6.4/tests/functional/job-submission/0000775000175000017500000000000015202510242022054 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/17-remote-localtime/0000775000175000017500000000000015202510242025543 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/17-remote-localtime/reference.log0000664000175000017500000000006715202510242030207 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/17-remote-localtime/flow.cylc0000664000175000017500000000037615202510242027374 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = False [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script = test -z "${TZ:-}" platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[job]]] execution time limit = PT1M cylc-flow-8.6.4/tests/functional/job-submission/06-garbage.t0000775000175000017500000000270615202510242024064 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job submission, poll and kill with a garbage command. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" if [[ -n "${PYTHONPATH:-}" ]]; then export PYTHONPATH="${PWD}/lib:${PYTHONPATH}" else export PYTHONPATH="${PWD}/lib" fi create_test_global_config ' [platforms] [[bad]] ' run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/18-clean-env.t0000775000175000017500000000315015202510242024341 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that local jobs can be divorced from the scheduler environment. . "$(dirname "$0")/test_header" create_test_global_config "" " [platforms] [[localhost]] cylc path = $(dirname "$(command -v cylc)") clean job submission environment = True job submission environment pass-through = BEEF " set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Export a variable and try to access from a task job. export BEEF=wellington export CHEESE=melted workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}//1/foo" > job.out grep_ok "BEEF wellington" job.out grep_ok "CHEESE undefined" job.out purge exit cylc-flow-8.6.4/tests/functional/job-submission/10-at-shell.t0000775000175000017500000000261015202510242024172 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job submission via at, with SHELL set to tcsh export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" # By setting "SHELL=/bin/tcsh", "at" would run its command under "/bin/tcsh", # which would cause a failure of this test without the fix in #1749. workflow_run_ok "${TEST_NAME_BASE}-run" \ env 'SHELL=/bin/tcsh' cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message/0000775000175000017500000000000015202510242025432 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message/lib/0000775000175000017500000000000015202510242026200 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message/lib/python/0000775000175000017500000000000015202510242027521 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message/lib/python/delayed_background.py0000664000175000017500000000216015202510242033700 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from time import sleep from cylc.flow.job_runner_handlers.background import BgCommandHandler class DelayedBgCommandHandler(BgCommandHandler): @classmethod def submit(cls, job_file_path, submit_opts): result = super().submit(job_file_path, submit_opts) sleep(10) return result JOB_RUNNER_HANDLER = DelayedBgCommandHandler() cylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message/flow.cylc0000664000175000017500000000026015202510242027253 0ustar alastairalastair# Test delayed job submission. [scheduling] [[graph]] R1 = foo => bar [runtime] [[bar]] [[foo]] platform = wobblygibblets script = sleep 20 cylc-flow-8.6.4/tests/functional/job-submission/00-user/0000775000175000017500000000000015202510242023247 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/00-user/lib/0000775000175000017500000000000015202510242024015 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/00-user/lib/python/0000775000175000017500000000000015202510242025336 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/00-user/lib/python/my_background2.py0000664000175000017500000000166115202510242030622 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.job_runner_handlers.background import BgCommandHandler class MyBgCommandHandler(BgCommandHandler): pass JOB_RUNNER_HANDLER = MyBgCommandHandler() cylc-flow-8.6.4/tests/functional/job-submission/00-user/python/0000775000175000017500000000000015202510242024570 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/00-user/python/my_background.py0000664000175000017500000000166115202510242027772 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.job_runner_handlers.background import BgCommandHandler class MyBgCommandHandler(BgCommandHandler): pass JOB_RUNNER_HANDLER = MyBgCommandHandler() cylc-flow-8.6.4/tests/functional/job-submission/00-user/reference.log0000664000175000017500000000012015202510242025701 0ustar alastairalastairInitial point: 1 Final point: 1 1/bar -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/00-user/flow.cylc0000664000175000017500000000037415202510242025076 0ustar alastairalastair# Test job submission modules in lib/python/ and python/ (deprecated). [scheduling] [[graph]] R1 = foo & bar [runtime] [[root]] script = true # quick [[foo]] platform = testme [[bar]] platform = testme2 cylc-flow-8.6.4/tests/functional/job-submission/01-job-nn-localhost/0000775000175000017500000000000015202510242025443 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/01-job-nn-localhost/db.sqlite30000664000175000017500000000643015202510242027341 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); CREATE TABLE workflow_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO workflow_params VALUES('cylc_version', '8.0.0'); INSERT INTO workflow_params VALUES('uuid_str', 'Something'); INSERT INTO workflow_params VALUES('UTC_mode', 1); CREATE TABLE workflow_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('1','foo','"poll_timer"','["tuple", [[99, "running"]]]','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "retrying"]','null','[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]',99,'0.0','1560508824.17287'); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, platform_name TEXT, job_runner_name TEXT, job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, flow_nums TEXT, outputs TEXT, PRIMARY KEY(cycle, name, flow_nums)); CREATE TABLE task_pool(cycle TEXT, name TEXT, flow_nums TEXT, status TEXT, is_held INTEGER, PRIMARY KEY(cycle, name, flow_nums)); INSERT INTO task_pool VALUES('1','foo','["1"]','waiting', 0); CREATE TABLE task_states(name TEXT, cycle TEXT, flow_nums TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, flow_wait INTEGER, PRIMARY KEY(name, cycle, flow_nums)); INSERT INTO task_states VALUES('foo','1','["1"]', '2019-06-14T11:30:16+01:00','2019-06-14T11:40:24+01:00',99,'waiting','0'); CREATE TABLE task_prerequisites(cycle TEXT, name TEXT, flow_nums TEXT, prereq_name TEXT, prereq_cycle TEXT, prereq_output TEXT, satisfied TEXT, PRIMARY KEY(cycle, name, flow_nums, prereq_name, prereq_cycle, prereq_output)); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); COMMIT; cylc-flow-8.6.4/tests/functional/job-submission/01-job-nn-localhost/reference.log0000664000175000017500000000007015202510242030101 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/01-job-nn-localhost/flow.cylc0000664000175000017500000000114215202510242027264 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True # Ignore DST [scheduling] [[graph]] R1 = foo [runtime] {% if CYLC_TEST_PLATFORM is defined %} [[root]] platform = {{ CYLC_TEST_PLATFORM }} {% endif %} [[foo]] script = """ JOB_LOG_DIR="$(dirname "${CYLC_TASK_LOG_DIR}")" NN_VALUE="$(readlink "${JOB_LOG_DIR}/NN")" # bash 4.2.0 bug: ((VAR == VAL)) does not trigger 'set -e': test "${CYLC_TASK_SUBMIT_NUMBER}" -eq "100" test "${NN_VALUE}" -eq "100" """ [[[job]]] execution retry delays = 99*PT0S cylc-flow-8.6.4/tests/functional/job-submission/00-user.t0000775000175000017500000000227315202510242023443 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test user-defined job runner handlers can be used. . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config "" " [platforms] [[testme]] hosts = localhost job runner = my_background install target = localhost [[testme2]] hosts = localhost job runner = my_background2 install target = localhost " reftest exit cylc-flow-8.6.4/tests/functional/job-submission/07-multi.t0000775000175000017500000000733015202510242023625 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job submission, multiple jobs per host. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" \ "${WORKFLOW_NAME}" RUN_DIR="$RUN_DIR/${WORKFLOW_NAME}" LOG="${RUN_DIR}/log/scheduler/log" sed -n 's/^.*\(cylc jobs-submit\)/\1/p' "${LOG}" | sort -u >'edited-workflow-log' PATHOPTS="--path=/bin --path=/usr/bin --path=/usr/local/bin --path=/sbin --path=/usr/sbin --path=/usr/local/sbin" sort >'edited-workflow-log-ref' <<__LOG__ cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20200101T0000Z/t0/01 20200101T0000Z/t1/01 20200101T0000Z/t2/01 20200101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20210101T0000Z/t0/01 20210101T0000Z/t1/01 20210101T0000Z/t2/01 20210101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20220101T0000Z/t0/01 20220101T0000Z/t1/01 20220101T0000Z/t2/01 20220101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20230101T0000Z/t0/01 20230101T0000Z/t1/01 20230101T0000Z/t2/01 20230101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20240101T0000Z/t0/01 20240101T0000Z/t1/01 20240101T0000Z/t2/01 20240101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20250101T0000Z/t0/01 20250101T0000Z/t1/01 20250101T0000Z/t2/01 20250101T0000Z/t3/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20200101T0000Z/t4/01 20200101T0000Z/t5/01 20200101T0000Z/t6/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20210101T0000Z/t4/01 20210101T0000Z/t5/01 20210101T0000Z/t6/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20220101T0000Z/t4/01 20220101T0000Z/t5/01 20220101T0000Z/t6/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20230101T0000Z/t4/01 20230101T0000Z/t5/01 20230101T0000Z/t6/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20240101T0000Z/t4/01 20240101T0000Z/t5/01 20240101T0000Z/t6/01 cylc jobs-submit --debug --utc-mode --remote-mode $PATHOPTS -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 20250101T0000Z/t4/01 20250101T0000Z/t5/01 20250101T0000Z/t6/01 __LOG__ cmp_ok 'edited-workflow-log' 'edited-workflow-log-ref' purge exit cylc-flow-8.6.4/tests/functional/job-submission/12-tidy-submits-of-prev-run/0000775000175000017500000000000015202510242027107 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/12-tidy-submits-of-prev-run/reference.log0000664000175000017500000000011615202510242031546 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/12-tidy-submits-of-prev-run/flow.cylc0000664000175000017500000000041415202510242030731 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = test "${CYLC_TASK_SUBMIT_NUMBER}" -eq 2 platform = {{ environ['CYLC_TEST_PLATFORM'] | default('localhost') }} [[[job]]] execution retry delays = P0Y cylc-flow-8.6.4/tests/functional/job-submission/03-job-nn-remote-host-with-shared-fs0000777000175000017500000000000015202510242033776 201-job-nn-localhostustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/20-check-chunking.t0000775000175000017500000000305515202510242025347 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that a job containing more than 100 tasks will split into batches. . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config '' " [scheduler] process pool size = 1 [platforms] [[$CYLC_TEST_PLATFORM]] max batch submit size = 2 " install_workflow run_ok "${TEST_NAME_BASE}-validate" cylc validate \ -s "CYLC_TEST_PLATFORM='$CYLC_TEST_PLATFORM'" \ "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play \ -s "CYLC_TEST_PLATFORM='$CYLC_TEST_PLATFORM'" \ --debug \ --no-detach \ --reference-test \ "${WORKFLOW_NAME}" grep_ok \ "# will invoke in batches, sizes=\[2, 2, 1\]" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" # tidy up purge exit ././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootcylc-flow-8.6.4/tests/functional/job-submission/14-tidy-submits-of-prev-run-remote-host-with-shared-fscylc-flow-8.6.4/tests/functional/job-submission/14-tidy-submits-of-prev-run-remote-host-with-shared-0000777000175000017500000000000015202510242040543 212-tidy-submits-of-prev-runustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/02-job-nn-remote-host0000777000175000017500000000000015202510242031152 201-job-nn-localhostustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2/0000775000175000017500000000000015202510242027264 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2/bin/0000775000175000017500000000000015202510242030034 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2/bin/my-host-select0000775000175000017500000000016115202510242032635 0ustar alastairalastair#!/usr/bin/env bash TXT="${CYLC_WORKFLOW_RUN_DIR}/my-host-select.txt" cat "${TXT}" || echo 'localhost' >"${TXT}" cylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2/reference.log0000664000175000017500000000012015202510242031716 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2/flow.cylc0000664000175000017500000000055015202510242031107 0ustar alastairalastair[scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True stall timeout = PT20S abort on stall timeout = True [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = true platform = $(my-host-select) [[[job]]] submission retry delays = PT5S cylc-flow-8.6.4/tests/functional/job-submission/19-platform_select/0000775000175000017500000000000015202510242025466 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/19-platform_select/reference.log0000664000175000017500000000062215202510242030127 0ustar alastairalastair1/platform_no_subshell -triggered off [] in flow 1 1/host_no_subshell -triggered off [] in flow 1 1/platform_subshell -triggered off [] in flow 1 1/host_subshell -triggered off [] in flow 1 1/host_subshell_backticks -triggered off [] in flow 1 1/localhost_subshell -triggered off [] in flow 1 1/platform_subshell_suffix -triggered off [] in flow 1 1/platform_subshell_empty -triggered off [] in flow 1 cylc-flow-8.6.4/tests/functional/job-submission/19-platform_select/flow.cylc0000664000175000017500000000256315202510242027317 0ustar alastairalastair[meta] purpose = """ Test that subshells are handled for platform and host configs. Tasks of the form .*no_subshell act as control runs. """ [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[dependencies]] R1 = """ host_no_subshell localhost_subshell platform_subshell:submit-fail? platform_no_subshell:submit-fail? platform_subshell_empty:submit-fail? platform_subshell_suffix:submit-fail? host_subshell:submit-fail? host_subshell_backticks:submit-fail? """ [runtime] [[root]] script = true [[platform_no_subshell]] platform = improbable platform name [[host_no_subshell]] [[[remote]]] host = localhost [[platform_subshell]] platform = $(echo "improbable platform name") [[platform_subshell_empty]] platform = $(echo "") [[platform_subshell_suffix]] platform = prefix-$( echo middle )-suffix [[host_subshell]] [[[remote]]] host = $(echo "improbable host name") [[host_subshell_backticks]] [[[remote]]] host = `echo "improbable host name"` [[localhost_subshell]] [[[remote]]] host = $(echo "localhost4.localdomain4") cylc-flow-8.6.4/tests/functional/job-submission/02-job-nn-remote-host.t0000775000175000017500000000257415202510242026122 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote host job log NN link correctness. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" mkdir -p "${WORKFLOW_RUN_DIR}/.service/" sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" <'db.sqlite3' workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --upgrade --reference-test --debug --no-detach \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/06-garbage/0000775000175000017500000000000015202510242023667 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/06-garbage/lib/0000775000175000017500000000000015202510242024435 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/06-garbage/lib/bad.py0000664000175000017500000000175615202510242025546 0ustar alastairalastair#!/usr/bin/env python3 # 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 . """Garbage batch system/job runner, for testing.""" class BadSubmitHandler: """Garbage batch system/job runner, for testing.""" SUBMIT_CMD_TMPL = "bad-bad-bad-submit '%(job)s'" JOB_RUNNER_HANDLER = BadSubmitHandler() cylc-flow-8.6.4/tests/functional/job-submission/06-garbage/reference.log0000664000175000017500000000012415202510242026325 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/job-submission/06-garbage/flow.cylc0000664000175000017500000000106615202510242025515 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = t1:submit-fail? => t2 [runtime] [[t1]] script = true platform = bad [[t2]] script = """ grep -q -F \ 'platform: bad - Could not connect to bad' \ "${CYLC_WORKFLOW_LOG_DIR}/log" grep -q -F \ 'remote-init will retry if another host is available' \ "${CYLC_WORKFLOW_LOG_DIR}/log" cylc shutdown "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/job-submission/16-timeout.t0000775000175000017500000000454015202510242024161 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that job submission kill on timeout results in a failed job submission. export REQUIRE_PLATFORM='runner:at comms:tcp' . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config "" " [scheduler] process pool timeout = PT10S [platforms] [[$CYLC_TEST_PLATFORM]] job runner command template = sleep 30 " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-workflow-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" # egrep -m is stop matching after matches # -A is number of lines of context after match cylc cat-log "${WORKFLOW_NAME}" \ | grep -E -m 1 -A 2 "ERROR - \[jobs-submit cmd\]" \ | sed -e 's/^.* \(ERROR\)/\1/' > log WORKFLOW_LOG_DIR=$(cylc cat-log -m p "${WORKFLOW_NAME}" | sed 's/01-start-01\.//') JOB_LOG_DIR="${WORKFLOW_LOG_DIR%scheduler/log}" JOB_LOG_DIR="${JOB_LOG_DIR/$HOME/\$HOME}" DEFAULT_PATHS='--path=/bin --path=/usr/bin --path=/usr/local/bin --path=/sbin --path=/usr/sbin --path=/usr/local/sbin' cmp_ok log <<__END__ ERROR - [jobs-submit cmd] cylc jobs-submit --debug ${DEFAULT_PATHS} -- '${JOB_LOG_DIR}job' 1/foo/01 [jobs-submit ret_code] -9 [jobs-submit err] killed on timeout (PT10S) __END__ cylc workflow-state --old-format "${WORKFLOW_NAME}" > workflow-state.log # make sure foo submit failed and the stopper ran contains_ok workflow-state.log << __END__ stopper, 1, succeeded foo, 1, submit-failed __END__ purge cylc-flow-8.6.4/tests/functional/job-submission/08-activity-log-host.t0000775000175000017500000000255615202510242026067 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job submission, activity log has remote host name export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" \ "${WORKFLOW_NAME}" purge exit ././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootcylc-flow-8.6.4/tests/functional/job-submission/14-tidy-submits-of-prev-run-remote-host-with-shared-fs.tcylc-flow-8.6.4/tests/functional/job-submission/14-tidy-submits-of-prev-run-remote-host-with-shared-0000775000175000017500000000362315202510242033601 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test tidy of submits of previous runs. export REQUIRE_PLATFORM='loc:remote fs:shared' . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" LOGD1="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/t1/01" LOGD2="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/t1/02" exists_ok "${LOGD1}" exists_ok "${LOGD2}" sed -i 's/script =.*$/script = true/' "${WORKFLOW_RUN_DIR}/flow.cylc" sed -i -n '1,/triggered off/p' "${WORKFLOW_RUN_DIR}/reference.log" delete_db workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" exists_ok "${LOGD1}" exists_fail "${LOGD2}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/04-submit-num.t0000775000175000017500000000300715202510242024565 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test usage of CYLC_TASK_SUBMIT_NUMBER. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/19-platform_select.t0000775000175000017500000000352015202510242025656 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test recovery of a failed host select command for a group of tasks. . "$(dirname "$0")/test_header" set_test_number 8 install_workflow "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" reftest_run logfile="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Check that host = $(cmd) is correctly evaluated grep_ok \ "1/host_subshell/01:.* evaluated as 'improbable host name'" \ "${logfile}" grep_ok \ "1/localhost_subshell/01:.* evaluated as 'localhost'" \ "${logfile}" # Check that host = `cmd` is correctly evaluated grep_ok \ "1/host_subshell_backticks/01:.* evaluated as 'improbable host name'" \ "${logfile}" # Check that platform = $(cmd) correctly evaluated grep_ok \ "1/platform_subshell:.* evaluated as 'improbable platform name'" \ "${logfile}" grep_ok \ "1/platform_subshell_empty:.* evaluated as ''" \ "${logfile}" grep_ok \ "1/platform_subshell_suffix:.* evaluated as 'prefix-middle-suffix'" \ "${logfile}" # purge cylc-flow-8.6.4/tests/functional/job-submission/13-tidy-submits-of-prev-run-remote-host0000777000175000017500000000000015202510242036250 212-tidy-submits-of-prev-runustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/09-activity-log-host-bad-submit/0000775000175000017500000000000015202510242027715 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/09-activity-log-host-bad-submit/reference.log0000664000175000017500000000026015202510242032354 0ustar alastairalastairInitial point: 19990101T0000Z Final point: 19990101T0000Z 19990101T0000Z/bad-submitter -triggered off [] 19990101T0000Z/grepper -triggered off ['19990101T0000Z/bad-submitter'] cylc-flow-8.6.4/tests/functional/job-submission/09-activity-log-host-bad-submit/flow.cylc0000664000175000017500000000140015202510242031533 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [[events]] expected task failures = 19990101T0000Z/bad-submitter [scheduling] initial cycle point=1999 final cycle point=1999 [[graph]] P1Y = bad-submitter:submit-failed? => grepper [runtime] [[root]] [[bad-submitter]] script = true platform = {{ CYLC_TEST_PLATFORM }} [[grepper]] script = """ set -x # Test that the original command is printed A_LOG="$(dirname "$0")/../../bad-submitter/01/job-activity.log" grep '\[jobs-submit cmd\] ssh .* {{CYLC_TEST_HOST}} .*cylc jobs-submit.*' \ "${A_LOG}" # Stop the workflow cleanly cylc stop "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/job-submission/10-at-shell/0000775000175000017500000000000015202510242024003 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/10-at-shell/reference.log0000664000175000017500000000007015202510242026441 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/10-at-shell/flow.cylc0000664000175000017500000000023215202510242025623 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = true platform = {{ environ['CYLC_TEST_PLATFORM'] }} cylc-flow-8.6.4/tests/functional/job-submission/09-activity-log-host-bad-submit.t0000775000175000017500000000320115202510242030101 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test bad job submission, activity log has original command and some stderr # with the host name written. export REQUIRE_PLATFORM='runner:at loc:remote' . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config '' " [platforms] [[${CYLC_TEST_PLATFORM}]] job runner = at job runner command template = at non " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" \ -s "CYLC_TEST_HOST='${CYLC_TEST_HOST}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test \ -s "CYLC_TEST_HOST='${CYLC_TEST_HOST}'" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/test_header0000777000175000017500000000000015202510242030371 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/08-activity-log-host/0000775000175000017500000000000015202510242025667 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/08-activity-log-host/reference.log0000664000175000017500000000041515202510242030330 0ustar alastairalastairInitial point: 19990101T0000Z Final point: 19990101T0000Z 19990101T0000Z/sleeper -triggered off [] 19990101T0000Z/killer -triggered off ['19990101T0000Z/sleeper'] 19990101T0000Z/releaser -triggered off ['19990101T0000Z/killer'] 19990101T0000Z/sleeper -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/08-activity-log-host/flow.cylc0000664000175000017500000000115415202510242027513 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] initial cycle point = 1999 final cycle point = 1999 [[graph]] P1Y = sleeper:start => killer => releaser [runtime] [[sleeper]] script = test "${CYLC_TASK_SUBMIT_NUMBER}" -eq 2 || sleep 60 platform = {{ CYLC_TEST_PLATFORM }} [[[job]]] execution retry delays = PT1S [[killer]] script = cylc kill "${CYLC_WORKFLOW_ID}//1999*/sleeper" [[releaser]] script = """ cylc__job__wait_cylc_message_started cylc release "${CYLC_WORKFLOW_ID}//1999*/sleeper" """ cylc-flow-8.6.4/tests/functional/job-submission/11-garbage-platform-command.t0000775000175000017500000000302415202510242027310 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job submission rundb task_jobs entry with garbage host command. . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config ' [platforms] [[badhost]] ' install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" sqlite3 \ "$RUN_DIR/${WORKFLOW_NAME}/log/db" \ "SELECT submit_num,submit_status FROM task_jobs WHERE name=='t1'" \ >'sqlite3.out' cmp_ok 'sqlite3.out' <<'__OUT__' 1|1 2|0 __OUT__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/21-late-sub-message.t0000775000175000017500000000347115202510242025627 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test that a late job submitted message does not put the task back in the # submitted state (it is possible, though unlikely, for the job started # message to arrive first). # Uses a modified background job runner (defined in the workflow source # directory) that sleeps before returning after submitting the job. . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config "" " [platforms] [[wobblygibblets]] hosts = localhost job runner = delayed_background install target = localhost " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" sqlite3 "$RUN_DIR/${WORKFLOW_NAME}/log/db" \ 'SELECT name, cycle, event from task_events;' >'sqlite3.out' cmp_ok 'sqlite3.out' <<'__OUT__' foo|1|submitted foo|1|started foo|1|succeeded bar|1|submitted bar|1|started bar|1|succeeded __OUT__ purge cylc-flow-8.6.4/tests/functional/job-submission/20-check-chunking/0000775000175000017500000000000015202510242025154 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/20-check-chunking/reference.log0000664000175000017500000000035715202510242027622 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1_p3 -triggered off [] 1/t1_p4 -triggered off [] 1/t1_p2 -triggered off [] 1/t1_p5 -triggered off [] 1/t1_p1 -triggered off [] 1/fin -triggered off ['1/t1_p1', '1/t1_p2', '1/t1_p3', '1/t1_p4', '1/t1_p5'] cylc-flow-8.6.4/tests/functional/job-submission/20-check-chunking/flow.cylc0000664000175000017500000000053515202510242027002 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on inactivity timeout = True abort on stall timeout = True stall timeout = PT0S inactivity timeout = PT10M [task parameters] p = 1..5 [scheduling] [[graph]] R1 = t1

=> fin [runtime] [[t1

]] platform = {{ CYLC_TEST_PLATFORM }} [[fin]] cylc-flow-8.6.4/tests/functional/job-submission/11-garbage-platform-command/0000775000175000017500000000000015202510242027121 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/11-garbage-platform-command/reference.log0000664000175000017500000000015315202510242031561 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/job-submission/11-garbage-platform-command/flow.cylc0000664000175000017500000000063015202510242030743 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = """t1:submit-fail? => t2""" [runtime] [[t1]] script = true platform = badhost [[t2]] script = """ cylc broadcast "${CYLC_WORKFLOW_ID}" \ -n 't1' -p '1' -s 'platform=localhost' cylc trigger "${CYLC_WORKFLOW_ID}//1/t1" """ cylc-flow-8.6.4/tests/functional/job-submission/03-job-nn-remote-host-with-shared-fs.t0000775000175000017500000000255015202510242030740 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job log NN link correctness on reaching 100, remote (with shared fs). export REQUIRE_PLATFORM='loc:remote fs:shared' . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" mkdir "${WORKFLOW_RUN_DIR}/.service" sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" <'db.sqlite3' workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --reference-test --debug --no-detach --upgrade "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/13-tidy-submits-of-prev-run-remote-host.t0000775000175000017500000000471615202510242031554 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test tidy of submits of previous runs. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 11 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" RLOGD1="cylc-run/${WORKFLOW_NAME}/log/job/1/t1/01" RLOGD2="cylc-run/${WORKFLOW_NAME}/log/job/1/t1/02" LOGD1="${RUN_DIR}/${WORKFLOW_NAME}/log/job/1/t1/01" LOGD2="${RUN_DIR}/${WORKFLOW_NAME}/log/job/1/t1/02" SSH='ssh -n -oBatchMode=yes -oConnectTimeout=5' # shellcheck disable=SC2086 run_ok "exists-rlogd1" ${SSH} "${CYLC_TEST_HOST}" test -e "${RLOGD1}" # shellcheck disable=SC2086 run_ok "exists-rlogd2" ${SSH} "${CYLC_TEST_HOST}" test -e "${RLOGD2}" exists_ok "${LOGD1}" exists_ok "${LOGD2}" sed -i 's/script =.*$/script = true/' "${RUN_DIR}/${WORKFLOW_NAME}/flow.cylc" sed -i -n '1,/triggered off/p' "${RUN_DIR}/${WORKFLOW_NAME}/reference.log" delete_db workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" # shellcheck disable=SC2086 run_ok "exists-rlogd1" ${SSH} "${CYLC_TEST_HOST}" test -e "${RLOGD1}" # shellcheck disable=SC2086 run_fail "not-exists-rlogd2" ${SSH} "${CYLC_TEST_HOST}" test -e "${RLOGD2}" exists_ok "${LOGD1}" exists_fail "${LOGD2}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/01-job-nn-localhost.t0000775000175000017500000000246115202510242025636 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job log NN link correctness on reaching 100, localhost. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" mkdir -p "${WORKFLOW_RUN_DIR}/.service/" sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" <'db.sqlite3' workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --reference-test --upgrade --debug --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/18-clean-env/0000775000175000017500000000000015202510242024152 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/18-clean-env/flow.cylc0000664000175000017500000000055515202510242026002 0ustar alastairalastair[cylc] [[events]] inactivity timeout = PT30S abort on inactivity timeout = True abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ echo "BEEF ${BEEF:-undefined}" echo "CHEESE ${CHEESE:-undefined}" """ cylc-flow-8.6.4/tests/functional/job-submission/07-multi/0000775000175000017500000000000015202510242023432 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/07-multi/reference.log0000664000175000017500000001675415202510242026110 0ustar alastairalastairInitial point: 20200101T0000Z Final point: 20250101T0000Z 20200101T0000Z/t5 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t1 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t0 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t2 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t4 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t3 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20200101T0000Z/t6 -triggered off ['20190101T0000Z/t0', '20190101T0000Z/t1', '20190101T0000Z/t2', '20190101T0000Z/t3', '20190101T0000Z/t4', '20190101T0000Z/t5', '20190101T0000Z/t6'] 20210101T0000Z/t5 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t1 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t0 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t2 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t4 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t3 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20210101T0000Z/t6 -triggered off ['20200101T0000Z/t0', '20200101T0000Z/t1', '20200101T0000Z/t2', '20200101T0000Z/t3', '20200101T0000Z/t4', '20200101T0000Z/t5', '20200101T0000Z/t6'] 20220101T0000Z/t5 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t2 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t3 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t0 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t4 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t1 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20220101T0000Z/t6 -triggered off ['20210101T0000Z/t0', '20210101T0000Z/t1', '20210101T0000Z/t2', '20210101T0000Z/t3', '20210101T0000Z/t4', '20210101T0000Z/t5', '20210101T0000Z/t6'] 20230101T0000Z/t2 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t4 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t5 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t1 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t6 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t3 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20230101T0000Z/t0 -triggered off ['20220101T0000Z/t0', '20220101T0000Z/t1', '20220101T0000Z/t2', '20220101T0000Z/t3', '20220101T0000Z/t4', '20220101T0000Z/t5', '20220101T0000Z/t6'] 20240101T0000Z/t5 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t0 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t2 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t3 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t6 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t1 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20240101T0000Z/t4 -triggered off ['20230101T0000Z/t0', '20230101T0000Z/t1', '20230101T0000Z/t2', '20230101T0000Z/t3', '20230101T0000Z/t4', '20230101T0000Z/t5', '20230101T0000Z/t6'] 20250101T0000Z/t4 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t6 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t2 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t3 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t1 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t0 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] 20250101T0000Z/t5 -triggered off ['20240101T0000Z/t0', '20240101T0000Z/t1', '20240101T0000Z/t2', '20240101T0000Z/t3', '20240101T0000Z/t4', '20240101T0000Z/t5', '20240101T0000Z/t6'] cylc-flow-8.6.4/tests/functional/job-submission/07-multi/flow.cylc0000664000175000017500000000051315202510242025254 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] initial cycle point = 2020 final cycle point = 2025 [[graph]] P1Y = T[-P1Y]:succeed-all => T [runtime] [[T]] script = true [[t0,t1,t2,t3]] inherit = T [[t4,t5,t6]] inherit = T platform = {{CYLC_TEST_PLATFORM}} cylc-flow-8.6.4/tests/functional/job-submission/16-timeout/0000775000175000017500000000000015202510242023766 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/16-timeout/flow.cylc0000664000175000017500000000033615202510242025613 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = "foo:submit-fail? => stopper" [runtime] [[foo]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[stopper]] script = cylc stop "${CYLC_WORKFLOW_ID}" cylc-flow-8.6.4/tests/functional/job-submission/15-garbage-platform-command-2.t0000775000175000017500000000223615202510242027457 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test recovery of a failed host select command. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-submission/12-tidy-submits-of-prev-run.t0000775000175000017500000000330515202510242027300 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test tidy of submits of previous runs. . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" LOGD1="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/t1/01" LOGD2="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/t1/02" exists_ok "${LOGD1}" exists_ok "${LOGD2}" sed -i 's/script =.*$/script = true/' "$RUN_DIR/$WORKFLOW_NAME/flow.cylc" sed -i -n '1,/triggered off/p' "$RUN_DIR/$WORKFLOW_NAME/reference.log" delete_db workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" exists_ok "${LOGD1}" exists_fail "${LOGD2}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-submission/17-remote-localtime.t0000775000175000017500000000200215202510242025725 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that workflow does not set remote job TZ when in local time. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/job-submission/04-submit-num/0000775000175000017500000000000015202510242024375 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-submission/04-submit-num/flow.cylc0000664000175000017500000000141715202510242026223 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT30S [scheduling] [[graph]] R1 = """ foo:fail? => bar foo? & bar => baz """ [runtime] [[foo]] script = """ echo "${CYLC_TASK_SUBMIT_NUMBER}" \ >>"${CYLC_WORKFLOW_RUN_DIR}/foo-submits.txt" # bash 4.2.0 bug: ((VAR == VAL)) does not trigger 'set -e': test "${CYLC_TASK_SUBMIT_NUMBER}" -gt "${CYLC_TASK_TRY_NUMBER}" """ [[[job]]] execution retry delays=2*PT0S [[bar]] script = cylc trigger "${CYLC_WORKFLOW_ID}//1/foo" [[baz]] script = """ printf "%d\n" {1..4} | cmp - "${CYLC_WORKFLOW_RUN_DIR}/foo-submits.txt" """ cylc-flow-8.6.4/tests/functional/graph-equivalence/0000775000175000017500000000000015202510242022511 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/03-multiline_and1.t0000664000175000017500000000413115202510242026022 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graph = "a & b => c" # gives the same result as # graph = """a => c # b => c""" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'multiline_and1' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- delete_db TEST_NAME="${TEST_NAME_BASE}-check-c" cylc play "${WORKFLOW_NAME}" --hold-after=1 1>'out' 2>&1 poll_grep_workflow_log 'Setting hold cycle point' cylc show "${WORKFLOW_NAME}//1/c" | sed -n "/prerequisites/,/outputs/p" > 'c-prereqs' contains_ok "${TEST_SOURCE_DIR}/multiline_and_refs/c-ref" 'c-prereqs' cylc shutdown "${WORKFLOW_NAME}" --now #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graph-equivalence/00-oneline.t0000664000175000017500000000455415202510242024554 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graph="a=>b=>c" gives the same result as # graph = """a => b\ # => c""" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" test1 #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-a" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/a-ref" 'a-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-b" poll_grep_workflow_log 'INFO - DONE' cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/b-ref" 'b-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-c" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/c-ref" 'c-prereqs' #------------------------------------------------------------------------------- cylc shutdown --max-polls=10 --interval=2 --now "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/graph-equivalence/test3/0000775000175000017500000000000015202510242023553 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/test3/reference.log0000664000175000017500000000015415202510242026214 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/c -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/graph-equivalence/test3/flow.cylc0000664000175000017500000000104315202510242025374 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = """a => b \ => c""" [runtime] [[a]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/a" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/a-prereqs """ [[b]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/b" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/b-prereqs """ [[c]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/c" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/c-prereqs """ cylc-flow-8.6.4/tests/functional/graph-equivalence/test2/0000775000175000017500000000000015202510242023552 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/test2/reference.log0000664000175000017500000000015415202510242026213 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/c -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/graph-equivalence/test2/flow.cylc0000664000175000017500000000104215202510242025372 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = """a => b b => c""" [runtime] [[a]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/a" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/a-prereqs """ [[b]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/b" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/b-prereqs """ [[c]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/c" \ | sed -n "/prerequisites/,/outputs/p" > {{TEST_OUTPUT_PATH}}/c-prereqs """ cylc-flow-8.6.4/tests/functional/graph-equivalence/01-twolines.t0000664000175000017500000000452015202510242024761 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graph = """a => b # b => c""" gives the same result as # graph = "a => b => c" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" test2 #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-a" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/a-ref" 'a-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-b" poll_grep_workflow_log 'INFO - DONE' cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/b-ref" 'b-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-c" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/c-ref" 'c-prereqs' #------------------------------------------------------------------------------- cylc shutdown "${WORKFLOW_NAME}" --now purge cylc-flow-8.6.4/tests/functional/graph-equivalence/splitline_refs/0000775000175000017500000000000015202510242025533 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/splitline_refs/b-ref0000664000175000017500000000013215202510242026445 0ustar alastairalastairprerequisites: ('⨯': not satisfied) ✓ 1/a succeeded outputs: ('⨯': not completed) cylc-flow-8.6.4/tests/functional/graph-equivalence/splitline_refs/a-ref0000664000175000017500000000006615202510242026452 0ustar alastairalastairprerequisites: (None) outputs: ('⨯': not completed) cylc-flow-8.6.4/tests/functional/graph-equivalence/splitline_refs/c-ref0000664000175000017500000000013215202510242026446 0ustar alastairalastairprerequisites: ('⨯': not satisfied) ✓ 1/b succeeded outputs: ('⨯': not completed) cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and2/0000775000175000017500000000000015202510242025417 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and2/reference.log0000664000175000017500000000015615202510242030062 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/c -triggered off ['1/a', '1/b'] cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and2/flow.cylc0000664000175000017500000000017515202510242027245 0ustar alastairalastair[scheduling] [[graph]] R1 = """a => c b => c""" [runtime] [[a,b,c]] script = true cylc-flow-8.6.4/tests/functional/graph-equivalence/test_header0000777000175000017500000000000015202510242031026 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/02-splitline.t0000664000175000017500000000443515202510242025126 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graph = """a => b\ # => c""" gives the same result as # graph = """a => b # b => c""" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" test3 #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ --set="TEST_OUTPUT_PATH='${PWD}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-a" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/a-ref" 'a-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-b" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/b-ref" 'b-prereqs' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-check-c" cmp_ok "${TEST_SOURCE_DIR}/splitline_refs/c-ref" 'c-prereqs' #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and_refs/0000775000175000017500000000000015202510242026354 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and_refs/c-ref0000664000175000017500000000016715202510242027277 0ustar alastairalastairprerequisites: ('⨯': not satisfied) ⨯ 1/a succeeded ⨯ 1/b succeeded outputs: ('⨯': not completed) (None) cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and_refs/c-ref-20000664000175000017500000000016715202510242027436 0ustar alastairalastairprerequisites: ('⨯': not satisfied) ⨯ 1/a succeeded ⨯ 1/b succeeded outputs: ('⨯': not completed) (None) cylc-flow-8.6.4/tests/functional/graph-equivalence/04-multiline_and2.t0000664000175000017500000000417015202510242026027 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graph = """a => c # b => c""" # gives the same result as # graph = "a & b => c" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'multiline_and2' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- delete_db TEST_NAME="${TEST_NAME_BASE}-check-c" cylc play "${WORKFLOW_NAME}" --hold-after=1 1>'out' 2>&1 poll_grep_workflow_log 'Setting hold cycle point' cylc show "${WORKFLOW_NAME}//1/c" | sed -n "/prerequisites/,/outputs/p" > 'c-prereqs' contains_ok "${TEST_SOURCE_DIR}/multiline_and_refs/c-ref-2" 'c-prereqs' cylc shutdown --max-polls=20 --interval=2 --now "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graph-equivalence/test1/0000775000175000017500000000000015202510242023551 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/test1/reference.log0000664000175000017500000000013315202510242026207 0ustar alastairalastairFinal point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/c -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/graph-equivalence/test1/flow.cylc0000664000175000017500000000123615202510242025376 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = "a => b => c" [runtime] [[a]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/a" \ | sed -n "/prerequisites/,/outputs/p" \ > {{TEST_OUTPUT_PATH}}/a-prereqs """ [[b]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/b" \ | sed -n "/prerequisites/,/outputs/p" \ > {{TEST_OUTPUT_PATH}}/b-prereqs """ [[c]] script = """ cylc show "${CYLC_WORKFLOW_ID}//1/c" \ | sed -n "/prerequisites/,/outputs/p" \ > {{TEST_OUTPUT_PATH}}/c-prereqs """ cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and1/0000775000175000017500000000000015202510242025416 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and1/reference.log0000664000175000017500000000015615202510242030061 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/c -triggered off ['1/a', '1/b'] cylc-flow-8.6.4/tests/functional/graph-equivalence/multiline_and1/flow.cylc0000664000175000017500000000014315202510242027237 0ustar alastairalastair[scheduling] [[graph]] R1 = "a & b => c" [runtime] [[a,b,c]] script = true cylc-flow-8.6.4/tests/functional/task-name/0000775000175000017500000000000015202510242020771 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/task-name/00-basic/0000775000175000017500000000000015202510242022267 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/task-name/00-basic/reference.log0000664000175000017500000000263615202510242024737 0ustar alastairalastairInitial point: 20150101T0000Z Final point: 20200101T0000Z 20150101T0000Z/t1 -triggered off ['20130101T0000Z/t1'] 20160101T0000Z/t1 -triggered off ['20140101T0000Z/t1'] 20170101T0000Z/t1 -triggered off ['20150101T0000Z/t1'] 20150101T0000Z/t1-a -triggered off ['20150101T0000Z/t1'] 20160101T0000Z/t1-a -triggered off ['20160101T0000Z/t1'] 20150101T0000Z/t1+a -triggered off ['20150101T0000Z/t1-a'] 20170101T0000Z/t1-a -triggered off ['20170101T0000Z/t1'] 20160101T0000Z/t1+a -triggered off ['20160101T0000Z/t1-a'] 20150101T0000Z/t1%a -triggered off ['20150101T0000Z/t1+a'] 20170101T0000Z/t1+a -triggered off ['20170101T0000Z/t1-a'] 20160101T0000Z/t1%a -triggered off ['20160101T0000Z/t1+a'] 20180101T0000Z/t1 -triggered off ['20160101T0000Z/t1'] 20170101T0000Z/t1%a -triggered off ['20170101T0000Z/t1+a'] 20190101T0000Z/t1 -triggered off ['20170101T0000Z/t1'] 20180101T0000Z/t1-a -triggered off ['20180101T0000Z/t1'] 20200101T0000Z/t1 -triggered off ['20180101T0000Z/t1'] 20190101T0000Z/t1-a -triggered off ['20190101T0000Z/t1'] 20180101T0000Z/t1+a -triggered off ['20180101T0000Z/t1-a'] 20200101T0000Z/t1-a -triggered off ['20200101T0000Z/t1'] 20190101T0000Z/t1+a -triggered off ['20190101T0000Z/t1-a'] 20180101T0000Z/t1%a -triggered off ['20180101T0000Z/t1+a'] 20200101T0000Z/t1+a -triggered off ['20200101T0000Z/t1-a'] 20190101T0000Z/t1%a -triggered off ['20190101T0000Z/t1+a'] 20200101T0000Z/t1%a -triggered off ['20200101T0000Z/t1+a'] cylc-flow-8.6.4/tests/functional/task-name/00-basic/flow.cylc0000664000175000017500000000043215202510242024111 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20150101 final cycle point = 20200101 [[graph]] P1Y = """ t1[-P2Y] => t1 t1 => t1-a => t1+a => t1%a """ [runtime] [[root]] script = printenv CYLC_TASK_ID [[t1, t1-a, t1+a, t1%a]] cylc-flow-8.6.4/tests/functional/task-name/test_header0000777000175000017500000000000015202510242027306 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/task-name/00-basic.t0000664000175000017500000000165515202510242022463 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Basic task names tests. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-subscribe/0000775000175000017500000000000015202510242022022 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-subscribe/01-subscribe.t0000664000175000017500000000345015202510242024410 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test `cylc subscribe`. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 6 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo => bar => wiq => qux [runtime] [[foo, bar, wiq, qux]] script = sleep 2 __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-subscribe-1" run_ok "${TEST_NAME}" cylc subscribe --once --topics="workflow" "${WORKFLOW_NAME}" grep_ok "lastUpdated" "${TEST_NAME}.stdout" TEST_NAME="${TEST_NAME_BASE}-subscribe-2" run_ok "${TEST_NAME}" cylc subscribe --once --topics="workflow" "${WORKFLOW_NAME}" grep_ok "lastUpdated" "${TEST_NAME}.stdout" cylc stop --kill --max-polls=20 --interval=1 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/cylc-subscribe/test_header0000777000175000017500000000000015202510242030337 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/0000775000175000017500000000000015202510242021352 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/00-satellite.t0000664000175000017500000000171215202510242023743 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the external trigger test workflow . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/ext-trigger/02-cycle-point.t0000664000175000017500000000212515202510242024204 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test GitHub #1893 - cycle point specific external triggers. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/ext-trigger/01-no-nudge/0000775000175000017500000000000015202510242023304 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/01-no-nudge/flow.cylc0000664000175000017500000000256115202510242025133 0ustar alastairalastair[meta] title = "Test for Github Issue 1543" description = """ External trigger events should stimulate task processing even when nothing else is happening in the workflow. Here, long-running task bar ext-triggers foo when nothing else is happening. If task processing occurs foo will submit and kill bar, allowing the workflow to shutdown. Otherwise, foo won't submit, bar will keep running, and the workflow will time out. """ [scheduler] [[events]] abort on stall timeout = True stall timeout = PT30S abort on inactivity timeout = True inactivity timeout = PT30S [scheduling] [[special tasks]] external-trigger = foo("drugs and money") [[graph]] # killed tasks are held to prevent retries; they have to be # released before they can be removed. R1 = """ foo & bar? bar:fail? => handler """ [runtime] [[foo]] script = """ cylc kill "$CYLC_WORKFLOW_ID//1/bar" cylc__job__poll_grep_workflow_log -E '1/bar.* \(internal\)failed' cylc release "$CYLC_WORKFLOW_ID//1/bar" """ [[bar]] script = """ sleep 5 cylc ext-trigger $CYLC_WORKFLOW_ID "drugs and money" 12345 sleep 60 """ [[handler]] script = true cylc-flow-8.6.4/tests/functional/ext-trigger/02-cycle-point/0000775000175000017500000000000015202510242024017 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/02-cycle-point/flow.cylc0000664000175000017500000000147615202510242025652 0ustar alastairalastair[meta] title = "Test for Github Issue 1893" description = """Cycle-point specific external triggering in a date-time cycling workflow. The workflow will time out and abort if the ext trigger fails.""" [scheduler] cycle point format = %Y [[events]] abort on stall timeout = True stall timeout = PT30S [scheduling] initial cycle point = 2020 final cycle point = 2020 [[special tasks]] external-trigger = ext("cheese on toast for $CYLC_TASK_CYCLE_POINT") [[graph]] P1Y = ext & trig [runtime] [[ext]] # Externally triggered task. script = echo $CYLC_EXT_TRIGGER_ID [[trig]] # Task to do the "external" triggering. script = cylc ext-trigger $CYLC_WORKFLOW_ID \ "cheese on toast for $CYLC_TASK_CYCLE_POINT" "blarghh!" cylc-flow-8.6.4/tests/functional/ext-trigger/00-satellite/0000775000175000017500000000000015202510242023555 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/00-satellite/reference.log0000664000175000017500000000160615202510242026221 0ustar alastairalastairInitial point: 1 Final point: 5 1/prep -triggered off [] 1/satsim -triggered off ['1/prep'] 1/get_data -triggered off ['0/get_data', '1/prep'] 2/get_data -triggered off ['1/get_data'] 1/proc1 -triggered off ['1/get_data'] 1/proc2 -triggered off ['1/proc1'] 3/get_data -triggered off ['2/get_data'] 2/proc1 -triggered off ['2/get_data'] 4/get_data -triggered off ['3/get_data'] 1/products -triggered off ['1/proc2'] 3/proc1 -triggered off ['3/get_data'] 2/proc2 -triggered off ['2/proc1'] 5/get_data -triggered off ['4/get_data'] 2/products -triggered off ['2/proc2'] 3/proc2 -triggered off ['3/proc1'] 4/proc1 -triggered off ['4/get_data'] 4/proc2 -triggered off ['4/proc1'] 3/products -triggered off ['3/proc2'] 5/proc1 -triggered off ['5/get_data'] 4/products -triggered off ['4/proc2'] 5/proc2 -triggered off ['5/proc1'] 5/products -triggered off ['5/proc2'] 5/collate -triggered off ['5/products'] cylc-flow-8.6.4/tests/functional/ext-trigger/00-satellite/flow.cylc0000664000175000017500000001027715202510242025407 0ustar alastairalastair#!Jinja2 # TEST WORKFLOW ADAPTED FROM examples/satellite/ext-trigger/ [meta] title = Real time satellite data processing demo, variant 3 of 3 description = """ Successive cycle points retrieve and processes the next arbitrarily timed and labelled dataset, in parallel if the data comes in quickly. This variant of the workflow has initial get_data tasks with external triggers: they do not submit until triggered by an external system. """ # Note that the satellite simulator task here that supplies the external event # trigger happens to be a workflow task - i.e. it is not really "external" - but # this is only a convenience - an easy route to a self-contained example workflow. # you can monitor output processing with: # $ watch -n 1 \ # "find ~/cylc-run//share; find ~/cylc-run//work" {% set N_DATASETS = 5 %} # define shared directories (could use runtime namespaces for this) {% set DATA_IN_DIR = "$CYLC_WORKFLOW_SHARE_DIR/incoming" %} {% set PRODUCT_DIR = "$CYLC_WORKFLOW_SHARE_DIR/products" %} [scheduler] UTC mode = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = {{N_DATASETS}} runahead limit = P4 [[special tasks]] external-trigger = get_data("new dataset ready for processing") [[graph]] R1 = prep => satsim & get_data P1 = """ # Processing chain for each dataset get_data => proc1 => proc2 => products # As one dataset is retrieved, start waiting on another. get_data[-P1] => get_data """ R1//{{N_DATASETS}} = products => collate # last cycle [runtime] [[prep]] script = rm -rf $CYLC_WORKFLOW_SHARE_DIR $CYLC_WORKFLOW_WORK_DIR [[[meta]]] title = clean the workflow output directories [[satsim]] pre-script = mkdir -p {{DATA_IN_DIR}} script = """ COUNT=0 while ((COUNT < {{N_DATASETS}})); do # sleep $((RANDOM % 20)) # Generate datasets very quickly to test parallel processing. DATA_ID=$(date +%s).$((RANDOM % 100)) DATA_FILE=dataset-${DATA_ID}.raw touch {{DATA_IN_DIR}}/$DATA_FILE ((COUNT += 1)) # (required to distinguish fast-arriving messages). # Trigger downstream processing in the workflow. cylc ext-trigger $CYLC_WORKFLOW_ID \ "new dataset ready for processing" $DATA_ID done """ [[[meta]]] title = simulate a satellite data feed description = """ Generates {{N_DATASETS}} arbitrarily labelled datasets very quickly, to show parallel processing streams. """ [[WORKDIR]] # Define a common cycle-point-specific work-directory for all # processing tasks so that they all work on the same dataset. work sub-directory = proc-$CYLC_TASK_CYCLE_POINT [[[environment]]] DATASET = dataset-$CYLC_EXT_TRIGGER_ID [[get_data]] inherit = WORKDIR script = mv {{DATA_IN_DIR}}/${DATASET}.raw $PWD [[[meta]]] title = retrieve next dataset description = just do it - we know it exists already [[proc1]] inherit = WORKDIR script = mv ${DATASET}.raw ${DATASET}.proc1 [[[meta]]] title = convert .raw dataset to .proc1 form [[proc2]] inherit = WORKDIR script = mv ${DATASET}.proc1 ${DATASET}.proc2 [[[meta]]] title = convert .proc1 dataset to .proc2 form [[products]] inherit = WORKDIR script = """ mkdir -p {{PRODUCT_DIR}} mv ${DATASET}.proc2 {{PRODUCT_DIR}}/${DATASET}.prod """ [[[meta]]] title = generate products from .proc2 processed dataset [[collate]] # Note you might want to use "cylc workflow-state" to check that # _all_ product tasks have finished before collating results. script = """ echo PRODUCTS: ls {{PRODUCT_DIR}} """ [[[meta]]] title = collate all products from the workflow run cylc-flow-8.6.4/tests/functional/ext-trigger/test_header0000777000175000017500000000000015202510242027667 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/ext-trigger/01-no-nudge.t0000664000175000017500000000250415202510242023472 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that external trigger events stimulate task processing even when nothing # else is happening in the workflow. # Note this test will probably become irrelevant once we go to entirely # event-driven scheduling. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/job-kill/0000775000175000017500000000000015202510242020614 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/03-slurm.t0000777000175000017500000000000015202510242025260 202-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/03-slurm/0000775000175000017500000000000015202510242022176 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/03-slurm/reference.log0000664000175000017500000000012615202510242024636 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/stop -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/job-kill/03-slurm/flow.cylc0000664000175000017500000000064315202510242024024 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = t1:start=>stop [runtime] [[t1]] script = sleep 120 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[directives]]] --time=03:00 [[stop]] script=""" cylc kill "$CYLC_WORKFLOW_ID//*/t1" cylc stop "$CYLC_WORKFLOW_ID" """ cylc-flow-8.6.4/tests/functional/job-kill/01-remote.t0000775000175000017500000000353115202510242022517 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test killing of jobs on a remote host. export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-ps run_fail "${TEST_NAME}" \ ssh -oBatchMode=yes -oConnectTimeout=5 -n "${CYLC_TEST_HOST}" \ "bash -c 'ps \$(cat cylc-run/${WORKFLOW_NAME}/work/*/t*/file)'" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-kill/04-pbs/0000775000175000017500000000000015202510242021621 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/04-pbs/reference.log0000664000175000017500000000012615202510242024261 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/stop -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/job-kill/04-pbs/flow.cylc0000664000175000017500000000110515202510242023441 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = t1:start=>stop [runtime] [[t1]] script = sleep 120 {% if "CYLC_TEST_PLATFORM" in environ and environ["CYLC_TEST_PLATFORM"] %} platform == {{ environ['CYLC_TEST_PLATFORM'] }} {% endif %} [[[job]]] execution time limit = PT2M [[[directives]]] -l select=1:ncpus=1:mem=15mb [[stop]] script=""" cylc kill "$CYLC_WORKFLOW_ID//*/t1" cylc stop "$CYLC_WORKFLOW_ID" """ cylc-flow-8.6.4/tests/functional/job-kill/04-pbs.t0000777000175000017500000000000015202510242024703 202-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/02-loadleveler.t0000775000175000017500000000226515202510242023526 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test killing of jobs submitted to loadleveler, slurm, pbs... # TODO Check this test on a dockerized system or VM. JOB_RUNNER="${0##*\/??-}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER%%.t} comms:tcp" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/job-kill/01-remote/0000775000175000017500000000000015202510242022325 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/01-remote/reference.log0000664000175000017500000000016515202510242024770 0ustar alastairalastairInitial point: 1 Final point: 1 1/t2 -triggered off [] 1/t1 -triggered off [] 1/stop -triggered off ['1/t1', '1/t2'] cylc-flow-8.6.4/tests/functional/job-kill/01-remote/flow.cylc0000664000175000017500000000101515202510242024145 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/t1, 1/t2 [scheduling] [[graph]] R1 = """ t1:start=>stop t2:start=>stop """ [runtime] [[T]] script=sleep 120 & echo $! >file; wait platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[t1]] inherit=T [[t2]] inherit=T [[stop]] script=""" cylc kill "$CYLC_WORKFLOW_ID//" '//1/t1' '//1/t2' || true cylc stop $CYLC_WORKFLOW_ID """ cylc-flow-8.6.4/tests/functional/job-kill/test_header0000777000175000017500000000000015202510242027131 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/02-loadleveler/0000775000175000017500000000000015202510242023331 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/02-loadleveler/reference.log0000664000175000017500000000012615202510242025771 0ustar alastairalastairInitial point: 1 Final point: 1 t11/ -triggered off [] 1/stop -triggered off ['t11/'] cylc-flow-8.6.4/tests/functional/job-kill/02-loadleveler/flow.cylc0000664000175000017500000000122515202510242025154 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1=t1:start=>stop [runtime] [[t1]] script=sleep 120 {% if "CYLC_TEST_PLATFORM" in environ and environ["CYLC_TEST_PLATFORM"] %} platform = {{ environ["CYLC_TEST_PLATFORM"] }} {% endif %} [[[directives]]] class=serial job_type=serial notification=never resources=ConsumableCpus(1) ConsumableMemory(64mb) wall_clock_limit=180,120 [[stop]] script=""" cylc kill "$CYLC_WORKFLOW_ID//*/t1" cylc stop "$CYLC_WORKFLOW_ID" """ cylc-flow-8.6.4/tests/functional/job-kill/00-local/0000775000175000017500000000000015202510242022123 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-kill/00-local/reference.log0000664000175000017500000000040415202510242024562 0ustar alastairalastairInitial point: 1 Final point: 1 1/t2 -triggered off [] 1/t1 -triggered off [] 1/stop1 -triggered off ['1/t1', '1/t2'] 1/t4 -triggered off ['1/stop1'] 1/t3 -triggered off ['1/stop1'] 1/stop2 -triggered off ['1/t3', '1/t4'] 1/shutdown -triggered off ['1/stop2'] cylc-flow-8.6.4/tests/functional/job-kill/00-local/flow.cylc0000664000175000017500000000145215202510242023750 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/t1, 1/t2, 1/t3, 1/t4, 1/stop2 [scheduling] [[graph]] R1 = """ t1:start & t2:start => stop1 => t3:start & t4:start => stop2? stop2:fail? => shutdown """ [runtime] [[T]] script = sleep 120 & echo $! >file; wait [[t1, t2, t3, t4]] inherit= T [[stop1]] script = """ # Kill 1/t1 and 1/t2 explicitly. cylc kill $CYLC_WORKFLOW_ID// //1/t1 //1/t2 || true """ [[stop2]] script = """ # Kill 1/t3, 1/t4, and myself! implicitly (kill all active tasks). cylc kill "$CYLC_WORKFLOW_ID//*" || true sleep 30 """ [[shutdown]] script = "cylc stop $CYLC_WORKFLOW_ID" cylc-flow-8.6.4/tests/functional/job-kill/00-local.t0000775000175000017500000000400615202510242022313 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test kill local jobs. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 10 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-ps for DIR in "${WORKFLOW_RUN_DIR}"/work/*/t*; do run_fail "${TEST_NAME}.$(basename "$DIR")" ps "$(cat "${DIR}/file")" done N=0 for FILE in "${WORKFLOW_RUN_DIR}"/log/job/*/t*/01/job.status; do run_fail "${TEST_NAME}-status-$((++N))" \ ps "$(awk -F= '$1 == "CYLC_JOB_PID" {print $2}' "$FILE")" done #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-graph-diff/0000775000175000017500000000000015202510242022050 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple.t0000664000175000017500000001100415202510242023737 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc graph --diff for two workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 15 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}-control" "${TEST_NAME_BASE}-control" CONTROL_WORKFLOW_NAME="${WORKFLOW_NAME}" install_workflow "${TEST_NAME_BASE}-diffs" "${TEST_NAME_BASE}-diffs" DIFF_WORKFLOW_NAME="${WORKFLOW_NAME}" install_workflow "${TEST_NAME_BASE}-same" "${TEST_NAME_BASE}-same" SAME_WORKFLOW_NAME="${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate-diffs" run_ok "${TEST_NAME}" cylc validate "${DIFF_WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate-same" run_ok "${TEST_NAME}" cylc validate "${SAME_WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate-new" run_ok "${TEST_NAME}" cylc validate "${CONTROL_WORKFLOW_NAME}" #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-bad-workflow-name" run_fail "${TEST_NAME}" \ cylc graph "${DIFF_WORKFLOW_NAME}" --diff "${CONTROL_WORKFLOW_NAME}.bad" cmp_ok "${TEST_NAME}.stdout" . #------------------------------------------------------------------------------- # Test for "cylc graph-diff WORKFLOW1 WORKFLOW2 -- ICP". . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}-1" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True [scheduling] [[graph]] R1 = foo => bar [runtime] [[foo, bar]] script = true __FLOW_CONFIG__ # shellcheck disable=SC2153 WORKFLOW_NAME1="${WORKFLOW_NAME}" init_workflow "${TEST_NAME_BASE}-2" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True [scheduling] [[graph]] R1 = food => barley [runtime] [[food, barley]] script = true __FLOW_CONFIG__ # shellcheck disable=SC2153 WORKFLOW_NAME2="${WORKFLOW_NAME}" run_fail "${TEST_NAME_BASE}" \ cylc graph "${WORKFLOW_NAME1}" --diff "${WORKFLOW_NAME2}" --icp='20200101T0000Z' contains_ok "${TEST_NAME_BASE}.stdout" <<__OUT__ -edge "20200101T0000Z/foo" "20200101T0000Z/bar" +edge "20200101T0000Z/food" "20200101T0000Z/barley" graph -node "20200101T0000Z/bar" "bar\n20200101T0000Z" -node "20200101T0000Z/foo" "foo\n20200101T0000Z" +node "20200101T0000Z/barley" "barley\n20200101T0000Z" +node "20200101T0000Z/food" "food\n20200101T0000Z" __OUT__ cmp_ok "${TEST_NAME_BASE}.stderr" <'/dev/null' purge "${WORKFLOW_NAME1}" purge "${WORKFLOW_NAME2}" exit cylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple-same0000777000175000017500000000000015202510242027526 200-simple-controlustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple-control/0000775000175000017500000000000015202510242025234 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple-control/flow.cylc0000664000175000017500000000037315202510242027062 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140808T00 [[graph]] R1 = cold_foo => foo T00 = foo => bar & baz [runtime] [[FOO]] [[foo]] inherit = None, FOO cylc-flow-8.6.4/tests/functional/cylc-graph-diff/test_header0000777000175000017500000000000015202510242030365 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple-diffs/0000775000175000017500000000000015202510242024647 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-graph-diff/00-simple-diffs/flow.cylc0000664000175000017500000000040415202510242026470 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140808T00 [[graph]] R1 = cold_foo => foo T00 = foo => bar => baz [runtime] [[FOO]] [[foo,bar,baz]] inherit = None, FOO cylc-flow-8.6.4/tests/functional/platforms/0000775000175000017500000000000015202510242021120 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/07-localhost-set-if-not-in-globalcfg/0000775000175000017500000000000015202510242027641 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/07-localhost-set-if-not-in-globalcfg/flow.cylc0000664000175000017500000000014515202510242031464 0ustar alastairalastair[meta] [scheduling] [[graph]] R1 = """foo""" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/platforms/13-ping-pong-host/0000775000175000017500000000000015202510242024212 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/13-ping-pong-host/flow.cylc0000664000175000017500000000201715202510242026035 0ustar alastairalastair#!jinja2 [scheduler] [[events]] stall timeout = PT0S [scheduling] cycling mode = integer final cycle point = 2 [[graph]] P1 = remote_task[-P1] => toggler => remote_task [runtime] [[remote_task]] [[[remote]]] host = $(cat ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info) [[toggler]] script = """ # Toggle the platform between localhost and the remote host # using the content of a file, ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info. # between localhost and the remote. if (( $CYLC_TASK_CYCLE_POINT % 2 == 1 )); then echo ${REMOTE_HOST} > ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info cylc message -- "changing platform to ${REMOTE_HOST}" else echo "localhost" > ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info cylc message -- "changing platform to localhost" fi """ [[[environment]]] REMOTE_HOST = {{ CYLC_TEST_HOST }} cylc-flow-8.6.4/tests/functional/platforms/03-platform-db/0000775000175000017500000000000015202510242023547 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/03-platform-db/flow.cylc0000664000175000017500000000034415202510242025373 0ustar alastairalastair[scheduling] [[graph]] R1 = frozen & disney [runtime] [[frozen]] script = "echo let it go" platform = elsa [[disney]] script = "echo cant hold it back anymore" platform = olaf cylc-flow-8.6.4/tests/functional/platforms/04-host-to-platform-upgrade-fail-inherit/0000775000175000017500000000000015202510242030556 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/04-host-to-platform-upgrade-fail-inherit/flow.cylc0000664000175000017500000000050615202510242032402 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] [[graph]] R1 = """ non-valid-child """ [runtime] [[root]] script = true [[VALID_PARENT]] [[[remote]]] host = parasite [[non-valid-child]] inherit = VALID_PARENT platform = _wibble cylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade.t0000664000175000017500000000462315202510242026375 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that platform upgraders work sensibly. # The following scenarios should be covered: # - Task with no settings # - Task with a host setting that should match the test platform export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Ensure that a mix of syntax will fail. run_fail "${TEST_NAME_BASE}-validate-fail" \ cylc validate "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/bad" # and fail in this specific manner: grep_ok "cannot be used with" "${TEST_NAME_BASE}-validate-fail.stderr" # Ensure that you can validate workflow run_ok "${TEST_NAME_BASE}-run" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_HOST='${CYLC_TEST_HOST}'" \ -s CYLC_TEST_HOST_FQDN="'$(ssh "$CYLC_TEST_HOST" hostname -f)'" # Check that the cfgspec/workflow.py has issued a warning about upgrades. grep_ok "\[t1\]\[remote\]host = ${CYLC_TEST_HOST}"\ "${TEST_NAME_BASE}-run.stderr" # the namespace with the host setting will be logged not the task that # inherits from it (because it happens in the cfgspec not the config) grep_ok "\[T2\]\[remote\]host = ${CYLC_TEST_HOST}"\ "${TEST_NAME_BASE}-run.stderr" # Run the workflow workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test \ -s CYLC_TEST_HOST="'$CYLC_TEST_HOST'" \ -s CYLC_TEST_HOST_FQDN="'$(ssh "$CYLC_TEST_HOST" hostname -f)'" \ "${WORKFLOW_NAME}" grep "host=" "${WORKFLOW_RUN_DIR}/log/scheduler/log" > hosts.log grep_ok "\[2/t2.*\].*host=${CYLC_TEST_HOST}" hosts.log purge exit cylc-flow-8.6.4/tests/functional/platforms/13-ping-pong-host.t0000664000175000017500000000306115202510242024377 0ustar alastairalastair#!/usr/bin/env bash # 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 . #----------------------------------------------------------------------------- # If a task has [remote]host=$(subshell) this should be evaluated # every time the task is run. # https://github.com/cylc/cylc-flow/issues/6808 export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --debug --no-detach named_grep_ok "1/remote_task submits to ${CYLC_TEST_PLATFORM}" \ "\[1/remote_task/01:preparing\] submitted to ${CYLC_TEST_HOST}" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" named_grep_ok "2/remote_task submits to localhost" \ "\[2/remote_task/01:preparing\] submitted to localhost" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge cylc-flow-8.6.4/tests/functional/platforms/03-platform-db.t0000664000175000017500000000325215202510242023736 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check db stores correct platform export REQUIRE_PLATFORM='loc:remote comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config '' " [platforms] [[elsa]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} [[olaf]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" DB_FILE="${WORKFLOW_RUN_DIR}/log/db" NAME='select-name-platform.out' sqlite3 "${DB_FILE}" 'SELECT name, platform_name FROM task_jobs ORDER BY name' \ >"${NAME}" cmp_ok "${NAME}" <<__SELECT__ disney|olaf frozen|elsa __SELECT__ purge exit cylc-flow-8.6.4/tests/functional/platforms/12-ping-pong.t0000664000175000017500000000307415202510242023427 0ustar alastairalastair#!/usr/bin/env bash # 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 . #----------------------------------------------------------------------------- # If a task has a platform set using a subshell this should be evaluated # every time the task is run. # https://github.com/cylc/cylc-flow/issues/6808 export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --debug --no-detach named_grep_ok "1/remote_task submits to ${CYLC_TEST_PLATFORM}" \ "\[1/remote_task/01:preparing\] submitted to ${CYLC_TEST_PLATFORM}" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" named_grep_ok "2/remote_task submits to localhost" \ "\[2/remote_task/01:preparing\] submitted to localhost" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge cylc-flow-8.6.4/tests/functional/platforms/01-platform-basic.t0000775000175000017500000000262115202510242024432 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating platforms in workflow. . "$(dirname "$0")/test_header" set_test_number 1 TEST_NAME="${TEST_NAME_BASE}-val" create_test_global_config "" " [platforms] [[localhost, lewis]] hosts = localhost install target = localhost " cat >'flow.cylc' <<'__FLOW_CONFIG__' [meta] title = "Test validation of simple multiple inheritance" description = """Bug identified at 5.1.1-314-g4960684.""" [scheduling] [[graph]] R1 = """foo""" [runtime] [[foo]] platform=lewis __FLOW_CONFIG__ run_ok "${TEST_NAME}" cylc validate . cylc-flow-8.6.4/tests/functional/platforms/11-platform-gone-on-restart.t0000664000175000017500000000414415202510242026375 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # If a platform is deleted from global config and restart cannot find an # approproiate match, don't keep going. # 1. Run a workflow which stops leaving a job running on a platform. # 2. Delete the platform from global.cylc # 3. Attempt to restart. # 4. Check that restart fails in the desired manner. . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config "" " [platforms] [[myplatform]] hosts = localhost install target = localhost " init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONF__' [scheduling] initial cycle point = 2934 [[graph]] R1 = foo => bar [runtime] [[foo]] script = """ cylc stop ${CYLC_WORKFLOW_ID} --now --now """ platform = myplatform [[bar]] script = true # only runs on restart __FLOW_CONF__ run_ok "${TEST_NAME_BASE}-play" \ cylc play "${WORKFLOW_NAME}" --no-detach # Wait for workflow to stop, then wreck the global config: create_test_global_config "" " " # Test that restart fails: run_fail "${TEST_NAME_BASE}-restart" \ cylc play "${WORKFLOW_NAME}" --no-detach named_grep_ok \ "${TEST_NAME_BASE}-cannot-restart" \ "platforms are not defined in the global.cylc" \ "${RUN_DIR}/${WORKFLOW_NAME}/log/scheduler/log" purge cylc-flow-8.6.4/tests/functional/platforms/12-ping-pong/0000775000175000017500000000000015202510242023236 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/12-ping-pong/flow.cylc0000664000175000017500000000172115202510242025062 0ustar alastairalastair#!jinja2 [scheduler] [[events]] stall timeout = PT0S [scheduling] cycling mode = integer final cycle point = 2 [[graph]] P1 = remote_task[-P1] => toggler => remote_task [runtime] [[remote_task]] platform = $(cat ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info) [[toggler]] # Toggle the platform between localhost and the remote host # using the content of a file, ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info. script = """ if (( $CYLC_TASK_CYCLE_POINT % 2 == 1 )); then echo ${REMOTE_PLATFORM} > ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info cylc message -- "changing platform to ${REMOTE_PLATFORM}" else echo "localhost" > ${CYLC_WORKFLOW_RUN_DIR}/pretend-hall-info cylc message -- "changing platform to localhost" fi """ [[[environment]]] REMOTE_PLATFORM = {{ CYLC_TEST_PLATFORM }} cylc-flow-8.6.4/tests/functional/platforms/06-host-to-platform-upgrade-dummy-mode/0000775000175000017500000000000015202510242030262 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/06-host-to-platform-upgrade-dummy-mode/flow.cylc0000664000175000017500000000056115202510242032107 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True [scheduling] [[graph]] R1 = """ upgradeable_cylc7_settings """ [runtime] [[upgradeable_cylc7_settings]] script = echo "In Dummy Mode this shouldn't be written to job.out" [[[remote]]] host = localhost [[[job]]] batch system = background batch submit command template = timeout 10 cylc-flow-8.6.4/tests/functional/platforms/04-host-to-platform-upgrade-fail-inherit.t0000664000175000017500000000265215202510242030750 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Parent and child tasks are both valid, before inheritance calculated. # Child function not valid after inheritance. # Check for task failure at job-submit. . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config '' " # non-existent platform [platforms] [[_wibble]] " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Run the workflow workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach "${WORKFLOW_NAME}" grep_ok "Task 'non-valid-child' has the following deprecated '\[runtime\]' setting(s)" \ "${TEST_NAME_BASE}-run.stderr" purge exit cylc-flow-8.6.4/tests/functional/platforms/10-do-not-host-check-platforms.t0000775000175000017500000000326115202510242026763 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that platform names are not treated as host names. E.g. a platform # name starting with "localhost" should not be treated as localhost. # https://github.com/cylc/cylc-flow/issues/5342 . "$(dirname "$0")/test_header" set_test_number 2 # shellcheck disable=SC2016 create_test_global_config '' ' [platforms] [[localhost_spice]] hosts = unreachable ' make_rnd_workflow cat > "${RND_WORKFLOW_SOURCE}/flow.cylc" <<__HEREDOC__ [scheduler] [[events]] stall timeout = PT0S [scheduling] [[graph]] R1 = foo [runtime] [[foo]] platform = localhost_spice __HEREDOC__ ERR_STR='Unable to find valid host for localhost_spice' TEST_NAME="${TEST_NAME_BASE}-vip-workflow" run_fail "${TEST_NAME}" cylc vip "${RND_WORKFLOW_SOURCE}" --no-detach grep_ok "${ERR_STR}" \ "${TEST_NAME}.stderr" -F purge_rnd_workflow exit cylc-flow-8.6.4/tests/functional/platforms/05-host-to-platform-upgrade-fail/0000775000175000017500000000000015202510242027117 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/05-host-to-platform-upgrade-fail/flow.cylc0000664000175000017500000000103015202510242030734 0ustar alastairalastair[meta] description = """ The task settings for not_upgradable_cylc7_settings should not match any platform: The workflow should fail as a result. """ [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = """ not_upgradable_cylc7_settings """ [runtime] [[not_upgradable_cylc7_settings]] script = True [[[remote]]] host = parasite [[[job]]] batch system = 'loaf' cylc-flow-8.6.4/tests/functional/platforms/14-trigger-paused-subshell.t0000664000175000017500000000524415202510242026275 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Triggering a task with a subshell platform setting while the workflow is # paused should only evaluate the subshell once. # Here we have a subshell command that alternates between two platforms on each # call, to check the platform does not change during a manual trigger. # https://github.com/cylc/cylc-flow/issues/6994 export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 11 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" local_host_name=$(hostname) remote_host_name=$(cylc config -i "[platforms][${CYLC_TEST_PLATFORM}]hosts") remote_host_name=$(ssh -oStrictHostKeyChecking=no "$remote_host_name" hostname) workflow_log="${WORKFLOW_RUN_DIR}/log/scheduler/log" # shellcheck disable=SC2034 # LOG_SCAN_GREP_OPTS is not unused LOG_SCAN_GREP_OPTS="-E" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" cylc play "${WORKFLOW_NAME}" --pause poll_grep_workflow_log "1/foo:waiting" cylc trigger "${WORKFLOW_NAME}//1/foo" log_scan "log-grep-01" "$workflow_log" 10 2 \ "\[1/foo/01:preparing\] submitted to localhost" \ "\[1/foo/01:.*\] \(received\)${local_host_name}" \ "\[1/foo/01:.*\] => succeeded" cylc trigger "${WORKFLOW_NAME}//1/foo" log_scan "log-grep-02" "$workflow_log" 10 2 \ "\[1/foo/02:preparing\] submitted to ${CYLC_TEST_PLATFORM}" \ "\[1/foo/02:.*\] \((received|polled)\)${remote_host_name}" \ "\[1/foo/02:.*\] => succeeded" cylc trigger "${WORKFLOW_NAME}//1/foo" log_scan "log-grep-03" "$workflow_log" 10 2 \ "\[1/foo/03:preparing\] submitted to localhost" \ "\[1/foo/03:.*\] \(received\)${local_host_name}" \ "\[1/foo/03:.*\] => succeeded" cylc stop "${WORKFLOW_NAME}" --now --now # Check DB as well: sqlite3 "${WORKFLOW_RUN_DIR}/.service/db" \ "SELECT submit_num, platform_name FROM task_jobs" > task_jobs.out cmp_ok task_jobs.out <<__EOF__ 1|localhost 2|${CYLC_TEST_PLATFORM} 3|localhost __EOF__ poll_workflow_stopped purge cylc-flow-8.6.4/tests/functional/platforms/14-trigger-paused-subshell/0000775000175000017500000000000015202510242026103 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/14-trigger-paused-subshell/toggle_platform.sh0000775000175000017500000000237115202510242031632 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Script that outputs the current platform written in the hall file and changes # it to the other one. This ensures the platform subshell result will alternate # each time it is called. remote_platform=$1 hall_file="${CYLC_WORKFLOW_RUN_DIR}/pretend_hall_info" if [[ ! -f "${hall_file}" ]]; then current=localhost else current=$(cat "$hall_file") fi echo "$current" if [[ "$current" == localhost ]]; then echo "$remote_platform" > "$hall_file" else echo localhost > "$hall_file" fi cylc-flow-8.6.4/tests/functional/platforms/14-trigger-paused-subshell/flow.cylc0000664000175000017500000000061015202510242027723 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = foo:never # Allows triggering repeatedly without workflow shutting down [runtime] [[foo]] platform = $("${CYLC_WORKFLOW_RUN_DIR}/toggle_platform.sh" {{ CYLC_TEST_PLATFORM }}) # Check what host we have truly run on: script = cylc message -- "$(hostname)" [[[outputs]]] never = gonna_give_u_up cylc-flow-8.6.4/tests/functional/platforms/test_header0000777000175000017500000000000015202510242027435 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/07-localhost-set-if-not-in-globalcfg.t0000775000175000017500000000276115202510242030037 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that ``[platforms][localhost]`` is only set automatically if it # not set in ``global.cylc``. export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config "" " [platforms] [[localhost, nine_and_three_quarters]] hosts = localhost job runner = at " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Run the workflow workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok "Job runner: at" "${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job" purge exit cylc-flow-8.6.4/tests/functional/platforms/05-host-to-platform-upgrade-fail.t0000664000175000017500000000360215202510242027305 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that platform upgraders fail if no platform can be found which # matches host settings. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config '' " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Both of these cases should validate ok. run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" \ -s "CYLC_TEST_HOST='${CYLC_TEST_HOST}'" # Check that the cfgspec/workflow.py has issued a warning about upgrades. grep_ok "\[not_upgradable_cylc7_settings\]\[remote\]host = parasite"\ "${TEST_NAME_BASE}-validate.stderr" # Run the workflow workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach \ -s "CYLC_TEST_HOST='${CYLC_TEST_HOST}'" "${WORKFLOW_NAME}" # Check that the workflow failed because no matching platform could be found. grep_ok "\[jobs-submit err\] No platform found matching your task"\ "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge exit cylc-flow-8.6.4/tests/functional/platforms/08-warn-if-regex-matches-localhost.t0000775000175000017500000000362015202510242027621 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that ``[platforms][localhost]`` is only set automatically if it # not set in ``global.cylc``. . "$(dirname "$0")/test_header" set_test_number 4 # shellcheck disable=SC2016 create_test_global_config '' ' [platforms] [[localh...]] # This should not override `localh...` in this one case, because the # localhost default pins it to the top of the list. [[localhost]] [[[meta]]] foo = "foo" ' make_rnd_workflow cat > "${RND_WORKFLOW_SOURCE}/flow.cylc" <<__HEREDOC__ [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo __HEREDOC__ ERR_STR='cannot be defined using a regular expression' TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate "${RND_WORKFLOW_SOURCE}" grep_ok "${ERR_STR}" \ "${TEST_NAME}.stderr" -F TEST_NAME="${TEST_NAME_BASE}-cylc-install" run_fail "${TEST_NAME}" cylc install \ "${RND_WORKFLOW_SOURCE}" \ --workflow-name "${RND_WORKFLOW_NAME}" grep_ok "${ERR_STR}" \ "${TEST_NAME}.stderr" -F purge_rnd_workflow exit cylc-flow-8.6.4/tests/functional/platforms/09-default-directives.t0000775000175000017500000000426715202510242025332 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Platforms can have default directives and these are overridden as expected. export REQUIRE_PLATFORM="runner:slurm" . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config "" " [platforms] [[no_default, default_only, overridden, neither]] hosts = localhost job runner = slurm [[default_only, overridden]] [[[directives]]] --wurble=foo " init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONF__' [scheduler] [[events]] stall timeout = PT0S [scheduling] [[graph]] R1 = lewis & morse & barnaby & cadfael [runtime] [[lewis]] platform = no_default [[[directives]]] --wurble=bar [[morse]] platform = default_only [[barnaby]] platform = overridden [[[directives]]] --wurble=qux [[cadfael]] platform = neither __FLOW_CONF__ run_fail "${TEST_NAME_BASE}-play" \ cylc play "${WORKFLOW_NAME}" --no-detach LOG_DIR="${RUN_DIR}/${WORKFLOW_NAME}/log/job/1/" named_grep_ok "${TEST_NAME_BASE}-no-default" "--wurble=bar" "${LOG_DIR}/lewis/NN/job" named_grep_ok "${TEST_NAME_BASE}-default-only" "--wurble=foo" "${LOG_DIR}/morse/NN/job" named_grep_ok "${TEST_NAME_BASE}-overridden" "--wurble=qux" "${LOG_DIR}/barnaby/NN/job" grep_fail "--wurble" "${LOG_DIR}/cadfael/NN/job" purge exit 0 cylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade/0000775000175000017500000000000015202510242026203 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade/bad/0000775000175000017500000000000015202510242026731 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade/bad/flow.cylc0000664000175000017500000000053115202510242030553 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] [[graph]] R1 = """ just_wrong """ [runtime] [[root]] script = true [[just_wrong]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[remote]]] host = 'parasite' [[[job]]] batch system = 'loaf' cylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade/reference.log0000664000175000017500000000045215202510242030645 0ustar alastairalastairInitial point: 1 Final point: 1 1/fin -triggered off ['1/no_settings', '1/t1', '1/t2'] 2/fin -triggered off ['2/no_settings', '2/t1', '2/t2'] 1/no_settings -triggered off [] 2/no_settings -triggered off [] 1/t1 -triggered off [] 2/t1 -triggered off [] 1/t2 -triggered off [] 2/t2 -triggered off [] cylc-flow-8.6.4/tests/functional/platforms/02-host-to-platform-upgrade/flow.cylc0000664000175000017500000000130515202510242030025 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 # Note: we check the upgrade persists over > 1 cycle # https://github.com/cylc/cylc-flow/issues/4167 [[graph]] P1 = """ no_settings & t1 & t2 => fin """ [runtime] [[root]] script = true [[no_settings, fin]] [[t1]] script = test {{CYLC_TEST_HOST_FQDN}} == "$(hostname -f)" [[[remote]]] host = {{CYLC_TEST_HOST}} [[T2]] script = test {{CYLC_TEST_HOST_FQDN}} == "$(hostname -f)" [[[remote]]] host = {{CYLC_TEST_HOST}} [[t2]] inherit = T2 cylc-flow-8.6.4/tests/functional/platforms/06-host-to-platform-upgrade-dummy-mode.t0000664000175000017500000000274215202510242030454 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that setting the platform to localhost for dummy mode doesn't # cause conflicts with Cylc 7 settings # TODO Remove test at Cylc 8.x. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Ensure that you can validate workflow run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --no-detach --mode=dummy # Check that the upgradeable config has been run on a sensible host. grep_ok \ "(dummy job succeed)"\ "${WORKFLOW_RUN_DIR}/log/job/1/upgradeable_cylc7_settings/NN/job.out" purge exit cylc-flow-8.6.4/tests/functional/inheritance/0000775000175000017500000000000015202510242021402 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/inheritance/00-namespace-list.t0000775000175000017500000000374215202510242024722 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that members of namespace lists [[n1,n2,...]] are inserted into the # [runtime] ordered dict in the correct order. If just appended, they break # repeat-section override for the member. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-config cylc config -i runtime "${WORKFLOW_NAME}" > runtime.out cmp_ok runtime.out <<'__DONE__' [[root]] [[FAMILY]] [[m1]] inherit = FAMILY completion = succeeded [[[environment]]] FOO = foo [[m2]] inherit = FAMILY completion = succeeded [[[environment]]] FOO = bar [[m3]] inherit = FAMILY completion = succeeded [[[environment]]] FOO = foo __DONE__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/inheritance/02-bad-reinherit/0000775000175000017500000000000015202510242024336 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/inheritance/02-bad-reinherit/flow.cylc0000664000175000017500000000040415202510242026157 0ustar alastairalastair[meta] title = a workflow with a task that multiply inherits from the same family description = should fail validation [scheduling] [[graph]] R1 = foo [runtime] [[A]] [[B]] inherit = C, A [[C]] [[foo]] inherit = A, B cylc-flow-8.6.4/tests/functional/inheritance/02-bad-reinherit.t0000664000175000017500000000335315202510242024527 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check bad multi inheritance fails validation with the correct error message. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-cmp" cmp_ok "${TEST_NAME_BASE}-validate.stderr" <<__ERR__ WorkflowConfigError: ERROR: foo: bad runtime namespace inheritance hierarchy. See the cylc documentation on multiple inheritance. __ERR__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/inheritance/00-namespace-list/0000775000175000017500000000000015202510242024524 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/inheritance/00-namespace-list/flow.cylc0000664000175000017500000000031415202510242026345 0ustar alastairalastair[scheduling] [[graph]] R1 = FAMILY [runtime] [[FAMILY]] [[m1,m2,m3]] inherit = FAMILY [[[environment]]] FOO = foo [[m2]] [[[environment]]] FOO = bar cylc-flow-8.6.4/tests/functional/inheritance/01-circular/0000775000175000017500000000000015202510242023424 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/inheritance/01-circular/flow.cylc0000664000175000017500000000040415202510242025245 0ustar alastairalastair[meta] title = a workflow with circular inheritance description = should fail validation [scheduling] [[graph]] R1 = foo [runtime] [[A]] inherit = B [[B]] inherit = C [[C]] inherit = B [[foo]] inherit = A cylc-flow-8.6.4/tests/functional/inheritance/01-circular.t0000664000175000017500000000323515202510242023614 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check circular inheritance fails validation with the correct error message. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-cmp" cmp_ok "${TEST_NAME_BASE}-validate.stderr" <<__ERR__ WorkflowConfigError: circular [runtime] inheritance? __ERR__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/inheritance/test_header0000777000175000017500000000000015202510242027717 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-clean/0000775000175000017500000000000015202510242021123 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-clean/01-remote.t0000664000175000017500000001260415202510242023024 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test that cylc clean succesfully removes the workflow on remote host export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" SSH_CMD="$(cylc config -d -i "[platforms][${CYLC_TEST_PLATFORM}]ssh command") ${CYLC_TEST_HOST}" if ! $SSH_CMD command -v 'tree' > '/dev/null'; then skip_all "'tree' command not available on remote host ${CYLC_TEST_HOST}" fi set_test_number 10 # Generate random name for symlink dirs to avoid any clashes with other tests SYM_NAME="$(mktemp -u)" SYM_NAME="${SYM_NAME##*tmp.}" create_test_global_config "" " [install] [[symlink dirs]] [[[${CYLC_TEST_INSTALL_TARGET}]]] run = ${TEST_DIR}/${SYM_NAME}/run log = ${TEST_DIR}/${SYM_NAME}/other log/job = ${TEST_DIR}/${SYM_NAME}/job share = ${TEST_DIR}/${SYM_NAME}/other share/cycle = ${TEST_DIR}/${SYM_NAME}/cycle work = ${TEST_DIR}/${SYM_NAME}/other " init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = santa [runtime] [[root]] platform = ${CYLC_TEST_PLATFORM} script = touch flow.cylc # testing that remote clean does not scan for workflows __FLOW__ FUNCTIONAL_DIR="${TEST_SOURCE_DIR_BASE%/*}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "$WORKFLOW_NAME" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "$WORKFLOW_NAME" --no-detach # Create a fake sibling workflow dir: $SSH_CMD mkdir "${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${CYLC_TEST_REG_BASE}/leave-me-alone" # ----------------------------------------------------------------------------- TEST_NAME="run-dir-readlink-pre-clean.remote" $SSH_CMD readlink "\$HOME/cylc-run/${WORKFLOW_NAME}" > "${TEST_NAME}.stdout" cmp_ok "${TEST_NAME}.stdout" <<< "${TEST_DIR}/${SYM_NAME}/run/cylc-run/${WORKFLOW_NAME}" TEST_NAME="test-dir-tree-pre-clean.remote" # shellcheck disable=SC2086 run_ok "${TEST_NAME}" $SSH_CMD tree -L 8 --noreport --charset=ascii \ "${TEST_DIR}/${SYM_NAME}/"'*'"/cylc-run/${CYLC_TEST_REG_BASE}" # Note: backticks need to be escaped in the heredoc cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${CYLC_TEST_REG_BASE} |-- ${FUNCTIONAL_DIR} | \`-- cylc-clean | \`-- ${TEST_NAME_BASE} | \`-- share | \`-- cycle | \`-- 1 \`-- leave-me-alone ${TEST_DIR}/${SYM_NAME}/job/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- log \`-- job \`-- 1 \`-- santa |-- 01 \`-- NN -> 01 ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- log | \`-- job -> ${TEST_DIR}/${SYM_NAME}/job/cylc-run/${WORKFLOW_NAME}/log/job |-- share | \`-- cycle -> ${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${WORKFLOW_NAME}/share/cycle \`-- work \`-- 1 \`-- santa \`-- flow.cylc ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- log -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/log |-- share -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/share \`-- work -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/work __TREE__ # ----------------------------------------------------------------------------- TEST_NAME="cylc-clean-ok" run_ok "$TEST_NAME" cylc clean "$WORKFLOW_NAME" --timeout PT2M # (timeout opt is covered by unit tests but no harm double-checking here) dump_std "$TEST_NAME" TEST_NAME="run-dir-not-exist-post-clean.local" # (Could use the function `exists_ok` here instead, but this keeps it consistent with the remote test below) if [[ ! -e "$WORKFLOW_RUN_DIR" ]]; then ok "$TEST_NAME" else fail "$TEST_NAME" fi TEST_NAME="run-dir-not-exist-post-clean.remote" if $SSH_CMD [[ ! -a "\$HOME/cylc-run/${WORKFLOW_NAME}" ]]; then ok "$TEST_NAME" else fail "$TEST_NAME" fi TEST_NAME="test-dir-tree-post-clean.remote" # shellcheck disable=SC2086 run_ok "${TEST_NAME}" $SSH_CMD tree --noreport --charset=ascii \ "${TEST_DIR}/${SYM_NAME}/"'*'"/cylc-run/${CYLC_TEST_REG_BASE}" # Note: backticks need to be escaped in the heredoc cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${CYLC_TEST_REG_BASE} \`-- leave-me-alone __TREE__ purge cylc-flow-8.6.4/tests/functional/cylc-clean/06-nfs.t0000664000175000017500000000531115202510242022321 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # This test covers the interaction between "cylc clean" and "cylc cat-log -m t" # on the NFS filesystem. The "cat-log" should not block the "clean". # # Tests: https://github.com/cylc/cylc-flow/pull/5359 # # If you try to delete a file that is stored on NFS, which is open for reading # by another process (e.g. `tail -f`), NFS will remove the file, but put a # ".nfs" file in its place. This ".nfs" file will cause "rm" operations on # the directory containing the NFS files to fail with one of two errors: # * https://docs.python.org/3/library/errno.html#errno.EBUSY # * https://docs.python.org/3/library/errno.html#errno.ENOTEMPTY # # To prevent "cylc cat-log -m t" which calls "tail -f" from blocking # "cylc clean" commands, we retry the "rm" operation with a delay. This # allows the "tail -f" to fail and release its file lock allowing the # "rm" to pass on a subsequent attempt. . "$(dirname "$0")/test_header" set_test_number 2 # install a blank source workflow init_workflow "${TEST_NAME_BASE}" <<< '# blank workflow' # add a scheduler log file with something written to it WORKFLOW_LOG_DIR="${WORKFLOW_RUN_DIR}/log/scheduler" mkdir -p "$WORKFLOW_LOG_DIR" LOG_STUFF='foo bar baz' echo "${LOG_STUFF}" > "${WORKFLOW_LOG_DIR}/01-start-01.log" # start cat-log running - this runs "tail -f" cylc cat-log -m t "$WORKFLOW_NAME" > out 2>err & PID="$!" # wait for tail to start poll pgrep -P "$PID" tail # try to clean the workflow run_ok "${TEST_NAME_BASE}-clean" cylc clean -y "${WORKFLOW_NAME}" # the tail command should have detected that the file isn't there any more # and exited -> cat-log should have exited poll_pid_done "$PID" # ensure the log dir was removed correctly # run_ok "${TEST_NAME_BASE}-dir-removed" [[ ! -d "${WORKFLOW_LOG_DIR}" ]] TEST_NAME="${TEST_NAME_BASE}-log-dir-removed" if [[ -d "${WORKFLOW_LOG_DIR}" ]]; then fail "${TEST_NAME}" else ok "${TEST_NAME}" fi purge cylc-flow-8.6.4/tests/functional/cylc-clean/05-old-remote-contact.t0000664000175000017500000000374415202510242025242 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test that cylc clean succesfully removes the workflow on remote host even # when there is a leftover contact file with an unreachable host recorded in it export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" set_test_number 3 SSH_CMD="$(cylc config -d -i "[platforms][${CYLC_TEST_PLATFORM}]ssh command") ${CYLC_TEST_HOST}" init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = dilophosaurus [runtime] [[dilophosaurus]] platform = ${CYLC_TEST_PLATFORM} __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "$WORKFLOW_NAME" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "$WORKFLOW_NAME" --no-detach # Create a fake old contact file on the remote host # shellcheck disable=SC2259 echo | $SSH_CMD "cat > \$HOME/cylc-run/${WORKFLOW_NAME}/.service/contact" << EOF CYLC_API=5 CYLC_VERSION=8.0.0 CYLC_WORKFLOW_COMMAND=echo Hello John CYLC_WORKFLOW_HOST=unreachable.isla_nublar.ingen CYLC_WORKFLOW_ID=${WORKFLOW_NAME} CYLC_WORKFLOW_PID=99999 CYLC_WORKFLOW_PORT=00000 EOF TEST_NAME="cylc-clean" run_ok "$TEST_NAME" cylc clean --remote "$WORKFLOW_NAME" dump_std "$TEST_NAME" purge cylc-flow-8.6.4/tests/functional/cylc-clean/04-runN.t0000664000175000017500000000233215202510242022453 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test cleaning latest run using /runN . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "$TEST_NAME_BASE" basic-workflow true exists_ok "${WORKFLOW_RUN_DIR}/run1" exists_ok "${WORKFLOW_RUN_DIR}/runN" run_ok "${TEST_NAME_BASE}-clean" cylc clean "${WORKFLOW_NAME}/runN" exists_fail "${WORKFLOW_RUN_DIR}/run1" exists_fail "${WORKFLOW_RUN_DIR}/runN" purge cylc-flow-8.6.4/tests/functional/cylc-clean/test_header0000777000175000017500000000000015202510242027440 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-clean/02-targeted.t0000664000175000017500000001464315202510242023336 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test the cylc clean command . "$(dirname "$0")/test_header" if ! command -v 'tree' >'/dev/null'; then skip_all '"tree" command not available' fi set_test_number 16 # Generate random name for symlink dirs to avoid any clashes with other tests SYM_NAME="$(mktemp -u)" SYM_NAME="sym-${SYM_NAME##*tmp.}" create_test_global_config "" " [install] [[symlink dirs]] [[[localhost]]] run = ${TEST_DIR}/${SYM_NAME}/run log = ${TEST_DIR}/${SYM_NAME}/other share = ${TEST_DIR}/${SYM_NAME}/other work = ${TEST_DIR}/${SYM_NAME}/other # Need to override any symlink dirs set in global.cylc: share/cycle = " install_workflow "${TEST_NAME_BASE}" basic-workflow # Also create some other file touch "${WORKFLOW_RUN_DIR}/darmok.cylc" run_ok "${TEST_NAME_BASE}-val" cylc validate "$WORKFLOW_NAME" FUNCTIONAL_DIR="${TEST_SOURCE_DIR_BASE%/*}" # ----------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run-dir-readlink-pre-clean" readlink "$WORKFLOW_RUN_DIR" > "${TEST_NAME}.stdout" cmp_ok "${TEST_NAME}.stdout" <<< "${TEST_DIR}/${SYM_NAME}/run/cylc-run/${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-testdir-tree-pre-clean" run_ok "${TEST_NAME}" tree -L 5 --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" # Note: backticks need to be escaped in the heredoc cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- log | \`-- install |-- share \`-- work ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- _cylc-install | \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} |-- darmok.cylc |-- flow.cylc |-- log -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/log |-- share -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/share \`-- work -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/work __TREE__ # ----------------------------------------------------------------------------- # Clean the log dir only run_ok "${TEST_NAME_BASE}-targeted-clean-1" cylc clean "$WORKFLOW_NAME" \ --rm log TEST_NAME="${TEST_NAME_BASE}-testdir-tree-1" run_ok "${TEST_NAME}" tree -L 5 --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- share \`-- work ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- _cylc-install | \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} |-- darmok.cylc |-- flow.cylc |-- share -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/share \`-- work -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/work __TREE__ # ----------------------------------------------------------------------------- # Clean using a glob run_ok "${TEST_NAME_BASE}-targeted-clean-2" cylc clean "$WORKFLOW_NAME" \ --rm 'wo*' TEST_NAME="${TEST_NAME_BASE}-testdir-tree-2" run_ok "${TEST_NAME}" tree -L 5 --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" # Note: when using glob, the symlink dir target is not deleted cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- share ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- _cylc-install | \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} |-- darmok.cylc |-- flow.cylc \`-- share -> ${TEST_DIR}/${SYM_NAME}/other/cylc-run/${WORKFLOW_NAME}/share __TREE__ # ----------------------------------------------------------------------------- # Clean the last remaining symlink dir run_ok "${TEST_NAME_BASE}-targeted-clean-3" cylc clean "$WORKFLOW_NAME" \ --rm 'share' TEST_NAME="${TEST_NAME_BASE}-testdir-tree-3" run_ok "${TEST_NAME}" tree -L 5 --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- _cylc-install | \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} |-- darmok.cylc \`-- flow.cylc __TREE__ # ----------------------------------------------------------------------------- # Clean multiple things run_ok "${TEST_NAME_BASE}-targeted-clean-3" cylc clean "$WORKFLOW_NAME" \ --rm 'flow.cylc' --rm 'darmok.cylc' TEST_NAME="${TEST_NAME_BASE}-testdir-tree-3" run_ok "${TEST_NAME}" tree -L 5 --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- _cylc-install \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} __TREE__ # ----------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-clean/03-multi.t0000664000175000017500000000757715202510242022702 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test cleaning multiple run dirs . "$(dirname "$0")/test_header" if ! command -v 'tree' > /dev/null; then skip_all '"tree" command not available' fi set_test_number 18 RND_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" WORKFLOW_NAME="${RND_NAME}/cylc-clean" WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" create_workflow() { mkdir -p "${TEST_DIR}/${WORKFLOW_NAME}" # make source dir touch "${TEST_DIR}/${WORKFLOW_NAME}/flow.cylc" cylc install "${TEST_DIR}/${WORKFLOW_NAME}" --workflow-name="${WORKFLOW_NAME}" } # ----------------------------------------------------------------------------- for _ in 1 2; do create_workflow done TEST_NAME="tree-pre-clean-1" run_ok "${TEST_NAME}" tree --noreport --charset=ascii -L 1 "${HOME}/cylc-run/${WORKFLOW_NAME}" # Note: backticks need to be escaped in the heredoc cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${HOME}/cylc-run/${WORKFLOW_NAME} |-- _cylc-install |-- run1 |-- run2 \`-- runN -> run2 __TREE__ # Test trying to clean multiple run dirs without --yes fails: run_fail "${TEST_NAME_BASE}-no" cylc clean "$WORKFLOW_NAME" exists_ok "${WORKFLOW_RUN_DIR}/run1" exists_ok "${WORKFLOW_RUN_DIR}/run2" # Should work with --yes (removes top level dir too): run_ok "${TEST_NAME_BASE}-yes" cylc clean -y "$WORKFLOW_NAME" exists_fail "${RUN_DIR}/${RND_NAME}" # ----------------------------------------------------------------------------- # Should continue cleaning a list of workflows even if one fails. for _ in 1 2; do create_workflow done TEST_NAME="tree-pre-clean-2" run_ok "${TEST_NAME}" tree --noreport --charset=ascii -L 1 "${HOME}/cylc-run/${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${HOME}/cylc-run/${WORKFLOW_NAME} |-- _cylc-install |-- run1 |-- run2 \`-- runN -> run2 __TREE__ mkdir "${WORKFLOW_RUN_DIR}/run1/.service" echo 'x' > "${WORKFLOW_RUN_DIR}/run1/.service/db" # corrupted db! TEST_NAME="${TEST_NAME_BASE}-yes-no" run_fail "${TEST_NAME}" \ cylc clean -y "$WORKFLOW_NAME/run1" "$WORKFLOW_NAME/run2" grep_ok "file is not a database" "${TEST_NAME}.stderr" -e TEST_NAME="tree-post-clean-2" run_ok "${TEST_NAME}" tree --noreport --charset=ascii -L 1 "${HOME}/cylc-run/${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${HOME}/cylc-run/${WORKFLOW_NAME} |-- _cylc-install \`-- run1 __TREE__ purge "$WORKFLOW_NAME" # ----------------------------------------------------------------------------- # Should not clean top level dir if not empty. create_workflow touch "${WORKFLOW_RUN_DIR}/jellyfish.txt" TEST_NAME="tree-pre-clean-3" run_ok "${TEST_NAME}" tree --noreport --charset=ascii -L 1 "${HOME}/cylc-run/${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${HOME}/cylc-run/${WORKFLOW_NAME} |-- _cylc-install |-- jellyfish.txt |-- run1 \`-- runN -> run1 __TREE__ run_ok "${TEST_NAME}" cylc clean -y "$WORKFLOW_NAME" TEST_NAME="tree-post-clean-3" run_ok "${TEST_NAME}" tree --noreport --charset=ascii -L 1 "${HOME}/cylc-run/${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${HOME}/cylc-run/${WORKFLOW_NAME} |-- _cylc-install \`-- jellyfish.txt __TREE__ purge cylc-flow-8.6.4/tests/functional/cylc-clean/basic-workflow/0000775000175000017500000000000015202510242024054 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-clean/basic-workflow/flow.cylc0000664000175000017500000000013315202510242025674 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = darmok cylc-flow-8.6.4/tests/functional/cylc-clean/00-basic.t0000664000175000017500000001227515202510242022615 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test the cylc clean command . "$(dirname "$0")/test_header" if ! command -v 'tree' >'/dev/null'; then skip_all '"tree" command not available' fi set_test_number 12 # Generate random name for symlink dirs to avoid any clashes with other tests SYM_NAME="$(mktemp -u)" SYM_NAME="${SYM_NAME##*tmp.}" create_test_global_config "" " [install] [[symlink dirs]] [[[localhost]]] run = ${TEST_DIR}/${SYM_NAME}/run log = ${TEST_DIR}/${SYM_NAME}/log log/job = ${TEST_DIR}/${SYM_NAME}/job share = ${TEST_DIR}/${SYM_NAME}/share share/cycle = ${TEST_DIR}/${SYM_NAME}/cycle work = ${TEST_DIR}/${SYM_NAME}/work " install_workflow "${TEST_NAME_BASE}" basic-workflow run_ok "${TEST_NAME_BASE}-val" cylc validate "$WORKFLOW_NAME" # Create a fake sibling workflow dir in the ${SYM_NAME}/log dir: mkdir "${TEST_DIR}/${SYM_NAME}/log/cylc-run/${CYLC_TEST_REG_BASE}/leave-me-alone" FUNCTIONAL_DIR="${TEST_SOURCE_DIR_BASE%/*}" # ----------------------------------------------------------------------------- TEST_NAME="run-dir-readlink-pre-clean" readlink "$WORKFLOW_RUN_DIR" > "${TEST_NAME}.stdout" cmp_ok "${TEST_NAME}.stdout" <<< "${TEST_DIR}/${SYM_NAME}/run/cylc-run/${WORKFLOW_NAME}" INSTALL_LOG_FILE=$(ls "${TEST_DIR}/${SYM_NAME}/log/cylc-run/${WORKFLOW_NAME}/log/install") TEST_NAME="test-dir-tree-pre-clean" run_ok "${TEST_NAME}" tree --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" # Note: backticks need to be escaped in the heredoc cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- share \`-- cycle ${TEST_DIR}/${SYM_NAME}/job/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- log \`-- job ${TEST_DIR}/${SYM_NAME}/log/cylc-run/${CYLC_TEST_REG_BASE} |-- ${FUNCTIONAL_DIR} | \`-- cylc-clean | \`-- ${TEST_NAME_BASE} | \`-- log | |-- install | | \`-- ${INSTALL_LOG_FILE} | \`-- job -> ${TEST_DIR}/${SYM_NAME}/job/cylc-run/${WORKFLOW_NAME}/log/job \`-- leave-me-alone ${TEST_DIR}/${SYM_NAME}/run/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} |-- _cylc-install | \`-- source -> ${TEST_DIR}/${WORKFLOW_NAME} |-- flow.cylc |-- log -> ${TEST_DIR}/${SYM_NAME}/log/cylc-run/${WORKFLOW_NAME}/log |-- share -> ${TEST_DIR}/${SYM_NAME}/share/cylc-run/${WORKFLOW_NAME}/share \`-- work -> ${TEST_DIR}/${SYM_NAME}/work/cylc-run/${WORKFLOW_NAME}/work ${TEST_DIR}/${SYM_NAME}/share/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- share \`-- cycle -> ${TEST_DIR}/${SYM_NAME}/cycle/cylc-run/${WORKFLOW_NAME}/share/cycle ${TEST_DIR}/${SYM_NAME}/work/cylc-run/${CYLC_TEST_REG_BASE} \`-- ${FUNCTIONAL_DIR} \`-- cylc-clean \`-- ${TEST_NAME_BASE} \`-- work __TREE__ # ----------------------------------------------------------------------------- TEST_NAME="clean" run_ok "$TEST_NAME" cylc clean "$WORKFLOW_NAME" dump_std "$TEST_NAME" # ----------------------------------------------------------------------------- TEST_NAME="run-dir-not-exist-post-clean" exists_fail "$WORKFLOW_RUN_DIR" TEST_NAME="test-dir-tree-post-clean" run_ok "${TEST_NAME}" tree --noreport --charset=ascii "${TEST_DIR}/${SYM_NAME}/"*"/cylc-run/${CYLC_TEST_REG_BASE}" cmp_ok "${TEST_NAME}.stdout" << __TREE__ ${TEST_DIR}/${SYM_NAME}/log/cylc-run/${CYLC_TEST_REG_BASE} \`-- leave-me-alone __TREE__ # ----------------------------------------------------------------------------- TEST_NAME="clean-non-exist" run_ok "$TEST_NAME" cylc clean "$WORKFLOW_NAME" dump_std "$TEST_NAME" cmp_ok "${TEST_NAME}.stdout" << __EOF__ INFO - No directory to clean at ${WORKFLOW_RUN_DIR} __EOF__ # ----------------------------------------------------------------------------- TEST_NAME="clean-non-exist-pattern" run_ok "$TEST_NAME" cylc clean "nope*" dump_std "$TEST_NAME" cmp_ok "${TEST_NAME}.stderr" << __EOF__ WARNING - No stopped workflows matching nope* __EOF__ # ----------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/0000775000175000017500000000000015202510242020555 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/17-yearly.t0000775000175000017500000000201715202510242022477 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="2005023T12" # shellcheck disable=SC2034 FINALCP="2008001T11" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/daily_final/0000775000175000017500000000000015202510242023030 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/daily_final/graph.plain.ref0000664000175000017500000000020415202510242025725 0ustar alastairalastairgraph node "20140101T0000Z/daily_foo" "daily_foo\n20140101T0000Z" node "20140102T0000Z/daily_foo" "daily_foo\n20140102T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/daily_final/reference.log0000664000175000017500000000022015202510242025463 0ustar alastairalastairInitial point: 20140101T0000Z Final point: 20140102T1200Z 20140101T0000Z/daily_foo -triggered off [] 20140102T0000Z/daily_foo -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/daily_final/flow.cylc0000664000175000017500000000032115202510242024647 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R//T00 = "daily_foo" [runtime] [[daily_foo]] script = true cylc-flow-8.6.4/tests/functional/cyclers/09-offset_initial.t0000775000175000017500000000201615202510242024171 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/common0000775000175000017500000000354715202510242022004 0ustar alastairalastair#!/usr/bin/env bash # 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 . . "$(dirname "$0")/test_header" if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then set_test_number 4 else set_test_number 3 fi CHOSEN_WORKFLOW="$(basename "$0" | sed "s/^.*-\(.*\)\.t/\1/g")" install_workflow "${TEST_NAME_BASE}" "${CHOSEN_WORKFLOW}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" \ -- "${INITIALCP}" "${FINALCP}" cmp_ok "${WORKFLOW_NAME}.graph.plain" "$TEST_SOURCE_DIR/$CHOSEN_WORKFLOW/graph.plain.ref" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then TEST_NAME="${TEST_NAME_BASE}-find" WORKFLOW_RUN_DIR="$RUN_DIR/${WORKFLOW_NAME}" { (cd "${WORKFLOW_RUN_DIR}" && find 'log/job' -type f) (cd "${WORKFLOW_RUN_DIR}" && find 'work' -type f) } | sort -V >"${TEST_NAME}" cmp_ok "${TEST_NAME}" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}-find.out" fi purge cylc-flow-8.6.4/tests/functional/cyclers/00-daily-find.out0000664000175000017500000000312415202510242023543 0ustar alastairalastairlog/job/20131231T2300Z/bar/01/job log/job/20131231T2300Z/bar/01/job.err log/job/20131231T2300Z/bar/01/job.out log/job/20131231T2300Z/bar/01/job.status log/job/20131231T2300Z/bar/01/job.xtrace log/job/20131231T2300Z/bar/01/job-activity.log log/job/20131231T2300Z/foo/01/job log/job/20131231T2300Z/foo/01/job.err log/job/20131231T2300Z/foo/01/job.out log/job/20131231T2300Z/foo/01/job.status log/job/20131231T2300Z/foo/01/job.xtrace log/job/20131231T2300Z/foo/01/job-activity.log log/job/20140101T2300Z/bar/01/job log/job/20140101T2300Z/bar/01/job.err log/job/20140101T2300Z/bar/01/job.out log/job/20140101T2300Z/bar/01/job.status log/job/20140101T2300Z/bar/01/job.xtrace log/job/20140101T2300Z/bar/01/job-activity.log log/job/20140101T2300Z/foo/01/job log/job/20140101T2300Z/foo/01/job.err log/job/20140101T2300Z/foo/01/job.out log/job/20140101T2300Z/foo/01/job.status log/job/20140101T2300Z/foo/01/job.xtrace log/job/20140101T2300Z/foo/01/job-activity.log log/job/20140102T2300Z/bar/01/job log/job/20140102T2300Z/bar/01/job.err log/job/20140102T2300Z/bar/01/job.out log/job/20140102T2300Z/bar/01/job.status log/job/20140102T2300Z/bar/01/job.xtrace log/job/20140102T2300Z/bar/01/job-activity.log log/job/20140102T2300Z/foo/01/job log/job/20140102T2300Z/foo/01/job.err log/job/20140102T2300Z/foo/01/job.out log/job/20140102T2300Z/foo/01/job.status log/job/20140102T2300Z/foo/01/job.xtrace log/job/20140102T2300Z/foo/01/job-activity.log work/20131231T2300Z/bar/typing work/20131231T2300Z/foo/typing work/20140101T2300Z/bar/typing work/20140101T2300Z/foo/typing work/20140102T2300Z/bar/typing work/20140102T2300Z/foo/typing cylc-flow-8.6.4/tests/functional/cyclers/06-multiweekly.t0000775000175000017500000000201315202510242023537 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="1000W011" # shellcheck disable=SC2034 FINALCP="1000W064" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/35-day_of_week.t0000775000175000017500000000201515202510242023444 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20100101T0000Z" # shellcheck disable=SC2034 FINALCP="+P3W" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/48-icp-cutoff.t0000775000175000017500000000255615202510242023245 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test ICP cutoff bug. . "$(dirname "$0")/test_header" set_test_number 1 init_workflow "${TEST_NAME_BASE}" <<'__WORKFLOW__' [scheduler] cycle point time zone = Z [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 20171101T0000Z [[graph]] R1 = foo R1/T00 = foo[^] => qux R1/T06 = foo[^] => qux [runtime] [[foo, qux]] script = true __WORKFLOW__ run_ok "${TEST_NAME_BASE}" cylc play "${WORKFLOW_NAME}" --no-detach purge exit cylc-flow-8.6.4/tests/functional/cyclers/r1_restricted/0000775000175000017500000000000015202510242023327 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_restricted/graph.plain.ref0000664000175000017500000000134615202510242026234 0ustar alastairalastairedge "20130808T0000Z/foo" "20130808T0600Z/foo" edge "20130808T0000Z/setup_foo" "20130808T0000Z/foo" edge "20130808T0600Z/foo" "20130808T0600Z/bar" edge "20130808T0600Z/foo" "20130808T1200Z/foo" edge "20130808T1200Z/foo" "20130808T1200Z/bar" edge "20130808T1200Z/foo" "20130808T1800Z/foo" edge "20130808T1800Z/foo" "20130808T1800Z/bar" graph node "20130808T0000Z/foo" "foo\n20130808T0000Z" node "20130808T0000Z/setup_foo" "setup_foo\n20130808T0000Z" node "20130808T0600Z/bar" "bar\n20130808T0600Z" node "20130808T0600Z/foo" "foo\n20130808T0600Z" node "20130808T1200Z/bar" "bar\n20130808T1200Z" node "20130808T1200Z/foo" "foo\n20130808T1200Z" node "20130808T1800Z/bar" "bar\n20130808T1800Z" node "20130808T1800Z/foo" "foo\n20130808T1800Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_restricted/reference.log0000664000175000017500000000077215202510242025776 0ustar alastairalastairInitial point: 20130808T0000Z Final point: 20130808T1800Z 20130808T0000Z/setup_foo -triggered off [] 20130808T0000Z/foo -triggered off ['20130808T0000Z/setup_foo'] 20130808T0600Z/foo -triggered off ['20130808T0000Z/foo'] 20130808T1200Z/foo -triggered off ['20130808T0600Z/foo'] 20130808T0600Z/bar -triggered off ['20130808T0600Z/foo'] 20130808T1200Z/bar -triggered off ['20130808T1200Z/foo'] 20130808T1800Z/foo -triggered off ['20130808T1200Z/foo'] 20130808T1800Z/bar -triggered off ['20130808T1800Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/r1_restricted/flow.cylc0000664000175000017500000000044215202510242025152 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20130808T00 final cycle point = 20130808T18 [[graph]] R1 = "setup_foo => foo" +PT6H/PT6H = "foo[-PT6H] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/20-multidaily_local.t0000775000175000017500000000422515202510242024516 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies, local time. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- CHOSEN_WORKFLOW="$(basename "$0" | sed "s/^.*-\(.*\)\.t/\1/g")" install_workflow "${TEST_NAME_BASE}" "${CHOSEN_WORKFLOW}" CURRENT_TZ_UTC_OFFSET="Z" sed -i "s/Z/$CURRENT_TZ_UTC_OFFSET/g" "${WORKFLOW_RUN_DIR}/reference.log" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" \ 20001231T0100 20010114 sed "s/Z/$CURRENT_TZ_UTC_OFFSET/g" \ "$TEST_SOURCE_DIR/$CHOSEN_WORKFLOW/graph.plain.ref" > 'graph.plain.local.ref' cmp_ok "${WORKFLOW_NAME}.graph.plain" 'graph.plain.local.ref' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/offset_initial/0000775000175000017500000000000015202510242023554 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/offset_initial/graph.plain.ref0000664000175000017500000000067015202510242026460 0ustar alastairalastairedge "20140102T0000Z/bar" "20140102T0000Z/baz" edge "20140102T0600Z/bar" "20140102T0600Z/baz" edge "20140102T1200Z/bar" "20140102T1200Z/baz" graph node "20140102T0000Z/bar" "bar\n20140102T0000Z" node "20140102T0000Z/baz" "baz\n20140102T0000Z" node "20140102T0600Z/bar" "bar\n20140102T0600Z" node "20140102T0600Z/baz" "baz\n20140102T0600Z" node "20140102T1200Z/bar" "bar\n20140102T1200Z" node "20140102T1200Z/baz" "baz\n20140102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/offset_initial/reference.log0000664000175000017500000000051315202510242026214 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140102T0000Z/bar -triggered off [] 20140102T0600Z/bar -triggered off [] 20140102T0000Z/baz -triggered off ['20140102T0000Z/bar'] 20140102T1200Z/bar -triggered off [] 20140102T0600Z/baz -triggered off ['20140102T0600Z/bar'] 20140102T1200Z/baz -triggered off ['20140102T1200Z/bar'] cylc-flow-8.6.4/tests/functional/cyclers/offset_initial/flow.cylc0000664000175000017500000000036015202510242025376 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] +P1D/PT6H = "bar => baz" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/multiyearly/0000775000175000017500000000000015202510242023135 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multiyearly/graph.plain.ref0000664000175000017500000000452315202510242026042 0ustar alastairalastairedge "10050101T0000Z/baz" "10050101T0000Z/qux" edge "10050101T0000Z/baz" "10090101T0000Z/baz" edge "10050101T0000Z/foo" "10050101T0000Z/bar" edge "10050101T0000Z/foo" "10060101T0000Z/foo" edge "10060101T0000Z/foo" "10060101T0000Z/bar" edge "10060101T0000Z/foo" "10070101T0000Z/foo" edge "10070101T0000Z/foo" "10070101T0000Z/bar" edge "10070101T0000Z/foo" "10080101T0000Z/foo" edge "10080101T0000Z/foo" "10080101T0000Z/bar" edge "10080101T0000Z/foo" "10090101T0000Z/foo" edge "10090101T0000Z/baz" "10090101T0000Z/qux" edge "10090101T0000Z/baz" "10130101T0000Z/baz" edge "10090101T0000Z/foo" "10090101T0000Z/bar" edge "10090101T0000Z/foo" "10100101T0000Z/foo" edge "10100101T0000Z/foo" "10100101T0000Z/bar" edge "10100101T0000Z/foo" "10110101T0000Z/foo" edge "10110101T0000Z/foo" "10110101T0000Z/bar" edge "10110101T0000Z/foo" "10120101T0000Z/foo" edge "10120101T0000Z/foo" "10120101T0000Z/bar" edge "10120101T0000Z/foo" "10130101T0000Z/foo" edge "10130101T0000Z/baz" "10130101T0000Z/qux" edge "10130101T0000Z/foo" "10130101T0000Z/bar" edge "10130101T0000Z/foo" "10140101T0000Z/foo" edge "10140101T0000Z/foo" "10140101T0000Z/bar" graph node "10050101T0000Z/bar" "bar\n10050101T0000Z" node "10050101T0000Z/baz" "baz\n10050101T0000Z" node "10050101T0000Z/foo" "foo\n10050101T0000Z" node "10050101T0000Z/qux" "qux\n10050101T0000Z" node "10060101T0000Z/bar" "bar\n10060101T0000Z" node "10060101T0000Z/foo" "foo\n10060101T0000Z" node "10070101T0000Z/bar" "bar\n10070101T0000Z" node "10070101T0000Z/foo" "foo\n10070101T0000Z" node "10080101T0000Z/bar" "bar\n10080101T0000Z" node "10080101T0000Z/foo" "foo\n10080101T0000Z" node "10090101T0000Z/bar" "bar\n10090101T0000Z" node "10090101T0000Z/baz" "baz\n10090101T0000Z" node "10090101T0000Z/foo" "foo\n10090101T0000Z" node "10090101T0000Z/qux" "qux\n10090101T0000Z" node "10100101T0000Z/bar" "bar\n10100101T0000Z" node "10100101T0000Z/foo" "foo\n10100101T0000Z" node "10110101T0000Z/bar" "bar\n10110101T0000Z" node "10110101T0000Z/foo" "foo\n10110101T0000Z" node "10120101T0000Z/bar" "bar\n10120101T0000Z" node "10120101T0000Z/foo" "foo\n10120101T0000Z" node "10130101T0000Z/bar" "bar\n10130101T0000Z" node "10130101T0000Z/baz" "baz\n10130101T0000Z" node "10130101T0000Z/foo" "foo\n10130101T0000Z" node "10130101T0000Z/qux" "qux\n10130101T0000Z" node "10140101T0000Z/bar" "bar\n10140101T0000Z" node "10140101T0000Z/foo" "foo\n10140101T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/multiyearly/reference.log0000664000175000017500000000276015202510242025603 0ustar alastairalastairInitial point: 1005 Final point: 1014 10050101T0000Z/baz -triggered off ['10010101T0000Z/baz'] 10050101T0000Z/foo -triggered off ['10040101T0000Z/foo'] 10050101T0000Z/qux -triggered off ['10050101T0000Z/baz'] 10090101T0000Z/baz -triggered off ['10050101T0000Z/baz'] 10050101T0000Z/bar -triggered off ['10050101T0000Z/foo'] 10060101T0000Z/foo -triggered off ['10050101T0000Z/foo'] 10060101T0000Z/bar -triggered off ['10060101T0000Z/foo'] 10070101T0000Z/foo -triggered off ['10060101T0000Z/foo'] 10090101T0000Z/qux -triggered off ['10090101T0000Z/baz'] 10070101T0000Z/bar -triggered off ['10070101T0000Z/foo'] 10080101T0000Z/foo -triggered off ['10070101T0000Z/foo'] 10130101T0000Z/baz -triggered off ['10090101T0000Z/baz'] 10080101T0000Z/bar -triggered off ['10080101T0000Z/foo'] 10090101T0000Z/foo -triggered off ['10080101T0000Z/foo'] 10130101T0000Z/qux -triggered off ['10130101T0000Z/baz'] 10090101T0000Z/bar -triggered off ['10090101T0000Z/foo'] 10100101T0000Z/foo -triggered off ['10090101T0000Z/foo'] 10100101T0000Z/bar -triggered off ['10100101T0000Z/foo'] 10110101T0000Z/foo -triggered off ['10100101T0000Z/foo'] 10110101T0000Z/bar -triggered off ['10110101T0000Z/foo'] 10120101T0000Z/foo -triggered off ['10110101T0000Z/foo'] 10120101T0000Z/bar -triggered off ['10120101T0000Z/foo'] 10130101T0000Z/foo -triggered off ['10120101T0000Z/foo'] 10130101T0000Z/bar -triggered off ['10130101T0000Z/foo'] 10140101T0000Z/foo -triggered off ['10130101T0000Z/foo'] 10140101T0000Z/bar -triggered off ['10140101T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multiyearly/flow.cylc0000664000175000017500000000042415202510242024760 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 1005 final cycle point = 1014 [[graph]] P1Y = "foo[-P1Y] => foo => bar" P4Y = "baz[-P4Y] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_back_comp_standalone_line/0000775000175000017500000000000015202510242030025 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_initial_back_comp_standalone_line/graph.plain.ref0000664000175000017500000000043715202510242032732 0ustar alastairalastairedge "2014010100/cold_foo" "2014010100/foo_midnight" edge "2014010100/foo_midnight" "2014010200/foo_midnight" graph node "2014010100/cold_foo" "cold_foo\n2014010100" node "2014010100/foo_midnight" "foo_midnight\n2014010100" node "2014010200/foo_midnight" "foo_midnight\n2014010200" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_back_comp_standalone_line/reference.log0000664000175000017500000000054015202510242032465 0ustar alastairalastairInitial point: 2014010100 Final point: 2014010400 2014010100/cold_foo -triggered off [] 2014010100/foo_midnight -triggered off ['2014010100/cold_foo'] 2014010200/foo_midnight -triggered off ['2014010100/foo_midnight'] 2014010300/foo_midnight -triggered off ['2014010200/foo_midnight'] 2014010400/foo_midnight -triggered off ['2014010300/foo_midnight'] cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_back_comp_standalone_line/flow.cylc0000664000175000017500000000055115202510242031651 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140104 [[special tasks]] start-up = cold_foo [[graph]] 0 = """ cold_foo foo_midnight[T-24] & cold_foo => foo_midnight """ [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/08-offset_final.t0000775000175000017500000000201615202510242023630 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/39-exclusions_advanced.t0000775000175000017500000000466715202510242025234 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then set_test_number 4 else set_test_number 3 fi #------------------------------------------------------------------------------- CHOSEN_WORKFLOW="$(basename "$0" | sed "s/^.*-\(.*\)\.t/\1/g")" install_workflow "${TEST_NAME_BASE}" "${CHOSEN_WORKFLOW}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" \ "20000101T00Z" "20010102T12Z" cmp_ok "${WORKFLOW_NAME}.graph.plain" "$TEST_SOURCE_DIR/$CHOSEN_WORKFLOW/graph.plain.ref" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then TEST_NAME="${TEST_NAME_BASE}-find" WORKFLOW_DIR="$RUN_DIR/${WORKFLOW_NAME}" (cd "${WORKFLOW_DIR}" && find 'log/job' 'work' -type f) | sort -V >"${TEST_NAME}" cmp_ok "${TEST_NAME}" "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" fi #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/40-integer_exclusions_advanced.t0000775000175000017500000000271515202510242026731 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" 'integer_exclusions_advanced' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" 1 "+P15" cmp_ok "${WORKFLOW_NAME}.graph.plain" 'graph.plain.ref' workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cyclers/exclusions/0000775000175000017500000000000015202510242022751 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/exclusions/graph.plain.ref0000664000175000017500000000223615202510242025655 0ustar alastairalastairedge "20000101T0000Z/start" "20000101T0000Z/foo" graph node "20000101T0000Z/baz" "baz\n20000101T0000Z" node "20000101T0000Z/foo" "foo\n20000101T0000Z" node "20000101T0000Z/nip" "nip\n20000101T0000Z" node "20000101T0000Z/quux" "quux\n20000101T0000Z" node "20000101T0000Z/qux" "qux\n20000101T0000Z" node "20000101T0000Z/start" "start\n20000101T0000Z" node "20000101T1000Z/quux" "quux\n20000101T1000Z" node "20000101T1200Z/baz" "baz\n20000101T1200Z" node "20000101T1200Z/nip" "nip\n20000101T1200Z" node "20000101T1500Z/quux" "quux\n20000101T1500Z" node "20000101T1800Z/nip" "nip\n20000101T1800Z" node "20000101T2000Z/quux" "quux\n20000101T2000Z" node "20000102T0000Z/bar" "bar\n20000102T0000Z" node "20000102T0000Z/baz" "baz\n20000102T0000Z" node "20000102T0000Z/nip" "nip\n20000102T0000Z" node "20000102T0000Z/qux" "qux\n20000102T0000Z" node "20000102T0100Z/quux" "quux\n20000102T0100Z" node "20000102T0600Z/nip" "nip\n20000102T0600Z" node "20000102T0600Z/quux" "quux\n20000102T0600Z" node "20000102T1100Z/quux" "quux\n20000102T1100Z" node "20000102T1200Z/nip" "nip\n20000102T1200Z" node "20000102T1200Z/pub" "pub\n20000102T1200Z" node "20000102T1200Z/qux" "qux\n20000102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/exclusions/reference.log0000664000175000017500000000165415202510242025420 0ustar alastairalastairInitial point: 20000101T0000Z Final point: 20000102T1200Z 20000101T0000Z/quux -triggered off [] 20000101T0000Z/start -triggered off [] 20000101T0000Z/baz -triggered off [] 20000101T0000Z/qux -triggered off [] 20000101T0000Z/nip -triggered off [] 20000101T1200Z/baz -triggered off [] 20000101T1200Z/nip -triggered off [] 20000101T1000Z/quux -triggered off [] 20000101T0000Z/foo -triggered off ['20000101T0000Z/start'] 20000101T1500Z/quux -triggered off [] 20000101T1800Z/nip -triggered off [] 20000101T2000Z/quux -triggered off [] 20000102T0000Z/nip -triggered off [] 20000102T0000Z/qux -triggered off [] 20000102T0000Z/bar -triggered off [] 20000102T0000Z/baz -triggered off [] 20000102T0100Z/quux -triggered off [] 20000102T0600Z/nip -triggered off [] 20000102T0600Z/quux -triggered off [] 20000102T1200Z/pub -triggered off [] 20000102T1200Z/qux -triggered off [] 20000102T1200Z/nip -triggered off [] 20000102T1100Z/quux -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/exclusions/flow.cylc0000664000175000017500000000131115202510242024570 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20000101T00Z final cycle point = 20000102T12Z [[graph]] R1 = start => foo # Don't run at the initial cycle point # Also test whitespace tolerance T00 ! ^ = bar # Don't run at the final cycle point PT12H!$ = baz # Don't run at a specific datetime PT12H!20000101T1200Z = qux # Don't run at a datetime with an offset R/PT6H!20000101T05Z+PT1H = nip # Don't run a multiple specific datetimes PT5H!(20000101T05Z,20000102T05Z) = quux R1/$ = pub [runtime] [[root]] script = echo success cylc-flow-8.6.4/tests/functional/cyclers/07-multiyearly.t0000775000175000017500000000200315202510242023544 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="1004" # shellcheck disable=SC2034 FINALCP="1014" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/multimonthly/0000775000175000017500000000000015202510242023322 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multimonthly/graph.plain.ref0000664000175000017500000000561515202510242026232 0ustar alastairalastairedge "10000101T0000Z/baz" "10000101T0000Z/qux" edge "10000101T0000Z/baz" "10000701T0000Z/baz" edge "10000101T0000Z/foo" "10000101T0000Z/bar" edge "10000101T0000Z/foo" "10000201T0000Z/foo" edge "10000201T0000Z/foo" "10000201T0000Z/bar" edge "10000201T0000Z/foo" "10000301T0000Z/foo" edge "10000301T0000Z/foo" "10000301T0000Z/bar" edge "10000301T0000Z/foo" "10000401T0000Z/foo" edge "10000401T0000Z/foo" "10000401T0000Z/bar" edge "10000401T0000Z/foo" "10000501T0000Z/foo" edge "10000501T0000Z/foo" "10000501T0000Z/bar" edge "10000501T0000Z/foo" "10000601T0000Z/foo" edge "10000601T0000Z/foo" "10000601T0000Z/bar" edge "10000601T0000Z/foo" "10000701T0000Z/foo" edge "10000701T0000Z/baz" "10000701T0000Z/qux" edge "10000701T0000Z/baz" "10010101T0000Z/baz" edge "10000701T0000Z/foo" "10000701T0000Z/bar" edge "10000701T0000Z/foo" "10000801T0000Z/foo" edge "10000801T0000Z/foo" "10000801T0000Z/bar" edge "10000801T0000Z/foo" "10000901T0000Z/foo" edge "10000901T0000Z/foo" "10000901T0000Z/bar" edge "10000901T0000Z/foo" "10001001T0000Z/foo" edge "10001001T0000Z/foo" "10001001T0000Z/bar" edge "10001001T0000Z/foo" "10001101T0000Z/foo" edge "10001101T0000Z/foo" "10001101T0000Z/bar" edge "10001101T0000Z/foo" "10001201T0000Z/foo" edge "10001201T0000Z/foo" "10001201T0000Z/bar" edge "10001201T0000Z/foo" "10010101T0000Z/foo" edge "10010101T0000Z/baz" "10010101T0000Z/qux" edge "10010101T0000Z/foo" "10010101T0000Z/bar" graph node "10000101T0000Z/bar" "bar\n10000101T0000Z" node "10000101T0000Z/baz" "baz\n10000101T0000Z" node "10000101T0000Z/foo" "foo\n10000101T0000Z" node "10000101T0000Z/qux" "qux\n10000101T0000Z" node "10000201T0000Z/bar" "bar\n10000201T0000Z" node "10000201T0000Z/foo" "foo\n10000201T0000Z" node "10000301T0000Z/bar" "bar\n10000301T0000Z" node "10000301T0000Z/foo" "foo\n10000301T0000Z" node "10000401T0000Z/bar" "bar\n10000401T0000Z" node "10000401T0000Z/foo" "foo\n10000401T0000Z" node "10000501T0000Z/bar" "bar\n10000501T0000Z" node "10000501T0000Z/foo" "foo\n10000501T0000Z" node "10000601T0000Z/bar" "bar\n10000601T0000Z" node "10000601T0000Z/foo" "foo\n10000601T0000Z" node "10000701T0000Z/bar" "bar\n10000701T0000Z" node "10000701T0000Z/baz" "baz\n10000701T0000Z" node "10000701T0000Z/foo" "foo\n10000701T0000Z" node "10000701T0000Z/qux" "qux\n10000701T0000Z" node "10000801T0000Z/bar" "bar\n10000801T0000Z" node "10000801T0000Z/foo" "foo\n10000801T0000Z" node "10000901T0000Z/bar" "bar\n10000901T0000Z" node "10000901T0000Z/foo" "foo\n10000901T0000Z" node "10001001T0000Z/bar" "bar\n10001001T0000Z" node "10001001T0000Z/foo" "foo\n10001001T0000Z" node "10001101T0000Z/bar" "bar\n10001101T0000Z" node "10001101T0000Z/foo" "foo\n10001101T0000Z" node "10001201T0000Z/bar" "bar\n10001201T0000Z" node "10001201T0000Z/foo" "foo\n10001201T0000Z" node "10010101T0000Z/bar" "bar\n10010101T0000Z" node "10010101T0000Z/baz" "baz\n10010101T0000Z" node "10010101T0000Z/foo" "foo\n10010101T0000Z" node "10010101T0000Z/qux" "qux\n10010101T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/multimonthly/reference.log0000664000175000017500000000350615202510242025767 0ustar alastairalastairInitial point: 1000 Final point: 1001 10000101T0000Z/baz -triggered off ['09990701T0000Z/baz'] 10000101T0000Z/foo -triggered off ['09991201T0000Z/foo'] 10000101T0000Z/bar -triggered off ['10000101T0000Z/foo'] 10000101T0000Z/qux -triggered off ['10000101T0000Z/baz'] 10000701T0000Z/baz -triggered off ['10000101T0000Z/baz'] 10000201T0000Z/foo -triggered off ['10000101T0000Z/foo'] 10000701T0000Z/qux -triggered off ['10000701T0000Z/baz'] 10000201T0000Z/bar -triggered off ['10000201T0000Z/foo'] 10000301T0000Z/foo -triggered off ['10000201T0000Z/foo'] 10000301T0000Z/bar -triggered off ['10000301T0000Z/foo'] 10000401T0000Z/foo -triggered off ['10000301T0000Z/foo'] 10000401T0000Z/bar -triggered off ['10000401T0000Z/foo'] 10000501T0000Z/foo -triggered off ['10000401T0000Z/foo'] 10010101T0000Z/baz -triggered off ['10000701T0000Z/baz'] 10000501T0000Z/bar -triggered off ['10000501T0000Z/foo'] 10000601T0000Z/foo -triggered off ['10000501T0000Z/foo'] 10010101T0000Z/qux -triggered off ['10010101T0000Z/baz'] 10000701T0000Z/foo -triggered off ['10000601T0000Z/foo'] 10000601T0000Z/bar -triggered off ['10000601T0000Z/foo'] 10000801T0000Z/foo -triggered off ['10000701T0000Z/foo'] 10000701T0000Z/bar -triggered off ['10000701T0000Z/foo'] 10000901T0000Z/foo -triggered off ['10000801T0000Z/foo'] 10000801T0000Z/bar -triggered off ['10000801T0000Z/foo'] 10001001T0000Z/foo -triggered off ['10000901T0000Z/foo'] 10000901T0000Z/bar -triggered off ['10000901T0000Z/foo'] 10001101T0000Z/foo -triggered off ['10001001T0000Z/foo'] 10001001T0000Z/bar -triggered off ['10001001T0000Z/foo'] 10001201T0000Z/foo -triggered off ['10001101T0000Z/foo'] 10001101T0000Z/bar -triggered off ['10001101T0000Z/foo'] 10010101T0000Z/foo -triggered off ['10001201T0000Z/foo'] 10001201T0000Z/bar -triggered off ['10001201T0000Z/foo'] 10010101T0000Z/bar -triggered off ['10010101T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multimonthly/flow.cylc0000664000175000017500000000042415202510242025145 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 1000 final cycle point = 1001 [[graph]] P1M = "foo[-P1M] => foo => bar" P6M = "baz[-P6M] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/12-r1_middle.t0000775000175000017500000000201615202510242023024 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/multidaily_local/0000775000175000017500000000000015202510242024104 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multidaily_local/graph.plain.ref0000664000175000017500000000641115202510242027007 0ustar alastairalastairedge "20001231T0100Z/baz" "20001231T0100Z/qux" edge "20001231T0100Z/baz" "20010104T0100Z/baz" edge "20001231T0100Z/foo" "20001231T0100Z/bar" edge "20001231T0100Z/foo" "20010101T0100Z/foo" edge "20010101T0100Z/foo" "20010101T0100Z/bar" edge "20010101T0100Z/foo" "20010102T0100Z/foo" edge "20010102T0100Z/foo" "20010102T0100Z/bar" edge "20010102T0100Z/foo" "20010103T0100Z/foo" edge "20010103T0100Z/foo" "20010103T0100Z/bar" edge "20010103T0100Z/foo" "20010104T0100Z/foo" edge "20010104T0100Z/baz" "20010104T0100Z/qux" edge "20010104T0100Z/baz" "20010108T0100Z/baz" edge "20010104T0100Z/foo" "20010104T0100Z/bar" edge "20010104T0100Z/foo" "20010105T0100Z/foo" edge "20010105T0100Z/foo" "20010105T0100Z/bar" edge "20010105T0100Z/foo" "20010106T0100Z/foo" edge "20010106T0100Z/foo" "20010106T0100Z/bar" edge "20010106T0100Z/foo" "20010107T0100Z/foo" edge "20010107T0100Z/foo" "20010107T0100Z/bar" edge "20010107T0100Z/foo" "20010108T0100Z/foo" edge "20010108T0100Z/baz" "20010108T0100Z/qux" edge "20010108T0100Z/baz" "20010112T0100Z/baz" edge "20010108T0100Z/foo" "20010108T0100Z/bar" edge "20010108T0100Z/foo" "20010109T0100Z/foo" edge "20010109T0100Z/foo" "20010109T0100Z/bar" edge "20010109T0100Z/foo" "20010110T0100Z/foo" edge "20010110T0100Z/foo" "20010110T0100Z/bar" edge "20010110T0100Z/foo" "20010111T0100Z/foo" edge "20010111T0100Z/foo" "20010111T0100Z/bar" edge "20010111T0100Z/foo" "20010112T0100Z/foo" edge "20010112T0100Z/baz" "20010112T0100Z/qux" edge "20010112T0100Z/foo" "20010112T0100Z/bar" edge "20010112T0100Z/foo" "20010113T0100Z/foo" edge "20010113T0100Z/foo" "20010113T0100Z/bar" graph node "20001231T0100Z/bar" "bar\n20001231T0100Z" node "20001231T0100Z/baz" "baz\n20001231T0100Z" node "20001231T0100Z/foo" "foo\n20001231T0100Z" node "20001231T0100Z/qux" "qux\n20001231T0100Z" node "20010101T0100Z/bar" "bar\n20010101T0100Z" node "20010101T0100Z/foo" "foo\n20010101T0100Z" node "20010102T0100Z/bar" "bar\n20010102T0100Z" node "20010102T0100Z/foo" "foo\n20010102T0100Z" node "20010103T0100Z/bar" "bar\n20010103T0100Z" node "20010103T0100Z/foo" "foo\n20010103T0100Z" node "20010104T0100Z/bar" "bar\n20010104T0100Z" node "20010104T0100Z/baz" "baz\n20010104T0100Z" node "20010104T0100Z/foo" "foo\n20010104T0100Z" node "20010104T0100Z/qux" "qux\n20010104T0100Z" node "20010105T0100Z/bar" "bar\n20010105T0100Z" node "20010105T0100Z/foo" "foo\n20010105T0100Z" node "20010106T0100Z/bar" "bar\n20010106T0100Z" node "20010106T0100Z/foo" "foo\n20010106T0100Z" node "20010107T0100Z/bar" "bar\n20010107T0100Z" node "20010107T0100Z/foo" "foo\n20010107T0100Z" node "20010108T0100Z/bar" "bar\n20010108T0100Z" node "20010108T0100Z/baz" "baz\n20010108T0100Z" node "20010108T0100Z/foo" "foo\n20010108T0100Z" node "20010108T0100Z/qux" "qux\n20010108T0100Z" node "20010109T0100Z/bar" "bar\n20010109T0100Z" node "20010109T0100Z/foo" "foo\n20010109T0100Z" node "20010110T0100Z/bar" "bar\n20010110T0100Z" node "20010110T0100Z/foo" "foo\n20010110T0100Z" node "20010111T0100Z/bar" "bar\n20010111T0100Z" node "20010111T0100Z/foo" "foo\n20010111T0100Z" node "20010112T0100Z/bar" "bar\n20010112T0100Z" node "20010112T0100Z/baz" "baz\n20010112T0100Z" node "20010112T0100Z/foo" "foo\n20010112T0100Z" node "20010112T0100Z/qux" "qux\n20010112T0100Z" node "20010113T0100Z/bar" "bar\n20010113T0100Z" node "20010113T0100Z/foo" "foo\n20010113T0100Z" stop cylc-flow-8.6.4/tests/functional/cyclers/multidaily_local/reference.log0000664000175000017500000000406715202510242026554 0ustar alastairalastairInitial point: 20001231T0100 Final point: 20010114 20001231T0100Z/baz -triggered off ['20001227T0100Z/baz'] 20001231T0100Z/foo -triggered off ['20001230T0100Z/foo'] 20001231T0100Z/qux -triggered off ['20001231T0100Z/baz'] 20010104T0100Z/baz -triggered off ['20001231T0100Z/baz'] 20001231T0100Z/bar -triggered off ['20001231T0100Z/foo'] 20010101T0100Z/foo -triggered off ['20001231T0100Z/foo'] 20010104T0100Z/qux -triggered off ['20010104T0100Z/baz'] 20010101T0100Z/bar -triggered off ['20010101T0100Z/foo'] 20010102T0100Z/foo -triggered off ['20010101T0100Z/foo'] 20010108T0100Z/baz -triggered off ['20010104T0100Z/baz'] 20010102T0100Z/bar -triggered off ['20010102T0100Z/foo'] 20010103T0100Z/foo -triggered off ['20010102T0100Z/foo'] 20010108T0100Z/qux -triggered off ['20010108T0100Z/baz'] 20010103T0100Z/bar -triggered off ['20010103T0100Z/foo'] 20010104T0100Z/foo -triggered off ['20010103T0100Z/foo'] 20010104T0100Z/bar -triggered off ['20010104T0100Z/foo'] 20010105T0100Z/foo -triggered off ['20010104T0100Z/foo'] 20010105T0100Z/bar -triggered off ['20010105T0100Z/foo'] 20010106T0100Z/foo -triggered off ['20010105T0100Z/foo'] 20010112T0100Z/baz -triggered off ['20010108T0100Z/baz'] 20010106T0100Z/bar -triggered off ['20010106T0100Z/foo'] 20010107T0100Z/foo -triggered off ['20010106T0100Z/foo'] 20010112T0100Z/qux -triggered off ['20010112T0100Z/baz'] 20010108T0100Z/foo -triggered off ['20010107T0100Z/foo'] 20010107T0100Z/bar -triggered off ['20010107T0100Z/foo'] 20010108T0100Z/bar -triggered off ['20010108T0100Z/foo'] 20010109T0100Z/foo -triggered off ['20010108T0100Z/foo'] 20010109T0100Z/bar -triggered off ['20010109T0100Z/foo'] 20010110T0100Z/foo -triggered off ['20010109T0100Z/foo'] 20010110T0100Z/bar -triggered off ['20010110T0100Z/foo'] 20010111T0100Z/foo -triggered off ['20010110T0100Z/foo'] 20010111T0100Z/bar -triggered off ['20010111T0100Z/foo'] 20010112T0100Z/foo -triggered off ['20010111T0100Z/foo'] 20010112T0100Z/bar -triggered off ['20010112T0100Z/foo'] 20010113T0100Z/foo -triggered off ['20010112T0100Z/foo'] 20010113T0100Z/bar -triggered off ['20010113T0100Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multidaily_local/flow.cylc0000664000175000017500000000041515202510242025727 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20001231T0100 final cycle point = 20010114 [[graph]] P1D = "foo[-P1D] => foo => bar" P4D = "baz[-P4D] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/16-weekly.t0000775000175000017500000000202615202510242022471 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="2005W015T12" # shellcheck disable=SC2034 FINALCP="2005W065T11-0100" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/r5_initial/0000775000175000017500000000000015202510242022614 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r5_initial/graph.plain.ref0000664000175000017500000000153415202510242025520 0ustar alastairalastairedge "20140101T0000Z/xyzzy" "20140101T0000Z/bar" edge "20140101T0300Z/xyzzy" "20140101T0300Z/bar" edge "20140101T0600Z/xyzzy" "20140101T0600Z/bar" edge "20140101T0900Z/xyzzy" "20140101T0900Z/bar" edge "20140101T1200Z/xyzzy" "20140101T1200Z/bar" graph node "20140101T0000Z/bar" "bar\n20140101T0000Z" node "20140101T0000Z/xyzzy" "xyzzy\n20140101T0000Z" node "20140101T0300Z/bar" "bar\n20140101T0300Z" node "20140101T0300Z/xyzzy" "xyzzy\n20140101T0300Z" node "20140101T0600Z/bar" "bar\n20140101T0600Z" node "20140101T0600Z/xyzzy" "xyzzy\n20140101T0600Z" node "20140101T0900Z/bar" "bar\n20140101T0900Z" node "20140101T0900Z/xyzzy" "xyzzy\n20140101T0900Z" node "20140101T1200Z/bar" "bar\n20140101T1200Z" node "20140101T1200Z/xyzzy" "xyzzy\n20140101T1200Z" node "20140101T1500Z/xyzzy" "xyzzy\n20140101T1500Z" node "20140101T1800Z/xyzzy" "xyzzy\n20140101T1800Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r5_initial/reference.log0000664000175000017500000000115115202510242025253 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140101T0000Z/xyzzy -triggered off [] 20140101T0300Z/xyzzy -triggered off [] 20140101T0600Z/xyzzy -triggered off [] 20140101T0000Z/bar -triggered off ['20140101T0000Z/xyzzy'] 20140101T0300Z/bar -triggered off ['20140101T0300Z/xyzzy'] 20140101T0900Z/xyzzy -triggered off [] 20140101T0600Z/bar -triggered off ['20140101T0600Z/xyzzy'] 20140101T0900Z/bar -triggered off ['20140101T0900Z/xyzzy'] 20140101T1200Z/xyzzy -triggered off [] 20140101T1200Z/bar -triggered off ['20140101T1200Z/xyzzy'] 20140101T1500Z/xyzzy -triggered off [] 20140101T1800Z/xyzzy -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/r5_initial/flow.cylc0000664000175000017500000000054115202510242024437 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140101T18 [[graph]] # xyzzy should not spawn bar in the last two cycle points: PT3H = xyzzy # 7 cycles R5//PT3H = "xyzzy => bar" # 5 cycles [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/yearly/0000775000175000017500000000000015202510242022062 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/yearly/graph.plain.ref0000664000175000017500000000102615202510242024762 0ustar alastairalastairedge "20050123T1200Z/foo" "20050123T1200Z/bar" edge "20050123T1200Z/foo" "20060123T1200Z/foo" edge "20060123T1200Z/foo" "20060123T1200Z/bar" edge "20060123T1200Z/foo" "20070123T1200Z/foo" edge "20070123T1200Z/foo" "20070123T1200Z/bar" graph node "20050123T1200Z/bar" "bar\n20050123T1200Z" node "20050123T1200Z/foo" "foo\n20050123T1200Z" node "20060123T1200Z/bar" "bar\n20060123T1200Z" node "20060123T1200Z/foo" "foo\n20060123T1200Z" node "20070123T1200Z/bar" "bar\n20070123T1200Z" node "20070123T1200Z/foo" "foo\n20070123T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/yearly/reference.log0000664000175000017500000000061015202510242024520 0ustar alastairalastairInitial point: 2005023T12 Final point: 2008001T11 20050123T1200Z/foo -triggered off ['20040123T1200Z/foo'] 20050123T1200Z/bar -triggered off ['20050123T1200Z/foo'] 20060123T1200Z/foo -triggered off ['20050123T1200Z/foo'] 20060123T1200Z/bar -triggered off ['20060123T1200Z/foo'] 20070123T1200Z/foo -triggered off ['20060123T1200Z/foo'] 20070123T1200Z/bar -triggered off ['20070123T1200Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/yearly/flow.cylc0000664000175000017500000000037015202510242023705 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2005023T12 final cycle point = 2008001T11 [[graph]] P1Y = "foo[-P1Y] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/multidaily/0000775000175000017500000000000015202510242022732 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multidaily/graph.plain.ref0000664000175000017500000000641115202510242025635 0ustar alastairalastairedge "20001231T0100Z/baz" "20001231T0100Z/qux" edge "20001231T0100Z/baz" "20010104T0100Z/baz" edge "20001231T0100Z/foo" "20001231T0100Z/bar" edge "20001231T0100Z/foo" "20010101T0100Z/foo" edge "20010101T0100Z/foo" "20010101T0100Z/bar" edge "20010101T0100Z/foo" "20010102T0100Z/foo" edge "20010102T0100Z/foo" "20010102T0100Z/bar" edge "20010102T0100Z/foo" "20010103T0100Z/foo" edge "20010103T0100Z/foo" "20010103T0100Z/bar" edge "20010103T0100Z/foo" "20010104T0100Z/foo" edge "20010104T0100Z/baz" "20010104T0100Z/qux" edge "20010104T0100Z/baz" "20010108T0100Z/baz" edge "20010104T0100Z/foo" "20010104T0100Z/bar" edge "20010104T0100Z/foo" "20010105T0100Z/foo" edge "20010105T0100Z/foo" "20010105T0100Z/bar" edge "20010105T0100Z/foo" "20010106T0100Z/foo" edge "20010106T0100Z/foo" "20010106T0100Z/bar" edge "20010106T0100Z/foo" "20010107T0100Z/foo" edge "20010107T0100Z/foo" "20010107T0100Z/bar" edge "20010107T0100Z/foo" "20010108T0100Z/foo" edge "20010108T0100Z/baz" "20010108T0100Z/qux" edge "20010108T0100Z/baz" "20010112T0100Z/baz" edge "20010108T0100Z/foo" "20010108T0100Z/bar" edge "20010108T0100Z/foo" "20010109T0100Z/foo" edge "20010109T0100Z/foo" "20010109T0100Z/bar" edge "20010109T0100Z/foo" "20010110T0100Z/foo" edge "20010110T0100Z/foo" "20010110T0100Z/bar" edge "20010110T0100Z/foo" "20010111T0100Z/foo" edge "20010111T0100Z/foo" "20010111T0100Z/bar" edge "20010111T0100Z/foo" "20010112T0100Z/foo" edge "20010112T0100Z/baz" "20010112T0100Z/qux" edge "20010112T0100Z/foo" "20010112T0100Z/bar" edge "20010112T0100Z/foo" "20010113T0100Z/foo" edge "20010113T0100Z/foo" "20010113T0100Z/bar" graph node "20001231T0100Z/bar" "bar\n20001231T0100Z" node "20001231T0100Z/baz" "baz\n20001231T0100Z" node "20001231T0100Z/foo" "foo\n20001231T0100Z" node "20001231T0100Z/qux" "qux\n20001231T0100Z" node "20010101T0100Z/bar" "bar\n20010101T0100Z" node "20010101T0100Z/foo" "foo\n20010101T0100Z" node "20010102T0100Z/bar" "bar\n20010102T0100Z" node "20010102T0100Z/foo" "foo\n20010102T0100Z" node "20010103T0100Z/bar" "bar\n20010103T0100Z" node "20010103T0100Z/foo" "foo\n20010103T0100Z" node "20010104T0100Z/bar" "bar\n20010104T0100Z" node "20010104T0100Z/baz" "baz\n20010104T0100Z" node "20010104T0100Z/foo" "foo\n20010104T0100Z" node "20010104T0100Z/qux" "qux\n20010104T0100Z" node "20010105T0100Z/bar" "bar\n20010105T0100Z" node "20010105T0100Z/foo" "foo\n20010105T0100Z" node "20010106T0100Z/bar" "bar\n20010106T0100Z" node "20010106T0100Z/foo" "foo\n20010106T0100Z" node "20010107T0100Z/bar" "bar\n20010107T0100Z" node "20010107T0100Z/foo" "foo\n20010107T0100Z" node "20010108T0100Z/bar" "bar\n20010108T0100Z" node "20010108T0100Z/baz" "baz\n20010108T0100Z" node "20010108T0100Z/foo" "foo\n20010108T0100Z" node "20010108T0100Z/qux" "qux\n20010108T0100Z" node "20010109T0100Z/bar" "bar\n20010109T0100Z" node "20010109T0100Z/foo" "foo\n20010109T0100Z" node "20010110T0100Z/bar" "bar\n20010110T0100Z" node "20010110T0100Z/foo" "foo\n20010110T0100Z" node "20010111T0100Z/bar" "bar\n20010111T0100Z" node "20010111T0100Z/foo" "foo\n20010111T0100Z" node "20010112T0100Z/bar" "bar\n20010112T0100Z" node "20010112T0100Z/baz" "baz\n20010112T0100Z" node "20010112T0100Z/foo" "foo\n20010112T0100Z" node "20010112T0100Z/qux" "qux\n20010112T0100Z" node "20010113T0100Z/bar" "bar\n20010113T0100Z" node "20010113T0100Z/foo" "foo\n20010113T0100Z" stop cylc-flow-8.6.4/tests/functional/cyclers/multidaily/reference.log0000664000175000017500000000406715202510242025402 0ustar alastairalastairInitial point: 20001231T0100 Final point: 20010114 20001231T0100Z/baz -triggered off ['20001227T0100Z/baz'] 20001231T0100Z/foo -triggered off ['20001230T0100Z/foo'] 20001231T0100Z/qux -triggered off ['20001231T0100Z/baz'] 20010104T0100Z/baz -triggered off ['20001231T0100Z/baz'] 20001231T0100Z/bar -triggered off ['20001231T0100Z/foo'] 20010101T0100Z/foo -triggered off ['20001231T0100Z/foo'] 20010104T0100Z/qux -triggered off ['20010104T0100Z/baz'] 20010101T0100Z/bar -triggered off ['20010101T0100Z/foo'] 20010102T0100Z/foo -triggered off ['20010101T0100Z/foo'] 20010108T0100Z/baz -triggered off ['20010104T0100Z/baz'] 20010102T0100Z/bar -triggered off ['20010102T0100Z/foo'] 20010103T0100Z/foo -triggered off ['20010102T0100Z/foo'] 20010108T0100Z/qux -triggered off ['20010108T0100Z/baz'] 20010103T0100Z/bar -triggered off ['20010103T0100Z/foo'] 20010104T0100Z/foo -triggered off ['20010103T0100Z/foo'] 20010104T0100Z/bar -triggered off ['20010104T0100Z/foo'] 20010105T0100Z/foo -triggered off ['20010104T0100Z/foo'] 20010105T0100Z/bar -triggered off ['20010105T0100Z/foo'] 20010106T0100Z/foo -triggered off ['20010105T0100Z/foo'] 20010112T0100Z/baz -triggered off ['20010108T0100Z/baz'] 20010106T0100Z/bar -triggered off ['20010106T0100Z/foo'] 20010107T0100Z/foo -triggered off ['20010106T0100Z/foo'] 20010112T0100Z/qux -triggered off ['20010112T0100Z/baz'] 20010108T0100Z/foo -triggered off ['20010107T0100Z/foo'] 20010107T0100Z/bar -triggered off ['20010107T0100Z/foo'] 20010108T0100Z/bar -triggered off ['20010108T0100Z/foo'] 20010109T0100Z/foo -triggered off ['20010108T0100Z/foo'] 20010109T0100Z/bar -triggered off ['20010109T0100Z/foo'] 20010110T0100Z/foo -triggered off ['20010109T0100Z/foo'] 20010110T0100Z/bar -triggered off ['20010110T0100Z/foo'] 20010111T0100Z/foo -triggered off ['20010110T0100Z/foo'] 20010111T0100Z/bar -triggered off ['20010111T0100Z/foo'] 20010112T0100Z/foo -triggered off ['20010111T0100Z/foo'] 20010112T0100Z/bar -triggered off ['20010112T0100Z/foo'] 20010113T0100Z/foo -triggered off ['20010112T0100Z/foo'] 20010113T0100Z/bar -triggered off ['20010113T0100Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multidaily/flow.cylc0000664000175000017500000000045315202510242024557 0ustar alastairalastair[scheduler] cycle point time zone = Z allow implicit tasks = True [scheduling] initial cycle point = 20001231T0100 final cycle point = 20010114 [[graph]] P1D = "foo[-P1D] => foo => bar" P4D = "baz[-P4D] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/exclusions_advanced/0000775000175000017500000000000015202510242024576 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/exclusions_advanced/graph.plain.ref0000664000175000017500000002150415202510242027501 0ustar alastairalastairedge "20000101T0000Z/start" "20000101T0000Z/foo" graph node "20000101T0000Z/dibble" "dibble\n20000101T0000Z" node "20000101T0000Z/foo" "foo\n20000101T0000Z" node "20000101T0000Z/qux" "qux\n20000101T0000Z" node "20000101T0000Z/start" "start\n20000101T0000Z" node "20000101T0030Z/quux" "quux\n20000101T0030Z" node "20000101T0100Z/nip" "nip\n20000101T0100Z" node "20000101T0100Z/wibble" "wibble\n20000101T0100Z" node "20000101T0115Z/bob" "bob\n20000101T0115Z" node "20000101T0115Z/toot" "toot\n20000101T0115Z" node "20000101T0130Z/quux" "quux\n20000101T0130Z" node "20000101T0200Z/nip" "nip\n20000101T0200Z" node "20000101T0200Z/wibble" "wibble\n20000101T0200Z" node "20000101T0215Z/bob" "bob\n20000101T0215Z" node "20000101T0215Z/toot" "toot\n20000101T0215Z" node "20000101T0230Z/dibble" "dibble\n20000101T0230Z" node "20000101T0230Z/quux" "quux\n20000101T0230Z" node "20000101T0300Z/baz" "baz\n20000101T0300Z" node "20000101T0300Z/dibble" "dibble\n20000101T0300Z" node "20000101T0300Z/nip" "nip\n20000101T0300Z" node "20000101T0300Z/qux" "qux\n20000101T0300Z" node "20000101T0300Z/wibble" "wibble\n20000101T0300Z" node "20000101T0330Z/quux" "quux\n20000101T0330Z" node "20000101T0400Z/nip" "nip\n20000101T0400Z" node "20000101T0400Z/wibble" "wibble\n20000101T0400Z" node "20000101T0415Z/bob" "bob\n20000101T0415Z" node "20000101T0415Z/toot" "toot\n20000101T0415Z" node "20000101T0430Z/quux" "quux\n20000101T0430Z" node "20000101T0500Z/nip" "nip\n20000101T0500Z" node "20000101T0500Z/wibble" "wibble\n20000101T0500Z" node "20000101T0515Z/bob" "bob\n20000101T0515Z" node "20000101T0515Z/toot" "toot\n20000101T0515Z" node "20000101T0530Z/quux" "quux\n20000101T0530Z" node "20000101T0600Z/dibble" "dibble\n20000101T0600Z" node "20000101T0600Z/qux" "qux\n20000101T0600Z" node "20000101T0630Z/quux" "quux\n20000101T0630Z" node "20000101T0700Z/nip" "nip\n20000101T0700Z" node "20000101T0700Z/wibble" "wibble\n20000101T0700Z" node "20000101T0715Z/bob" "bob\n20000101T0715Z" node "20000101T0715Z/toot" "toot\n20000101T0715Z" node "20000101T0730Z/quux" "quux\n20000101T0730Z" node "20000101T0800Z/nip" "nip\n20000101T0800Z" node "20000101T0800Z/wibble" "wibble\n20000101T0800Z" node "20000101T0815Z/bob" "bob\n20000101T0815Z" node "20000101T0815Z/toot" "toot\n20000101T0815Z" node "20000101T0830Z/quux" "quux\n20000101T0830Z" node "20000101T0900Z/baz" "baz\n20000101T0900Z" node "20000101T0900Z/nip" "nip\n20000101T0900Z" node "20000101T0900Z/qux" "qux\n20000101T0900Z" node "20000101T0900Z/wibble" "wibble\n20000101T0900Z" node "20000101T0930Z/quux" "quux\n20000101T0930Z" node "20000101T1000Z/nip" "nip\n20000101T1000Z" node "20000101T1000Z/wibble" "wibble\n20000101T1000Z" node "20000101T1015Z/bob" "bob\n20000101T1015Z" node "20000101T1015Z/toot" "toot\n20000101T1015Z" node "20000101T1030Z/quux" "quux\n20000101T1030Z" node "20000101T1100Z/nip" "nip\n20000101T1100Z" node "20000101T1100Z/wibble" "wibble\n20000101T1100Z" node "20000101T1115Z/bob" "bob\n20000101T1115Z" node "20000101T1115Z/toot" "toot\n20000101T1115Z" node "20000101T1130Z/quux" "quux\n20000101T1130Z" node "20000101T1200Z/qux" "qux\n20000101T1200Z" node "20000101T1230Z/quux" "quux\n20000101T1230Z" node "20000101T1300Z/nip" "nip\n20000101T1300Z" node "20000101T1300Z/wibble" "wibble\n20000101T1300Z" node "20000101T1315Z/bob" "bob\n20000101T1315Z" node "20000101T1315Z/toot" "toot\n20000101T1315Z" node "20000101T1330Z/quux" "quux\n20000101T1330Z" node "20000101T1400Z/nip" "nip\n20000101T1400Z" node "20000101T1400Z/wibble" "wibble\n20000101T1400Z" node "20000101T1415Z/bob" "bob\n20000101T1415Z" node "20000101T1415Z/toot" "toot\n20000101T1415Z" node "20000101T1430Z/quux" "quux\n20000101T1430Z" node "20000101T1500Z/baz" "baz\n20000101T1500Z" node "20000101T1500Z/dibble" "dibble\n20000101T1500Z" node "20000101T1500Z/nip" "nip\n20000101T1500Z" node "20000101T1500Z/qux" "qux\n20000101T1500Z" node "20000101T1500Z/wibble" "wibble\n20000101T1500Z" node "20000101T1530Z/quux" "quux\n20000101T1530Z" node "20000101T1600Z/nip" "nip\n20000101T1600Z" node "20000101T1600Z/wibble" "wibble\n20000101T1600Z" node "20000101T1615Z/bob" "bob\n20000101T1615Z" node "20000101T1615Z/toot" "toot\n20000101T1615Z" node "20000101T1630Z/quux" "quux\n20000101T1630Z" node "20000101T1700Z/nip" "nip\n20000101T1700Z" node "20000101T1700Z/wibble" "wibble\n20000101T1700Z" node "20000101T1715Z/bob" "bob\n20000101T1715Z" node "20000101T1715Z/toot" "toot\n20000101T1715Z" node "20000101T1730Z/quux" "quux\n20000101T1730Z" node "20000101T1800Z/dibble" "dibble\n20000101T1800Z" node "20000101T1800Z/qux" "qux\n20000101T1800Z" node "20000101T1830Z/quux" "quux\n20000101T1830Z" node "20000101T1900Z/nip" "nip\n20000101T1900Z" node "20000101T1900Z/wibble" "wibble\n20000101T1900Z" node "20000101T1915Z/bob" "bob\n20000101T1915Z" node "20000101T1915Z/toot" "toot\n20000101T1915Z" node "20000101T1930Z/quux" "quux\n20000101T1930Z" node "20000101T1945Z/dibble" "dibble\n20000101T1945Z" node "20000101T2000Z/nip" "nip\n20000101T2000Z" node "20000101T2000Z/wibble" "wibble\n20000101T2000Z" node "20000101T2015Z/bob" "bob\n20000101T2015Z" node "20000101T2015Z/toot" "toot\n20000101T2015Z" node "20000101T2030Z/quux" "quux\n20000101T2030Z" node "20000101T2100Z/baz" "baz\n20000101T2100Z" node "20000101T2100Z/dibble" "dibble\n20000101T2100Z" node "20000101T2100Z/nip" "nip\n20000101T2100Z" node "20000101T2100Z/qux" "qux\n20000101T2100Z" node "20000101T2100Z/wibble" "wibble\n20000101T2100Z" node "20000101T2130Z/quux" "quux\n20000101T2130Z" node "20000101T2200Z/nip" "nip\n20000101T2200Z" node "20000101T2200Z/wibble" "wibble\n20000101T2200Z" node "20000101T2215Z/bob" "bob\n20000101T2215Z" node "20000101T2215Z/toot" "toot\n20000101T2215Z" node "20000101T2230Z/quux" "quux\n20000101T2230Z" node "20000101T2300Z/nip" "nip\n20000101T2300Z" node "20000101T2300Z/wibble" "wibble\n20000101T2300Z" node "20000101T2315Z/bob" "bob\n20000101T2315Z" node "20000101T2315Z/toot" "toot\n20000101T2315Z" node "20000101T2330Z/quux" "quux\n20000101T2330Z" node "20000102T0000Z/bar" "bar\n20000102T0000Z" node "20000102T0000Z/dibble" "dibble\n20000102T0000Z" node "20000102T0000Z/qux" "qux\n20000102T0000Z" node "20000102T0030Z/quux" "quux\n20000102T0030Z" node "20000102T0100Z/nip" "nip\n20000102T0100Z" node "20000102T0100Z/wibble" "wibble\n20000102T0100Z" node "20000102T0115Z/bob" "bob\n20000102T0115Z" node "20000102T0115Z/toot" "toot\n20000102T0115Z" node "20000102T0130Z/quux" "quux\n20000102T0130Z" node "20000102T0200Z/nip" "nip\n20000102T0200Z" node "20000102T0200Z/wibble" "wibble\n20000102T0200Z" node "20000102T0215Z/bob" "bob\n20000102T0215Z" node "20000102T0215Z/toot" "toot\n20000102T0215Z" node "20000102T0230Z/dibble" "dibble\n20000102T0230Z" node "20000102T0230Z/quux" "quux\n20000102T0230Z" node "20000102T0300Z/baz" "baz\n20000102T0300Z" node "20000102T0300Z/dibble" "dibble\n20000102T0300Z" node "20000102T0300Z/nip" "nip\n20000102T0300Z" node "20000102T0300Z/qux" "qux\n20000102T0300Z" node "20000102T0300Z/wibble" "wibble\n20000102T0300Z" node "20000102T0330Z/quux" "quux\n20000102T0330Z" node "20000102T0400Z/nip" "nip\n20000102T0400Z" node "20000102T0400Z/wibble" "wibble\n20000102T0400Z" node "20000102T0415Z/bob" "bob\n20000102T0415Z" node "20000102T0415Z/toot" "toot\n20000102T0415Z" node "20000102T0430Z/quux" "quux\n20000102T0430Z" node "20000102T0500Z/nip" "nip\n20000102T0500Z" node "20000102T0500Z/wibble" "wibble\n20000102T0500Z" node "20000102T0515Z/bob" "bob\n20000102T0515Z" node "20000102T0515Z/toot" "toot\n20000102T0515Z" node "20000102T0530Z/quux" "quux\n20000102T0530Z" node "20000102T0600Z/dibble" "dibble\n20000102T0600Z" node "20000102T0600Z/qux" "qux\n20000102T0600Z" node "20000102T0630Z/quux" "quux\n20000102T0630Z" node "20000102T0700Z/nip" "nip\n20000102T0700Z" node "20000102T0700Z/wibble" "wibble\n20000102T0700Z" node "20000102T0715Z/bob" "bob\n20000102T0715Z" node "20000102T0715Z/toot" "toot\n20000102T0715Z" node "20000102T0730Z/quux" "quux\n20000102T0730Z" node "20000102T0800Z/nip" "nip\n20000102T0800Z" node "20000102T0800Z/wibble" "wibble\n20000102T0800Z" node "20000102T0815Z/bob" "bob\n20000102T0815Z" node "20000102T0815Z/toot" "toot\n20000102T0815Z" node "20000102T0830Z/quux" "quux\n20000102T0830Z" node "20000102T0900Z/baz" "baz\n20000102T0900Z" node "20000102T0900Z/nip" "nip\n20000102T0900Z" node "20000102T0900Z/qux" "qux\n20000102T0900Z" node "20000102T0900Z/wibble" "wibble\n20000102T0900Z" node "20000102T0930Z/quux" "quux\n20000102T0930Z" node "20000102T1000Z/nip" "nip\n20000102T1000Z" node "20000102T1000Z/wibble" "wibble\n20000102T1000Z" node "20000102T1015Z/bob" "bob\n20000102T1015Z" node "20000102T1015Z/toot" "toot\n20000102T1015Z" node "20000102T1030Z/quux" "quux\n20000102T1030Z" node "20000102T1100Z/nip" "nip\n20000102T1100Z" node "20000102T1100Z/wibble" "wibble\n20000102T1100Z" node "20000102T1115Z/bob" "bob\n20000102T1115Z" node "20000102T1115Z/toot" "toot\n20000102T1115Z" node "20000102T1130Z/quux" "quux\n20000102T1130Z" node "20000102T1200Z/pub" "pub\n20000102T1200Z" node "20000102T1200Z/qux" "qux\n20000102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/exclusions_advanced/reference.log0000664000175000017500000001535315202510242027246 0ustar alastairalastairInitial point: 20000101T0000Z Final point: 20000102T1200Z 20000101T0000Z/start -triggered off [] 20000101T0000Z/dibble -triggered off [] 20000101T0100Z/nip -triggered off [] 20000101T0000Z/qux -triggered off [] 20000101T0030Z/quux -triggered off [] 20000101T0100Z/wibble -triggered off [] 20000101T0000Z/foo -triggered off ['20000101T0000Z/start'] 20000101T0200Z/nip -triggered off [] 20000101T0115Z/bob -triggered off [] 20000101T0130Z/quux -triggered off [] 20000101T0200Z/wibble -triggered off [] 20000101T0115Z/toot -triggered off [] 20000101T0300Z/nip -triggered off [] 20000101T0230Z/quux -triggered off [] 20000101T0300Z/wibble -triggered off [] 20000101T0215Z/bob -triggered off [] 20000101T0230Z/dibble -triggered off [] 20000101T0215Z/toot -triggered off [] 20000101T0300Z/qux -triggered off [] 20000101T0300Z/baz -triggered off [] 20000101T0300Z/dibble -triggered off [] 20000101T0400Z/nip -triggered off [] 20000101T0400Z/wibble -triggered off [] 20000101T0330Z/quux -triggered off [] 20000101T0415Z/bob -triggered off [] 20000101T0415Z/toot -triggered off [] 20000101T0430Z/quux -triggered off [] 20000101T0500Z/nip -triggered off [] 20000101T0500Z/wibble -triggered off [] 20000101T0515Z/bob -triggered off [] 20000101T0515Z/toot -triggered off [] 20000101T0530Z/quux -triggered off [] 20000101T0600Z/dibble -triggered off [] 20000101T0600Z/qux -triggered off [] 20000101T0630Z/quux -triggered off [] 20000101T0715Z/bob -triggered off [] 20000101T0700Z/nip -triggered off [] 20000101T0715Z/toot -triggered off [] 20000101T0700Z/wibble -triggered off [] 20000101T0730Z/quux -triggered off [] 20000101T0815Z/bob -triggered off [] 20000101T0815Z/toot -triggered off [] 20000101T0800Z/wibble -triggered off [] 20000101T0800Z/nip -triggered off [] 20000101T0830Z/quux -triggered off [] 20000101T0900Z/wibble -triggered off [] 20000101T0900Z/baz -triggered off [] 20000101T0900Z/nip -triggered off [] 20000101T0900Z/qux -triggered off [] 20000101T0930Z/quux -triggered off [] 20000101T1000Z/nip -triggered off [] 20000101T1000Z/wibble -triggered off [] 20000101T1015Z/bob -triggered off [] 20000101T1015Z/toot -triggered off [] 20000101T1030Z/quux -triggered off [] 20000101T1100Z/wibble -triggered off [] 20000101T1100Z/nip -triggered off [] 20000101T1115Z/bob -triggered off [] 20000101T1115Z/toot -triggered off [] 20000101T1130Z/quux -triggered off [] 20000101T1200Z/qux -triggered off [] 20000101T1230Z/quux -triggered off [] 20000101T1300Z/nip -triggered off [] 20000101T1300Z/wibble -triggered off [] 20000101T1315Z/bob -triggered off [] 20000101T1315Z/toot -triggered off [] 20000101T1330Z/quux -triggered off [] 20000101T1400Z/wibble -triggered off [] 20000101T1400Z/nip -triggered off [] 20000101T1415Z/bob -triggered off [] 20000101T1415Z/toot -triggered off [] 20000101T1430Z/quux -triggered off [] 20000101T1500Z/nip -triggered off [] 20000101T1500Z/wibble -triggered off [] 20000101T1500Z/baz -triggered off [] 20000101T1500Z/dibble -triggered off [] 20000101T1500Z/qux -triggered off [] 20000101T1530Z/quux -triggered off [] 20000101T1600Z/nip -triggered off [] 20000101T1600Z/wibble -triggered off [] 20000101T1615Z/toot -triggered off [] 20000101T1615Z/bob -triggered off [] 20000101T1630Z/quux -triggered off [] 20000101T1700Z/nip -triggered off [] 20000101T1700Z/wibble -triggered off [] 20000101T1715Z/bob -triggered off [] 20000101T1715Z/toot -triggered off [] 20000101T1730Z/quux -triggered off [] 20000101T1800Z/dibble -triggered off [] 20000101T1800Z/qux -triggered off [] 20000101T1830Z/quux -triggered off [] 20000101T1900Z/nip -triggered off [] 20000101T1900Z/wibble -triggered off [] 20000101T1915Z/bob -triggered off [] 20000101T1915Z/toot -triggered off [] 20000101T1930Z/quux -triggered off [] 20000101T1945Z/dibble -triggered off [] 20000101T2000Z/wibble -triggered off [] 20000101T2000Z/nip -triggered off [] 20000101T2015Z/bob -triggered off [] 20000101T2015Z/toot -triggered off [] 20000101T2030Z/quux -triggered off [] 20000101T2100Z/wibble -triggered off [] 20000101T2100Z/qux -triggered off [] 20000101T2100Z/baz -triggered off [] 20000101T2100Z/dibble -triggered off [] 20000101T2100Z/nip -triggered off [] 20000101T2130Z/quux -triggered off [] 20000101T2200Z/nip -triggered off [] 20000101T2200Z/wibble -triggered off [] 20000101T2215Z/bob -triggered off [] 20000101T2215Z/toot -triggered off [] 20000101T2230Z/quux -triggered off [] 20000101T2300Z/nip -triggered off [] 20000101T2300Z/wibble -triggered off [] 20000101T2315Z/toot -triggered off [] 20000101T2315Z/bob -triggered off [] 20000101T2330Z/quux -triggered off [] 20000102T0000Z/qux -triggered off [] 20000102T0000Z/dibble -triggered off [] 20000102T0000Z/bar -triggered off [] 20000102T0030Z/quux -triggered off [] 20000102T0100Z/nip -triggered off [] 20000102T0100Z/wibble -triggered off [] 20000102T0115Z/bob -triggered off [] 20000102T0115Z/toot -triggered off [] 20000102T0130Z/quux -triggered off [] 20000102T0200Z/nip -triggered off [] 20000102T0200Z/wibble -triggered off [] 20000102T0215Z/toot -triggered off [] 20000102T0215Z/bob -triggered off [] 20000102T0230Z/dibble -triggered off [] 20000102T0230Z/quux -triggered off [] 20000102T0300Z/baz -triggered off [] 20000102T0300Z/qux -triggered off [] 20000102T0300Z/nip -triggered off [] 20000102T0300Z/wibble -triggered off [] 20000102T0300Z/dibble -triggered off [] 20000102T0330Z/quux -triggered off [] 20000102T0400Z/wibble -triggered off [] 20000102T0400Z/nip -triggered off [] 20000102T0415Z/bob -triggered off [] 20000102T0430Z/quux -triggered off [] 20000102T0415Z/toot -triggered off [] 20000102T0500Z/nip -triggered off [] 20000102T0500Z/wibble -triggered off [] 20000102T0515Z/toot -triggered off [] 20000102T0515Z/bob -triggered off [] 20000102T0530Z/quux -triggered off [] 20000102T0600Z/qux -triggered off [] 20000102T0600Z/dibble -triggered off [] 20000102T0630Z/quux -triggered off [] 20000102T0700Z/nip -triggered off [] 20000102T0700Z/wibble -triggered off [] 20000102T0715Z/bob -triggered off [] 20000102T0715Z/toot -triggered off [] 20000102T0800Z/wibble -triggered off [] 20000102T0800Z/nip -triggered off [] 20000102T0730Z/quux -triggered off [] 20000102T0815Z/toot -triggered off [] 20000102T0815Z/bob -triggered off [] 20000102T0900Z/nip -triggered off [] 20000102T0830Z/quux -triggered off [] 20000102T0900Z/baz -triggered off [] 20000102T0900Z/wibble -triggered off [] 20000102T0900Z/qux -triggered off [] 20000102T0930Z/quux -triggered off [] 20000102T1000Z/wibble -triggered off [] 20000102T1000Z/nip -triggered off [] 20000102T1015Z/toot -triggered off [] 20000102T1015Z/bob -triggered off [] 20000102T1030Z/quux -triggered off [] 20000102T1115Z/toot -triggered off [] 20000102T1115Z/bob -triggered off [] 20000102T1100Z/nip -triggered off [] 20000102T1200Z/pub -triggered off [] 20000102T1100Z/wibble -triggered off [] 20000102T1130Z/quux -triggered off [] 20000102T1200Z/qux -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/exclusions_advanced/flow.cylc0000664000175000017500000000210015202510242026412 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20000101T00Z final cycle point = 20000102T12Z [[graph]] R1/^ = start => foo # Don't run at the initial cycle point # Also test whitespace tolerance T00 ! ^ = bar # Run 3-hourly but not 6 hourly. PT3H!PT6H = baz # Run 4 hourly but not at 0800 from ICP PT3H!T08 = qux # Run hourly but not at 00:00, 06:00, 12:00, 18:00 T-00!(T00, T06, T12, T18) = nip # Run half-hourly but not on the hour from ICP PT30M!T-00 = quux # Run hourly on the hour, but not 6 hourly on the hour T-00!PT6H = wibble # Run hourly at 15 minutes past except every 3rd hour T-15!T-15/PT3H = bob # Run hourly at 15 minutes past except every 3rd hour # Implicit start point T-15!PT3H = toot # Stacked sequences T0230, PT3H! (T09, T12 ) , T1945 = dibble R1/$ = pub [runtime] [[root]] script = echo success cylc-flow-8.6.4/tests/functional/cyclers/r1_middle/0000775000175000017500000000000015202510242022415 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_middle/graph.plain.ref0000664000175000017500000000023215202510242025313 0ustar alastairalastairedge "20140101T0010Z/foo" "20140101T0010Z/bar" graph node "20140101T0010Z/bar" "bar\n20140101T0010Z" node "20140101T0010Z/foo" "foo\n20140101T0010Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_middle/reference.log0000664000175000017500000000021715202510242025056 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140101T0010Z/foo -triggered off [] 20140101T0010Z/bar -triggered off ['20140101T0010Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/r1_middle/flow.cylc0000664000175000017500000000036015202510242024237 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R1/+PT10M = "foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/r1_initial/0000775000175000017500000000000015202510242022610 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_initial/graph.plain.ref0000664000175000017500000000070615202510242025514 0ustar alastairalastairedge "20140101T0000Z/cold_foo" "20140101T0000Z/foo" edge "20140101T0000Z/foo" "20140101T1200Z/foo" edge "20140101T1200Z/foo" "20140102T0000Z/foo" edge "20140102T0000Z/foo" "20140102T1200Z/foo" graph node "20140101T0000Z/cold_foo" "cold_foo\n20140101T0000Z" node "20140101T0000Z/foo" "foo\n20140101T0000Z" node "20140101T1200Z/foo" "foo\n20140101T1200Z" node "20140102T0000Z/foo" "foo\n20140102T0000Z" node "20140102T1200Z/foo" "foo\n20140102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_initial/reference.log0000664000175000017500000000053215202510242025251 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140101T0000Z/cold_foo -triggered off [] 20140101T0000Z/foo -triggered off ['20131231T1200Z/foo', '20140101T0000Z/cold_foo'] 20140101T1200Z/foo -triggered off ['20140101T0000Z/foo'] 20140102T0000Z/foo -triggered off ['20140101T1200Z/foo'] 20140102T1200Z/foo -triggered off ['20140102T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/r1_initial/flow.cylc0000664000175000017500000000042315202510242024432 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R1 = "cold_foo => foo" PT12H = "foo[-PT12H] => foo" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/integer1/0000775000175000017500000000000015202510242022273 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/integer1/graph.plain.ref0000664000175000017500000000424315202510242025177 0ustar alastairalastairedge "1/foo" "1/bar" edge "1/foo" "4/foo" edge "1/foo" "4/on_toast" edge "4/foo" "4/bar" edge "4/foo" "7/foo" edge "4/foo" "7/on_toast" edge "4/foo" "7/qux" edge "7/foo" "7/bar" edge "7/foo" "10/foo" edge "7/foo" "10/on_toast" edge "10/foo" "10/bar" edge "10/foo" "13/foo" edge "10/foo" "13/on_toast" edge "10/foo" "13/qux" edge "13/foo" "13/bar" edge "13/foo" "16/foo" edge "13/foo" "16/on_toast" edge "16/foo" "16/bar" edge "1/seq" "1/foo" edge "4/seq" "4/foo" edge "7/seq" "7/foo" edge "10/seq" "10/foo" edge "13/seq" "13/foo" edge "16/seq" "16/foo" edge "1/wibble" "7/wobble" edge "7/wobble" "16/wubble" edge "2/woo" "1/bar" edge "2/woo" "1/foo" edge "3/woo" "4/foo" edge "5/woo" "4/bar" edge "5/woo" "4/foo" edge "6/woo" "7/foo" edge "8/woo" "7/bar" edge "8/woo" "7/foo" edge "9/woo" "10/foo" edge "11/woo" "10/bar" edge "11/woo" "10/foo" edge "12/woo" "13/foo" edge "14/woo" "13/bar" edge "14/woo" "13/foo" edge "15/woo" "16/foo" edge "17/woo" "16/bar" edge "17/woo" "16/foo" graph node "1/bar" "bar\n1" node "4/bar" "bar\n4" node "7/bar" "bar\n7" node "10/bar" "bar\n10" node "13/bar" "bar\n13" node "16/bar" "bar\n16" node "16/baz" "baz\n16" node "1/foo" "foo\n1" node "4/foo" "foo\n4" node "7/foo" "foo\n7" node "10/foo" "foo\n10" node "13/foo" "foo\n13" node "16/foo" "foo\n16" node "1/nang" "nang\n1" node "4/ning" "ning\n4" node "12/ning" "ning\n12" node "16/ning" "ning\n16" node "2/nong" "nong\n2" node "8/nong" "nong\n8" node "1/on_toast" "on_toast\n1" node "4/on_toast" "on_toast\n4" node "7/on_toast" "on_toast\n7" node "10/on_toast" "on_toast\n10" node "13/on_toast" "on_toast\n13" node "16/on_toast" "on_toast\n16" node "8/quux" "quux\n8" node "16/quux" "quux\n16" node "7/qux" "qux\n7" node "13/qux" "qux\n13" node "1/seq" "seq\n1" node "4/seq" "seq\n4" node "7/seq" "seq\n7" node "10/seq" "seq\n10" node "13/seq" "seq\n13" node "16/seq" "seq\n16" node "1/wibble" "wibble\n1" node "7/wobble" "wobble\n7" node "2/woo" "woo\n2" node "3/woo" "woo\n3" node "5/woo" "woo\n5" node "6/woo" "woo\n6" node "8/woo" "woo\n8" node "9/woo" "woo\n9" node "11/woo" "woo\n11" node "12/woo" "woo\n12" node "14/woo" "woo\n14" node "15/woo" "woo\n15" node "17/woo" "woo\n17" node "16/wubble" "wubble\n16" stop cylc-flow-8.6.4/tests/functional/cyclers/integer1/reference.log0000664000175000017500000000305615202510242024740 0ustar alastairalastair10/bar -triggered off ['10/foo', '11/woo'] 13/bar -triggered off ['13/foo', '14/woo'] 1/bar -triggered off ['1/foo', '2/woo'] 4/bar -triggered off ['4/foo', '5/woo'] 7/bar -triggered off ['7/foo', '8/woo'] 16/baz -triggered off [] Final point: 16 10/foo -triggered off ['10/seq', '11/woo', '7/foo', '9/woo'] 13/foo -triggered off ['10/foo', '12/woo', '13/seq', '14/woo'] 1/foo -triggered off ['-2/foo', '0/woo', '1/seq', '2/woo'] 4/foo -triggered off ['1/foo', '3/woo', '4/seq', '5/woo'] 7/foo -triggered off ['4/foo', '6/woo', '7/seq', '8/woo'] Initial point: 1 1/nang -triggered off [] 12/ning -triggered off [] 16/ning -triggered off [] 4/ning -triggered off [] 2/nong -triggered off [] 8/nong -triggered off [] 10/on_toast -triggered off ['7/foo'] 13/on_toast -triggered off ['10/foo'] 16/on_toast -triggered off ['13/foo'] 1/on_toast -triggered off ['-2/foo'] 4/on_toast -triggered off ['1/foo'] 7/on_toast -triggered off ['4/foo'] 16/quux -triggered off [] 8/quux -triggered off [] 13/qux -triggered off ['10/foo'] 7/qux -triggered off ['4/foo'] 10/seq -triggered off ['7/seq'] 13/seq -triggered off ['10/seq'] 16/seq -triggered off ['13/seq'] 1/seq -triggered off [] 4/seq -triggered off ['1/seq'] 7/seq -triggered off ['4/seq'] 1/wibble -triggered off [] 7/wobble -triggered off ['1/wibble'] 11/woo -triggered off [] 12/woo -triggered off [] 14/woo -triggered off [] 15/woo -triggered off [] 2/woo -triggered off [] 3/woo -triggered off [] 5/woo -triggered off [] 6/woo -triggered off [] 8/woo -triggered off [] 9/woo -triggered off [] 16/wubble -triggered off ['7/wobble'] cylc-flow-8.6.4/tests/functional/cyclers/integer1/flow.cylc0000664000175000017500000000230615202510242024117 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1 final cycle point = +P15 # = 16 runahead limit = P11 cycling mode = integer [[special tasks]] sequential = seq [[graph]] R1 = wibble R1/+P6 = wibble[^] => wobble R1/P0 = wobble[^+P6] => wubble P3 = """ seq => foo # sequential task foo[-P3] => foo # prev instance trigger foo => bar # plain trigger woo[+P1] => foo # prev cycle woo[+P1] => bar woo[-P1] => foo # next cycle foo[-P3]:out1 => on_toast # message outputs """ +P1/P3 = woo R/+P2/P3 = woo R/7/P6 = foo[-P3] => qux # every second cycle R1/$ = baz R/P4!8 = ning R/P4!(4,12) = quux # Multiple exclusion points R1/^ = nang R/+P1/P6!14 = nong [runtime] [[root]] script = true [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" "the cheese is ready" """ [[[outputs]]] out1 = "the cheese is ready" cylc-flow-8.6.4/tests/functional/cyclers/14-r5_initial.t0000775000175000017500000000201615202510242023225 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140101T18" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/monthly/0000775000175000017500000000000015202510242022247 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/monthly/graph.plain.ref0000664000175000017500000000422515202510242025153 0ustar alastairalastairedge "20000131T0100Z/foo" "20000131T0100Z/bar" edge "20000229T0100Z/foo" "20000229T0100Z/bar" edge "20000229T0100Z/foo" "20000329T0100Z/foo" edge "20000329T0100Z/foo" "20000329T0100Z/bar" edge "20000329T0100Z/foo" "20000429T0100Z/foo" edge "20000429T0100Z/foo" "20000429T0100Z/bar" edge "20000429T0100Z/foo" "20000529T0100Z/foo" edge "20000529T0100Z/foo" "20000529T0100Z/bar" edge "20000529T0100Z/foo" "20000629T0100Z/foo" edge "20000629T0100Z/foo" "20000629T0100Z/bar" edge "20000629T0100Z/foo" "20000729T0100Z/foo" edge "20000729T0100Z/foo" "20000729T0100Z/bar" edge "20000729T0100Z/foo" "20000829T0100Z/foo" edge "20000829T0100Z/foo" "20000829T0100Z/bar" edge "20000829T0100Z/foo" "20000929T0100Z/foo" edge "20000929T0100Z/foo" "20000929T0100Z/bar" edge "20000929T0100Z/foo" "20001029T0100Z/foo" edge "20001029T0100Z/foo" "20001029T0100Z/bar" edge "20001029T0100Z/foo" "20001129T0100Z/foo" edge "20001129T0100Z/foo" "20001129T0100Z/bar" edge "20001129T0100Z/foo" "20001229T0100Z/foo" edge "20001229T0100Z/foo" "20001229T0100Z/bar" graph node "20000131T0100Z/bar" "bar\n20000131T0100Z" node "20000131T0100Z/foo" "foo\n20000131T0100Z" node "20000229T0100Z/bar" "bar\n20000229T0100Z" node "20000229T0100Z/foo" "foo\n20000229T0100Z" node "20000329T0100Z/bar" "bar\n20000329T0100Z" node "20000329T0100Z/foo" "foo\n20000329T0100Z" node "20000429T0100Z/bar" "bar\n20000429T0100Z" node "20000429T0100Z/foo" "foo\n20000429T0100Z" node "20000529T0100Z/bar" "bar\n20000529T0100Z" node "20000529T0100Z/foo" "foo\n20000529T0100Z" node "20000629T0100Z/bar" "bar\n20000629T0100Z" node "20000629T0100Z/foo" "foo\n20000629T0100Z" node "20000729T0100Z/bar" "bar\n20000729T0100Z" node "20000729T0100Z/foo" "foo\n20000729T0100Z" node "20000829T0100Z/bar" "bar\n20000829T0100Z" node "20000829T0100Z/foo" "foo\n20000829T0100Z" node "20000929T0100Z/bar" "bar\n20000929T0100Z" node "20000929T0100Z/foo" "foo\n20000929T0100Z" node "20001029T0100Z/bar" "bar\n20001029T0100Z" node "20001029T0100Z/foo" "foo\n20001029T0100Z" node "20001129T0100Z/bar" "bar\n20001129T0100Z" node "20001129T0100Z/foo" "foo\n20001129T0100Z" node "20001229T0100Z/bar" "bar\n20001229T0100Z" node "20001229T0100Z/foo" "foo\n20001229T0100Z" stop cylc-flow-8.6.4/tests/functional/cyclers/monthly/reference.log0000664000175000017500000000260715202510242024715 0ustar alastairalastairInitial point: 20000131T0100 Final point: 2001 20000131T0100Z/foo -triggered off ['19991231T0100Z/foo'] 20000229T0100Z/foo -triggered off ['20000129T0100Z/foo'] 20000131T0100Z/bar -triggered off ['20000131T0100Z/foo'] 20000329T0100Z/foo -triggered off ['20000229T0100Z/foo'] 20000229T0100Z/bar -triggered off ['20000229T0100Z/foo'] 20000429T0100Z/foo -triggered off ['20000329T0100Z/foo'] 20000329T0100Z/bar -triggered off ['20000329T0100Z/foo'] 20000529T0100Z/foo -triggered off ['20000429T0100Z/foo'] 20000429T0100Z/bar -triggered off ['20000429T0100Z/foo'] 20000629T0100Z/foo -triggered off ['20000529T0100Z/foo'] 20000529T0100Z/bar -triggered off ['20000529T0100Z/foo'] 20000729T0100Z/foo -triggered off ['20000629T0100Z/foo'] 20000629T0100Z/bar -triggered off ['20000629T0100Z/foo'] 20000829T0100Z/foo -triggered off ['20000729T0100Z/foo'] 20000729T0100Z/bar -triggered off ['20000729T0100Z/foo'] 20000929T0100Z/foo -triggered off ['20000829T0100Z/foo'] 20000829T0100Z/bar -triggered off ['20000829T0100Z/foo'] 20001029T0100Z/foo -triggered off ['20000929T0100Z/foo'] 20000929T0100Z/bar -triggered off ['20000929T0100Z/foo'] 20001129T0100Z/foo -triggered off ['20001029T0100Z/foo'] 20001029T0100Z/bar -triggered off ['20001029T0100Z/foo'] 20001229T0100Z/foo -triggered off ['20001129T0100Z/foo'] 20001129T0100Z/bar -triggered off ['20001129T0100Z/foo'] 20001229T0100Z/bar -triggered off ['20001229T0100Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/monthly/flow.cylc0000664000175000017500000000036615202510242024077 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20000131T0100Z final cycle point = 2001 [[graph]] P1M = "foo[-P1M] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/34-r1_initial_immortal.t0000775000175000017500000000202115202510242025123 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140110T0000Z" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/offset_final/0000775000175000017500000000000015202510242023214 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/offset_final/graph.plain.ref0000664000175000017500000000023215202510242026112 0ustar alastairalastairedge "20140101T1200Z/bar" "20140101T1200Z/baz" graph node "20140101T1200Z/bar" "bar\n20140101T1200Z" node "20140101T1200Z/baz" "baz\n20140101T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/offset_final/reference.log0000664000175000017500000000021715202510242025655 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140101T1200Z/bar -triggered off [] 20140101T1200Z/baz -triggered off ['20140101T1200Z/bar'] cylc-flow-8.6.4/tests/functional/cyclers/offset_final/flow.cylc0000664000175000017500000000035715202510242025044 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R1//-P1D = "bar => baz" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/14-r5_initial-integer.t0000775000175000017500000000277415202510242024673 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test triggering from unbounded to bound sequences - integer cycling. . "$(dirname "$0")/test_header" set_test_number 3 CHOSEN_WORKFLOW="$(basename "$0" | sed "s/^[0-9]*-\(.*\)\.t/\1/g")" install_workflow "${TEST_NAME_BASE}" "${CHOSEN_WORKFLOW}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" 1 7 cmp_ok "${WORKFLOW_NAME}.graph.plain" "$TEST_SOURCE_DIR/$CHOSEN_WORKFLOW/graph.plain.ref" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/cyclers/daily/0000775000175000017500000000000015202510242021657 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/daily/graph.plain.ref0000664000175000017500000000102615202510242024557 0ustar alastairalastairedge "20131231T2300Z/foo" "20131231T2300Z/bar" edge "20131231T2300Z/foo" "20140101T2300Z/foo" edge "20140101T2300Z/foo" "20140101T2300Z/bar" edge "20140101T2300Z/foo" "20140102T2300Z/foo" edge "20140102T2300Z/foo" "20140102T2300Z/bar" graph node "20131231T2300Z/bar" "bar\n20131231T2300Z" node "20131231T2300Z/foo" "foo\n20131231T2300Z" node "20140101T2300Z/bar" "bar\n20140101T2300Z" node "20140101T2300Z/foo" "foo\n20140101T2300Z" node "20140102T2300Z/bar" "bar\n20140102T2300Z" node "20140102T2300Z/foo" "foo\n20140102T2300Z" stop cylc-flow-8.6.4/tests/functional/cyclers/daily/reference.log0000664000175000017500000000061615202510242024323 0ustar alastairalastairInitial point: 20131231T2300 Final point: 20140103T0000 20131231T2300Z/foo -triggered off ['20131230T2300Z/foo'] 20131231T2300Z/bar -triggered off ['20131231T2300Z/foo'] 20140101T2300Z/foo -triggered off ['20131231T2300Z/foo'] 20140101T2300Z/bar -triggered off ['20140101T2300Z/foo'] 20140102T2300Z/foo -triggered off ['20140101T2300Z/foo'] 20140102T2300Z/bar -triggered off ['20140102T2300Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/daily/flow.cylc0000664000175000017500000000040615202510242023502 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20131231T2300 final cycle point = 20140103T0000 [[graph]] P1D = "foo[-P1D] => foo => bar" [runtime] [[root]] script = touch typing cylc-flow-8.6.4/tests/functional/cyclers/aeon/0000775000175000017500000000000015202510242021477 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/aeon/graph.plain.ref0000664000175000017500000000360515202510242024404 0ustar alastairalastairedge "-138000000000229T0530Z/big_bang" "-138000000000229T0530Z/count_the_aeons" graph node "+002000000000229T0530Z/count_the_aeons" "count_the_aeons\n+002000000000229T0530Z" node "+012000000000229T0530Z/count_the_aeons" "count_the_aeons\n+012000000000229T0530Z" node "+022000000000229T0530Z/count_the_aeons" "count_the_aeons\n+022000000000229T0530Z" node "+032000000000229T0530Z/count_the_aeons" "count_the_aeons\n+032000000000229T0530Z" node "+042000000000229T0530Z/count_the_aeons" "count_the_aeons\n+042000000000229T0530Z" node "+052000000000229T0530Z/count_the_aeons" "count_the_aeons\n+052000000000229T0530Z" node "-008000000000229T0530Z/count_the_aeons" "count_the_aeons\n-008000000000229T0530Z" node "-018000000000229T0530Z/count_the_aeons" "count_the_aeons\n-018000000000229T0530Z" node "-028000000000229T0530Z/count_the_aeons" "count_the_aeons\n-028000000000229T0530Z" node "-038000000000229T0530Z/count_the_aeons" "count_the_aeons\n-038000000000229T0530Z" node "-048000000000229T0530Z/count_the_aeons" "count_the_aeons\n-048000000000229T0530Z" node "-058000000000229T0530Z/count_the_aeons" "count_the_aeons\n-058000000000229T0530Z" node "-068000000000229T0530Z/count_the_aeons" "count_the_aeons\n-068000000000229T0530Z" node "-078000000000229T0530Z/count_the_aeons" "count_the_aeons\n-078000000000229T0530Z" node "-088000000000229T0530Z/count_the_aeons" "count_the_aeons\n-088000000000229T0530Z" node "-098000000000229T0530Z/count_the_aeons" "count_the_aeons\n-098000000000229T0530Z" node "-108000000000229T0530Z/count_the_aeons" "count_the_aeons\n-108000000000229T0530Z" node "-118000000000229T0530Z/count_the_aeons" "count_the_aeons\n-118000000000229T0530Z" node "-128000000000229T0530Z/count_the_aeons" "count_the_aeons\n-128000000000229T0530Z" node "-138000000000229T0530Z/big_bang" "big_bang\n-138000000000229T0530Z" node "-138000000000229T0530Z/count_the_aeons" "count_the_aeons\n-138000000000229T0530Z" stop cylc-flow-8.6.4/tests/functional/cyclers/aeon/reference.log0000664000175000017500000000242115202510242024137 0ustar alastairalastairInitial point: -138000000000229T0530Z Final point: +054000000001231T2359Z -138000000000229T0530Z/big_bang -triggered off [] -138000000000229T0530Z/count_the_aeons -triggered off ['-138000000000229T0530Z/big_bang'] -128000000000229T0530Z/count_the_aeons -triggered off [] -118000000000229T0530Z/count_the_aeons -triggered off [] -108000000000229T0530Z/count_the_aeons -triggered off [] -098000000000229T0530Z/count_the_aeons -triggered off [] -088000000000229T0530Z/count_the_aeons -triggered off [] -078000000000229T0530Z/count_the_aeons -triggered off [] -068000000000229T0530Z/count_the_aeons -triggered off [] -058000000000229T0530Z/count_the_aeons -triggered off [] -048000000000229T0530Z/count_the_aeons -triggered off [] -038000000000229T0530Z/count_the_aeons -triggered off [] -028000000000229T0530Z/count_the_aeons -triggered off [] -018000000000229T0530Z/count_the_aeons -triggered off [] -008000000000229T0530Z/count_the_aeons -triggered off [] +002000000000229T0530Z/count_the_aeons -triggered off [] +012000000000229T0530Z/count_the_aeons -triggered off [] +022000000000229T0530Z/count_the_aeons -triggered off [] +032000000000229T0530Z/count_the_aeons -triggered off [] +042000000000229T0530Z/count_the_aeons -triggered off [] +052000000000229T0530Z/count_the_aeons -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/aeon/flow.cylc0000664000175000017500000000064515202510242023327 0ustar alastairalastair[scheduler] cycle point num expanded year digits = 7 UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = -13800000000-02-29T05:30 # Big Bang final cycle point = +05400000000-12-31T23:59 # Sun leaves main sequence [[graph]] R1 = "big_bang => count_the_aeons" +P1000000000Y/P1000000000Y = "count_the_aeons" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/multihourly/0000775000175000017500000000000015202510242023152 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multihourly/graph.plain.ref0000664000175000017500000001136515202510242026061 0ustar alastairalastairedge "20000131T1400+13/baz" "20000131T1400+13/qux" edge "20000131T1400+13/baz" "20000131T2000+13/baz" edge "20000131T1400+13/foo" "20000131T1400+13/bar" edge "20000131T1400+13/foo" "20000131T1700+13/foo" edge "20000131T1700+13/foo" "20000131T1700+13/bar" edge "20000131T1700+13/foo" "20000131T2000+13/foo" edge "20000131T2000+13/baz" "20000131T2000+13/qux" edge "20000131T2000+13/baz" "20000201T0200+13/baz" edge "20000131T2000+13/foo" "20000131T2000+13/bar" edge "20000131T2000+13/foo" "20000131T2300+13/foo" edge "20000131T2300+13/foo" "20000131T2300+13/bar" edge "20000131T2300+13/foo" "20000201T0200+13/foo" edge "20000201T0200+13/baz" "20000201T0200+13/qux" edge "20000201T0200+13/baz" "20000201T0800+13/baz" edge "20000201T0200+13/foo" "20000201T0200+13/bar" edge "20000201T0200+13/foo" "20000201T0500+13/foo" edge "20000201T0500+13/foo" "20000201T0500+13/bar" edge "20000201T0500+13/foo" "20000201T0800+13/foo" edge "20000201T0800+13/baz" "20000201T0800+13/qux" edge "20000201T0800+13/baz" "20000201T1400+13/baz" edge "20000201T0800+13/foo" "20000201T0800+13/bar" edge "20000201T0800+13/foo" "20000201T1100+13/foo" edge "20000201T1100+13/foo" "20000201T1100+13/bar" edge "20000201T1100+13/foo" "20000201T1400+13/foo" edge "20000201T1400+13/baz" "20000201T1400+13/qux" edge "20000201T1400+13/baz" "20000201T2000+13/baz" edge "20000201T1400+13/foo" "20000201T1400+13/bar" edge "20000201T1400+13/foo" "20000201T1700+13/foo" edge "20000201T1700+13/foo" "20000201T1700+13/bar" edge "20000201T1700+13/foo" "20000201T2000+13/foo" edge "20000201T2000+13/baz" "20000201T2000+13/qux" edge "20000201T2000+13/baz" "20000202T0200+13/baz" edge "20000201T2000+13/foo" "20000201T2000+13/bar" edge "20000201T2000+13/foo" "20000201T2300+13/foo" edge "20000201T2300+13/foo" "20000201T2300+13/bar" edge "20000201T2300+13/foo" "20000202T0200+13/foo" edge "20000202T0200+13/baz" "20000202T0200+13/qux" edge "20000202T0200+13/baz" "20000202T0800+13/baz" edge "20000202T0200+13/foo" "20000202T0200+13/bar" edge "20000202T0200+13/foo" "20000202T0500+13/foo" edge "20000202T0500+13/foo" "20000202T0500+13/bar" edge "20000202T0500+13/foo" "20000202T0800+13/foo" edge "20000202T0800+13/baz" "20000202T0800+13/qux" edge "20000202T0800+13/foo" "20000202T0800+13/bar" edge "20000202T0800+13/foo" "20000202T1100+13/foo" edge "20000202T1100+13/foo" "20000202T1100+13/bar" graph node "20000131T1400+13/bar" "bar\n20000131T1400+13" node "20000131T1400+13/baz" "baz\n20000131T1400+13" node "20000131T1400+13/foo" "foo\n20000131T1400+13" node "20000131T1400+13/qux" "qux\n20000131T1400+13" node "20000131T1700+13/bar" "bar\n20000131T1700+13" node "20000131T1700+13/foo" "foo\n20000131T1700+13" node "20000131T2000+13/bar" "bar\n20000131T2000+13" node "20000131T2000+13/baz" "baz\n20000131T2000+13" node "20000131T2000+13/foo" "foo\n20000131T2000+13" node "20000131T2000+13/qux" "qux\n20000131T2000+13" node "20000131T2300+13/bar" "bar\n20000131T2300+13" node "20000131T2300+13/foo" "foo\n20000131T2300+13" node "20000201T0200+13/bar" "bar\n20000201T0200+13" node "20000201T0200+13/baz" "baz\n20000201T0200+13" node "20000201T0200+13/foo" "foo\n20000201T0200+13" node "20000201T0200+13/qux" "qux\n20000201T0200+13" node "20000201T0500+13/bar" "bar\n20000201T0500+13" node "20000201T0500+13/foo" "foo\n20000201T0500+13" node "20000201T0800+13/bar" "bar\n20000201T0800+13" node "20000201T0800+13/baz" "baz\n20000201T0800+13" node "20000201T0800+13/foo" "foo\n20000201T0800+13" node "20000201T0800+13/qux" "qux\n20000201T0800+13" node "20000201T1100+13/bar" "bar\n20000201T1100+13" node "20000201T1100+13/foo" "foo\n20000201T1100+13" node "20000201T1400+13/bar" "bar\n20000201T1400+13" node "20000201T1400+13/baz" "baz\n20000201T1400+13" node "20000201T1400+13/foo" "foo\n20000201T1400+13" node "20000201T1400+13/qux" "qux\n20000201T1400+13" node "20000201T1700+13/bar" "bar\n20000201T1700+13" node "20000201T1700+13/foo" "foo\n20000201T1700+13" node "20000201T2000+13/bar" "bar\n20000201T2000+13" node "20000201T2000+13/baz" "baz\n20000201T2000+13" node "20000201T2000+13/foo" "foo\n20000201T2000+13" node "20000201T2000+13/qux" "qux\n20000201T2000+13" node "20000201T2300+13/bar" "bar\n20000201T2300+13" node "20000201T2300+13/foo" "foo\n20000201T2300+13" node "20000202T0200+13/bar" "bar\n20000202T0200+13" node "20000202T0200+13/baz" "baz\n20000202T0200+13" node "20000202T0200+13/foo" "foo\n20000202T0200+13" node "20000202T0200+13/qux" "qux\n20000202T0200+13" node "20000202T0500+13/bar" "bar\n20000202T0500+13" node "20000202T0500+13/foo" "foo\n20000202T0500+13" node "20000202T0800+13/bar" "bar\n20000202T0800+13" node "20000202T0800+13/baz" "baz\n20000202T0800+13" node "20000202T0800+13/foo" "foo\n20000202T0800+13" node "20000202T0800+13/qux" "qux\n20000202T0800+13" node "20000202T1100+13/bar" "bar\n20000202T1100+13" node "20000202T1100+13/foo" "foo\n20000202T1100+13" stop cylc-flow-8.6.4/tests/functional/cyclers/multihourly/reference.log0000664000175000017500000000565615202510242025627 0ustar alastairalastairInitial point: 20000131T0100Z Final point: 20000202T0600+0600 20000131T1400+13/baz -triggered off ['20000131T0800+13/baz'] 20000131T1400+13/foo -triggered off ['20000131T1100+13/foo'] 20000131T1400+13/qux -triggered off ['20000131T1400+13/baz'] 20000131T2000+13/baz -triggered off ['20000131T1400+13/baz'] 20000131T1400+13/bar -triggered off ['20000131T1400+13/foo'] 20000131T1700+13/foo -triggered off ['20000131T1400+13/foo'] 20000131T2000+13/qux -triggered off ['20000131T2000+13/baz'] 20000201T0200+13/baz -triggered off ['20000131T2000+13/baz'] 20000131T1700+13/bar -triggered off ['20000131T1700+13/foo'] 20000131T2000+13/foo -triggered off ['20000131T1700+13/foo'] 20000201T0200+13/qux -triggered off ['20000201T0200+13/baz'] 20000201T0800+13/baz -triggered off ['20000201T0200+13/baz'] 20000131T2000+13/bar -triggered off ['20000131T2000+13/foo'] 20000131T2300+13/foo -triggered off ['20000131T2000+13/foo'] 20000201T0800+13/qux -triggered off ['20000201T0800+13/baz'] 20000131T2300+13/bar -triggered off ['20000131T2300+13/foo'] 20000201T0200+13/foo -triggered off ['20000131T2300+13/foo'] 20000201T1400+13/baz -triggered off ['20000201T0800+13/baz'] 20000201T0200+13/bar -triggered off ['20000201T0200+13/foo'] 20000201T0500+13/foo -triggered off ['20000201T0200+13/foo'] 20000201T1400+13/qux -triggered off ['20000201T1400+13/baz'] 20000201T0800+13/foo -triggered off ['20000201T0500+13/foo'] 20000201T0500+13/bar -triggered off ['20000201T0500+13/foo'] 20000201T1100+13/foo -triggered off ['20000201T0800+13/foo'] 20000201T0800+13/bar -triggered off ['20000201T0800+13/foo'] 20000201T2000+13/baz -triggered off ['20000201T1400+13/baz'] 20000201T1400+13/foo -triggered off ['20000201T1100+13/foo'] 20000201T1100+13/bar -triggered off ['20000201T1100+13/foo'] 20000201T2000+13/qux -triggered off ['20000201T2000+13/baz'] 20000201T1700+13/foo -triggered off ['20000201T1400+13/foo'] 20000201T1400+13/bar -triggered off ['20000201T1400+13/foo'] 20000202T0200+13/baz -triggered off ['20000201T2000+13/baz'] 20000201T2000+13/foo -triggered off ['20000201T1700+13/foo'] 20000201T1700+13/bar -triggered off ['20000201T1700+13/foo'] 20000202T0200+13/qux -triggered off ['20000202T0200+13/baz'] 20000202T0800+13/baz -triggered off ['20000202T0200+13/baz'] 20000201T2300+13/foo -triggered off ['20000201T2000+13/foo'] 20000201T2000+13/bar -triggered off ['20000201T2000+13/foo'] 20000202T0800+13/qux -triggered off ['20000202T0800+13/baz'] 20000201T2300+13/bar -triggered off ['20000201T2300+13/foo'] 20000202T0200+13/foo -triggered off ['20000201T2300+13/foo'] 20000202T0200+13/bar -triggered off ['20000202T0200+13/foo'] 20000202T0500+13/foo -triggered off ['20000202T0200+13/foo'] 20000202T0500+13/bar -triggered off ['20000202T0500+13/foo'] 20000202T0800+13/foo -triggered off ['20000202T0500+13/foo'] 20000202T0800+13/bar -triggered off ['20000202T0800+13/foo'] 20000202T1100+13/foo -triggered off ['20000202T0800+13/foo'] 20000202T1100+13/bar -triggered off ['20000202T1100+13/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multihourly/flow.cylc0000664000175000017500000000047415202510242025002 0ustar alastairalastair[scheduler] cycle point time zone = +13 allow implicit tasks = True [scheduling] initial cycle point = 20000131T0100Z final cycle point = 20000202T0600+0600 [[graph]] PT3H = "foo[-PT3H] => foo => bar" PT6H = "baz[-PT6H] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/monthly_complex/0000775000175000017500000000000015202510242023776 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/monthly_complex/reference.log0000664000175000017500000000224515202510242026442 0ustar alastairalastair20000430T0100Z/flub -triggered off [] 20000406T0000Z/foo -triggered off [] 20000331T0100Z/flob -triggered off ['20000430T0100Z/flub'] 20000401T0100Z/flob -triggered off ['20000331T0100Z/flob', '20000430T0100Z/flub'] 20000402T0100Z/flob -triggered off ['20000401T0100Z/flob', '20000430T0100Z/flub'] 20000403T0100Z/flob -triggered off ['20000402T0100Z/flob', '20000430T0100Z/flub'] 20000506T0000Z/foo -triggered off [] 20000508T1200Z/bar -triggered off ['20000506T0000Z/foo'] 20000606T0000Z/foo -triggered off [] 20000605T1200Z/bar -triggered off ['20000606T0000Z/foo'] 20000706T0000Z/foo -triggered off [] 20000703T1200Z/bar -triggered off ['20000706T0000Z/foo'] 20000806T0000Z/foo -triggered off [] 20000731T1200Z/bar -triggered off ['20000806T0000Z/foo'] 20000906T0000Z/foo -triggered off [] 20000828T1200Z/bar -triggered off ['20000906T0000Z/foo'] 20001006T0000Z/foo -triggered off [] 20000925T1200Z/bar -triggered off ['20001006T0000Z/foo'] 20001106T0000Z/foo -triggered off [] 20001023T1200Z/bar -triggered off ['20001106T0000Z/foo'] 20001206T0000Z/foo -triggered off [] 20001120T1200Z/bar -triggered off ['20001206T0000Z/foo'] 20001218T1200Z/bar -triggered off ['20001206T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/monthly_complex/flow.cylc0000664000175000017500000000055115202510242025622 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20000331T0100Z final cycle point = 2001 [[graph]] 20T-P2W/P1M = "foo" 30T06+P8DT6H/P3W = "foo[20T-P2W] => bar" R1/+P1M = "flub" R4//P1D = "flub[^+P1M] & flob[-P1D] => flob" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/33-integer1.t0000775000175000017500000000200015202510242022676 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="1" # shellcheck disable=SC2034 FINALCP="+P15" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/day_of_week/0000775000175000017500000000000015202510242023031 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/day_of_week/graph.plain.ref0000664000175000017500000000023315202510242025730 0ustar alastairalastairgraph node "20100104T0000Z/foo" "foo\n20100104T0000Z" node "20100111T0000Z/foo" "foo\n20100111T0000Z" node "20100118T0000Z/foo" "foo\n20100118T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/day_of_week/reference.log0000664000175000017500000000031615202510242025472 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100125T0000Z 20100104T0000Z/foo -triggered off [] 20100111T0000Z/foo -triggered off [] 20100118T0000Z/foo -triggered off [] 20100125T0000Z/foo -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/day_of_week/flow.cylc0000664000175000017500000000032215202510242024651 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T0000Z final cycle point = 20100125T0000Z [[graph]] R5/W-1/P1W = foo [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/cyclers/10-r1_final.t0000775000175000017500000000201615202510242022655 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/25-aeon.t0000775000175000017500000000205315202510242022113 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="-13800000000-02-29T05:30" # shellcheck disable=SC2034 FINALCP="+05400000000-12-31T23:59" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/rnone_reverse/0000775000175000017500000000000015202510242023431 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/rnone_reverse/graph.plain.ref0000664000175000017500000000115615202510242026335 0ustar alastairalastairedge "20150221T0000Z/foo" "20150221T0000Z/nonstop" edge "20150221T0600Z/foo" "20150221T0600Z/nonstop" edge "20150221T1200Z/foo" "20150221T1200Z/nonstop" edge "20150221T1800Z/foo" "20150221T1800Z/stop" graph node "20150221T0000Z/foo" "foo\n20150221T0000Z" node "20150221T0000Z/nonstop" "nonstop\n20150221T0000Z" node "20150221T0600Z/foo" "foo\n20150221T0600Z" node "20150221T0600Z/nonstop" "nonstop\n20150221T0600Z" node "20150221T1200Z/foo" "foo\n20150221T1200Z" node "20150221T1200Z/nonstop" "nonstop\n20150221T1200Z" node "20150221T1800Z/foo" "foo\n20150221T1800Z" node "20150221T1800Z/stop" "stop\n20150221T1800Z" stop cylc-flow-8.6.4/tests/functional/cyclers/rnone_reverse/reference.log0000664000175000017500000000067715202510242026104 0ustar alastairalastairInitial point: 20150221T0000Z Final point: 20150221T1800Z 20150221T0000Z/foo -triggered off [] 20150221T0600Z/foo -triggered off [] 20150221T1200Z/foo -triggered off [] 20150221T0000Z/nonstop -triggered off ['20150221T0000Z/foo'] 20150221T0600Z/nonstop -triggered off ['20150221T0600Z/foo'] 20150221T1200Z/nonstop -triggered off ['20150221T1200Z/foo'] 20150221T1800Z/foo -triggered off [] 20150221T1800Z/stop -triggered off ['20150221T1800Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/rnone_reverse/flow.cylc0000664000175000017500000000045415202510242025257 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2015-02-21T00 final cycle point = 2015-02-21T18 [[graph]] PT6H = foo R1/P0D = foo => stop R/PT6H/-PT6H = foo => nonstop [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/r1_final/0000775000175000017500000000000015202510242022250 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_final/graph.plain.ref0000664000175000017500000000025415202510242025152 0ustar alastairalastairedge "20140102T1200Z/foo" "20140102T1200Z/final_foo" graph node "20140102T1200Z/final_foo" "final_foo\n20140102T1200Z" node "20140102T1200Z/foo" "foo\n20140102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_final/reference.log0000664000175000017500000000022515202510242024710 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140102T1200Z/foo -triggered off [] 20140102T1200Z/final_foo -triggered off ['20140102T1200Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/r1_final/flow.cylc0000664000175000017500000000036315202510242024075 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R1/P0D = "foo => final_foo" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/00-daily.t0000775000175000017500000000202515202510242022263 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20131231T2300" # shellcheck disable=SC2034 FINALCP="20140103T0000" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/multiweekly/0000775000175000017500000000000015202510242023130 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/multiweekly/graph.plain.ref0000664000175000017500000000263515202510242026037 0ustar alastairalastairedge "09991230T0000Z/baz" "09991230T0000Z/qux" edge "09991230T0000Z/baz" "10000120T0000Z/baz" edge "09991230T0000Z/foo" "09991230T0000Z/bar" edge "09991230T0000Z/foo" "10000106T0000Z/foo" edge "10000106T0000Z/foo" "10000106T0000Z/bar" edge "10000106T0000Z/foo" "10000113T0000Z/foo" edge "10000113T0000Z/foo" "10000113T0000Z/bar" edge "10000113T0000Z/foo" "10000120T0000Z/foo" edge "10000120T0000Z/baz" "10000120T0000Z/qux" edge "10000120T0000Z/foo" "10000120T0000Z/bar" edge "10000120T0000Z/foo" "10000127T0000Z/foo" edge "10000127T0000Z/foo" "10000127T0000Z/bar" edge "10000127T0000Z/foo" "10000203T0000Z/foo" edge "10000203T0000Z/foo" "10000203T0000Z/bar" graph node "09991230T0000Z/bar" "bar\n09991230T0000Z" node "09991230T0000Z/baz" "baz\n09991230T0000Z" node "09991230T0000Z/foo" "foo\n09991230T0000Z" node "09991230T0000Z/qux" "qux\n09991230T0000Z" node "10000106T0000Z/bar" "bar\n10000106T0000Z" node "10000106T0000Z/foo" "foo\n10000106T0000Z" node "10000113T0000Z/bar" "bar\n10000113T0000Z" node "10000113T0000Z/foo" "foo\n10000113T0000Z" node "10000120T0000Z/bar" "bar\n10000120T0000Z" node "10000120T0000Z/baz" "baz\n10000120T0000Z" node "10000120T0000Z/foo" "foo\n10000120T0000Z" node "10000120T0000Z/qux" "qux\n10000120T0000Z" node "10000127T0000Z/bar" "bar\n10000127T0000Z" node "10000127T0000Z/foo" "foo\n10000127T0000Z" node "10000203T0000Z/bar" "bar\n10000203T0000Z" node "10000203T0000Z/foo" "foo\n10000203T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/multiweekly/reference.log0000664000175000017500000000167615202510242025603 0ustar alastairalastairInitial point: 1000W011 Final point: 1000W064 09991230T0000Z/baz -triggered off ['09991209T0000Z/baz'] 09991230T0000Z/foo -triggered off ['09991223T0000Z/foo'] 09991230T0000Z/bar -triggered off ['09991230T0000Z/foo'] 09991230T0000Z/qux -triggered off ['09991230T0000Z/baz'] 10000120T0000Z/baz -triggered off ['09991230T0000Z/baz'] 10000106T0000Z/foo -triggered off ['09991230T0000Z/foo'] 10000106T0000Z/bar -triggered off ['10000106T0000Z/foo'] 10000120T0000Z/qux -triggered off ['10000120T0000Z/baz'] 10000113T0000Z/foo -triggered off ['10000106T0000Z/foo'] 10000113T0000Z/bar -triggered off ['10000113T0000Z/foo'] 10000120T0000Z/foo -triggered off ['10000113T0000Z/foo'] 10000120T0000Z/bar -triggered off ['10000120T0000Z/foo'] 10000127T0000Z/foo -triggered off ['10000120T0000Z/foo'] 10000127T0000Z/bar -triggered off ['10000127T0000Z/foo'] 10000203T0000Z/foo -triggered off ['10000127T0000Z/foo'] 10000203T0000Z/bar -triggered off ['10000203T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/multiweekly/flow.cylc0000664000175000017500000000043415202510242024754 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 1000W011 final cycle point = 1000W064 [[graph]] P1W = "foo[-P1W] => foo => bar" P3W = "baz[-P3W] => baz => qux" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/test_header0000777000175000017500000000000015202510242027072 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/02-monthly.t0000775000175000017500000000201515202510242022654 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20000131T0100Z" # shellcheck disable=SC2034 FINALCP="2001" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/26-0000_rollunder.t0000775000175000017500000000262215202510242023641 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 0000_rollunder #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" grep_ok "Cannot dump TimePoint year: -1 not in bounds 0 to 9999." \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/28-missing-cycling-sequence.t0000775000175000017500000000336415202510242026107 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that missing cycling task fails validation. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] initial cycle point = 20140808T00 [[graph]] P1D = foo[-P1D] => bar [runtime] [[foo,bar]] __FLOW__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-cmp" cmp_ok "${TEST_NAME_BASE}-validate.stderr" <<__ERR__ TaskDefError: No cycling sequences defined for foo __ERR__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/11-r1_initial.t0000775000175000017500000000201615202510242023216 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/r5_final/0000775000175000017500000000000015202510242022254 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r5_final/graph.plain.ref0000664000175000017500000000136415202510242025161 0ustar alastairalastairedge "20140102T0000Z/xyzzy" "20140102T0000Z/bar" edge "20140102T0300Z/xyzzy" "20140102T0300Z/bar" edge "20140102T0600Z/xyzzy" "20140102T0600Z/bar" edge "20140102T0900Z/xyzzy" "20140102T0900Z/bar" edge "20140102T1200Z/xyzzy" "20140102T1200Z/bar" graph node "20140102T0000Z/bar" "bar\n20140102T0000Z" node "20140102T0000Z/xyzzy" "xyzzy\n20140102T0000Z" node "20140102T0300Z/bar" "bar\n20140102T0300Z" node "20140102T0300Z/xyzzy" "xyzzy\n20140102T0300Z" node "20140102T0600Z/bar" "bar\n20140102T0600Z" node "20140102T0600Z/xyzzy" "xyzzy\n20140102T0600Z" node "20140102T0900Z/bar" "bar\n20140102T0900Z" node "20140102T0900Z/xyzzy" "xyzzy\n20140102T0900Z" node "20140102T1200Z/bar" "bar\n20140102T1200Z" node "20140102T1200Z/xyzzy" "xyzzy\n20140102T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r5_final/reference.log0000664000175000017500000000103315202510242024712 0ustar alastairalastairInitial point: 20140101 Final point: 20140102T12 20140102T0000Z/xyzzy -triggered off [] 20140102T0300Z/xyzzy -triggered off [] 20140102T0600Z/xyzzy -triggered off [] 20140102T0000Z/bar -triggered off ['20140102T0000Z/xyzzy'] 20140102T0300Z/bar -triggered off ['20140102T0300Z/xyzzy'] 20140102T0600Z/bar -triggered off ['20140102T0600Z/xyzzy'] 20140102T0900Z/xyzzy -triggered off [] 20140102T1200Z/xyzzy -triggered off [] 20140102T0900Z/bar -triggered off ['20140102T0900Z/xyzzy'] 20140102T1200Z/bar -triggered off ['20140102T1200Z/xyzzy'] cylc-flow-8.6.4/tests/functional/cyclers/r5_final/flow.cylc0000664000175000017500000000036015202510242024076 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 final cycle point = 20140102T12 [[graph]] R5/PT3H = "xyzzy => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/04-multihourly.t0000775000175000017500000000203315202510242023561 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20000131T0100Z" # shellcheck disable=SC2034 FINALCP="20000202T0600+0600" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/01-hourly.t0000775000175000017500000000202015202510242022477 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20000229T2000" # shellcheck disable=SC2034 FINALCP="20000301" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/0000_rollunder/0000775000175000017500000000000015202510242023222 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/0000_rollunder/flow.cylc0000664000175000017500000000032215202510242025042 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 00000101T00 final cycle point = 00000101T00 [[graph]] P1D = "foo[-P1Y] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/cyclers/r5_initial-integer/0000775000175000017500000000000015202510242024247 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r5_initial-integer/graph.plain.ref0000664000175000017500000000064215202510242027152 0ustar alastairalastairedge "1/xyzzy" "1/bar" edge "2/xyzzy" "2/bar" edge "3/xyzzy" "3/bar" edge "4/xyzzy" "4/bar" edge "5/xyzzy" "5/bar" graph node "1/bar" "bar\n1" node "2/bar" "bar\n2" node "3/bar" "bar\n3" node "4/bar" "bar\n4" node "5/bar" "bar\n5" node "1/xyzzy" "xyzzy\n1" node "2/xyzzy" "xyzzy\n2" node "3/xyzzy" "xyzzy\n3" node "4/xyzzy" "xyzzy\n4" node "5/xyzzy" "xyzzy\n5" node "6/xyzzy" "xyzzy\n6" node "7/xyzzy" "xyzzy\n7" stop cylc-flow-8.6.4/tests/functional/cyclers/r5_initial-integer/reference.log0000664000175000017500000000057315202510242026715 0ustar alastairalastairInitial point: 1 Final point: 7 1/xyzzy -triggered off [] 2/xyzzy -triggered off [] 3/xyzzy -triggered off [] 1/bar -triggered off ['1/xyzzy'] 2/bar -triggered off ['2/xyzzy'] 4/xyzzy -triggered off [] 3/bar -triggered off ['3/xyzzy'] 4/bar -triggered off ['4/xyzzy'] 5/xyzzy -triggered off [] 5/bar -triggered off ['5/xyzzy'] 6/xyzzy -triggered off [] 7/xyzzy -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/r5_initial-integer/flow.cylc0000664000175000017500000000052315202510242026072 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 7 [[graph]] # xyzzy should not spawn bar in the last two cycle points: P1 = xyzzy # 7 cycles R5//P1 = "xyzzy => bar" # 5 cycles [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/18-r1_multi_start.t0000775000175000017500000000201715202510242024144 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="2014-01-01" # shellcheck disable=SC2034 FINALCP="2014-01-04" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/31-rnone_reverse.t0000775000175000017500000000202515202510242024041 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="2015-02-21T00" # shellcheck disable=SC2034 FINALCP="2015-02-21T18" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_immortal/0000775000175000017500000000000015202510242024514 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_initial_immortal/graph.plain.ref0000664000175000017500000000554115202510242027422 0ustar alastairalastairedge "20140101T0000Z/cold_foo" "20140101T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140101T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140102T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140102T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140103T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140103T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140104T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140104T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140105T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140105T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140106T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140106T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140107T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140107T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140108T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140108T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140109T0000Z/foo" edge "20140101T0000Z/cold_foo" "20140109T1200Z/foo" edge "20140101T0000Z/cold_foo" "20140110T0000Z/foo" edge "20140101T0000Z/foo" "20140101T1200Z/foo" edge "20140101T1200Z/foo" "20140102T0000Z/foo" edge "20140102T0000Z/foo" "20140102T1200Z/foo" edge "20140102T1200Z/foo" "20140103T0000Z/foo" edge "20140103T0000Z/foo" "20140103T1200Z/foo" edge "20140103T1200Z/foo" "20140104T0000Z/foo" edge "20140104T0000Z/foo" "20140104T1200Z/foo" edge "20140104T1200Z/foo" "20140105T0000Z/foo" edge "20140105T0000Z/foo" "20140105T1200Z/foo" edge "20140105T0000Z/stop" "20140105T0000Z/foo" edge "20140105T1200Z/foo" "20140106T0000Z/foo" edge "20140106T0000Z/foo" "20140106T1200Z/foo" edge "20140106T1200Z/foo" "20140107T0000Z/foo" edge "20140107T0000Z/foo" "20140107T1200Z/foo" edge "20140107T1200Z/foo" "20140108T0000Z/foo" edge "20140108T0000Z/foo" "20140108T1200Z/foo" edge "20140108T1200Z/foo" "20140109T0000Z/foo" edge "20140109T0000Z/foo" "20140109T1200Z/foo" edge "20140109T1200Z/foo" "20140110T0000Z/foo" graph node "20140101T0000Z/cold_foo" "cold_foo\n20140101T0000Z" node "20140101T0000Z/foo" "foo\n20140101T0000Z" node "20140101T1200Z/foo" "foo\n20140101T1200Z" node "20140102T0000Z/foo" "foo\n20140102T0000Z" node "20140102T1200Z/foo" "foo\n20140102T1200Z" node "20140103T0000Z/foo" "foo\n20140103T0000Z" node "20140103T1200Z/foo" "foo\n20140103T1200Z" node "20140104T0000Z/foo" "foo\n20140104T0000Z" node "20140104T1200Z/foo" "foo\n20140104T1200Z" node "20140105T0000Z/foo" "foo\n20140105T0000Z" node "20140105T0000Z/stop" "stop\n20140105T0000Z" node "20140105T1200Z/foo" "foo\n20140105T1200Z" node "20140106T0000Z/foo" "foo\n20140106T0000Z" node "20140106T1200Z/foo" "foo\n20140106T1200Z" node "20140107T0000Z/foo" "foo\n20140107T0000Z" node "20140107T1200Z/foo" "foo\n20140107T1200Z" node "20140108T0000Z/foo" "foo\n20140108T0000Z" node "20140108T1200Z/foo" "foo\n20140108T1200Z" node "20140109T0000Z/foo" "foo\n20140109T0000Z" node "20140109T1200Z/foo" "foo\n20140109T1200Z" node "20140110T0000Z/foo" "foo\n20140110T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_immortal/reference.log0000664000175000017500000000233315202510242027156 0ustar alastairalastairInitial point: 20140101T0000Z Final point: None 20140101T0000Z/cold_foo -triggered off [] 20140101T0000Z/foo -triggered off ['20131231T1200Z/foo', '20140101T0000Z/cold_foo'] 20140101T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140101T0000Z/foo'] 20140102T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140101T1200Z/foo'] 20140102T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140102T0000Z/foo'] 20140103T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140102T1200Z/foo'] 20140103T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140103T0000Z/foo'] 20140105T0000Z/stop -triggered off [] 20140104T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140103T1200Z/foo'] 20140104T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140104T0000Z/foo'] 20140105T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140104T1200Z/foo', '20140105T0000Z/stop'] 20140105T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140105T0000Z/foo'] 20140106T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140105T1200Z/foo'] 20140106T1200Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140106T0000Z/foo'] 20140107T0000Z/foo -triggered off ['20140101T0000Z/cold_foo', '20140106T1200Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/r1_initial_immortal/flow.cylc0000664000175000017500000000053615202510242026343 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20140101 [[graph]] R1 = "cold_foo" PT12H = "cold_foo[^] & foo[-PT12H] => foo" R1/+P4D = "stop => foo" [runtime] [[root]] script = true [[stop]] script = cylc stop "${CYLC_WORKFLOW_ID}//20140107" cylc-flow-8.6.4/tests/functional/cyclers/37-exclusions.t0000775000175000017500000000466615202510242023404 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then set_test_number 4 else set_test_number 3 fi #------------------------------------------------------------------------------- CHOSEN_WORKFLOW="$(basename "$0" | sed "s/^.*-\(.*\)\.t/\1/g")" install_workflow "${TEST_NAME_BASE}" "${CHOSEN_WORKFLOW}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" \ "20000101T00Z" "20000102T12Z" cmp_ok "${WORKFLOW_NAME}.graph.plain" "$TEST_SOURCE_DIR/$CHOSEN_WORKFLOW/graph.plain.ref" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- if [[ -f "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" ]]; then TEST_NAME="${TEST_NAME_BASE}-find" WORKFLOW_DIR="$RUN_DIR/${WORKFLOW_NAME}" (cd "$WORKFLOW_DIR" && find 'log/job' 'work' -type f) | sort -V >"${TEST_NAME}" cmp_ok "${TEST_NAME}" "$TEST_SOURCE_DIR/${TEST_NAME_BASE}-find.out" fi #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cyclers/r1_multi_start/0000775000175000017500000000000015202510242023526 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/r1_multi_start/graph.plain.ref0000664000175000017500000000164615202510242026436 0ustar alastairalastairedge "20140101T0000Z/cold_foo" "20140101T0000Z/foo_midnight" edge "20140101T0000Z/cold_foo" "20140101T0600Z/foo_dawn" edge "20140101T0000Z/foo_midnight" "20140102T0000Z/foo_midnight" edge "20140101T0600Z/foo_dawn" "20140102T0600Z/foo_dawn" edge "20140102T0000Z/foo_midnight" "20140103T0000Z/foo_midnight" edge "20140102T0600Z/foo_dawn" "20140103T0600Z/foo_dawn" edge "20140103T0000Z/foo_midnight" "20140104T0000Z/foo_midnight" graph node "20140101T0000Z/cold_foo" "cold_foo\n20140101T0000Z" node "20140101T0000Z/foo_midnight" "foo_midnight\n20140101T0000Z" node "20140101T0600Z/foo_dawn" "foo_dawn\n20140101T0600Z" node "20140102T0000Z/foo_midnight" "foo_midnight\n20140102T0000Z" node "20140102T0600Z/foo_dawn" "foo_dawn\n20140102T0600Z" node "20140103T0000Z/foo_midnight" "foo_midnight\n20140103T0000Z" node "20140103T0600Z/foo_dawn" "foo_dawn\n20140103T0600Z" node "20140104T0000Z/foo_midnight" "foo_midnight\n20140104T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/r1_multi_start/reference.log0000664000175000017500000000120715202510242026167 0ustar alastairalastairInitial point: 2014-01-01 Final point: 2014-01-04 20140101T0000Z/cold_foo -triggered off [] 20140101T0000Z/foo_midnight -triggered off ['20131231T0000Z/foo_midnight', '20140101T0000Z/cold_foo'] 20140101T0600Z/foo_dawn -triggered off ['20131231T0600Z/foo_dawn', '20140101T0000Z/cold_foo'] 20140102T0000Z/foo_midnight -triggered off ['20140101T0000Z/foo_midnight'] 20140102T0600Z/foo_dawn -triggered off ['20140101T0600Z/foo_dawn'] 20140103T0000Z/foo_midnight -triggered off ['20140102T0000Z/foo_midnight'] 20140103T0600Z/foo_dawn -triggered off ['20140102T0600Z/foo_dawn'] 20140104T0000Z/foo_midnight -triggered off ['20140103T0000Z/foo_midnight'] cylc-flow-8.6.4/tests/functional/cyclers/r1_multi_start/flow.cylc0000664000175000017500000000064015202510242025351 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2014-01-01 final cycle point = 2014-01-04 [[graph]] R1 = "cold_foo" R1/T00 = "cold_foo[^] => foo_midnight" R1/T06 = "cold_foo[^] => foo_dawn" T00 = "foo_midnight[-P1D] => foo_midnight" T06 = "foo_dawn[-P1D] => foo_dawn" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/13-r5_final.t0000775000175000017500000000201615202510242022664 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20140101" # shellcheck disable=SC2034 FINALCP="20140102T12" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/29-r1_restricted.t0000775000175000017500000000202115202510242023742 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20130808T00" # shellcheck disable=SC2034 FINALCP="20130808T18" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/32-rmany_reverse.t0000775000175000017500000000202515202510242024047 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="2015-02-21T00" # shellcheck disable=SC2034 FINALCP="2015-02-21T18" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/03-multidaily.t0000775000175000017500000000202015202510242023334 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20001231T0100" # shellcheck disable=SC2034 FINALCP="20010114" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/subhourly/0000775000175000017500000000000015202510242022611 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/subhourly/graph.plain.ref0000664000175000017500000000132415202510242025512 0ustar alastairalastairedge "20131231T2300Z/foo" "20131231T2300Z/bar" edge "20131231T2300Z/foo" "20131231T2330Z/foo" edge "20131231T2330Z/foo" "20131231T2330Z/bar" edge "20131231T2330Z/foo" "20140101T0000Z/foo" edge "20140101T0000Z/foo" "20140101T0000Z/bar" edge "20140101T0000Z/foo" "20140101T0030Z/foo" edge "20140101T0030Z/foo" "20140101T0030Z/bar" graph node "20131231T2300Z/bar" "bar\n20131231T2300Z" node "20131231T2300Z/foo" "foo\n20131231T2300Z" node "20131231T2330Z/bar" "bar\n20131231T2330Z" node "20131231T2330Z/foo" "foo\n20131231T2330Z" node "20140101T0000Z/bar" "bar\n20140101T0000Z" node "20140101T0000Z/foo" "foo\n20140101T0000Z" node "20140101T0030Z/bar" "bar\n20140101T0030Z" node "20140101T0030Z/foo" "foo\n20140101T0030Z" stop cylc-flow-8.6.4/tests/functional/cyclers/subhourly/reference.log0000664000175000017500000000100015202510242025241 0ustar alastairalastairInitial point: 20131231T2300 Final point: 20140101T0050 20131231T2300Z/foo -triggered off ['20131231T2230Z/foo'] 20131231T2300Z/bar -triggered off ['20131231T2300Z/foo'] 20131231T2330Z/foo -triggered off ['20131231T2300Z/foo'] 20131231T2330Z/bar -triggered off ['20131231T2330Z/foo'] 20140101T0000Z/foo -triggered off ['20131231T2330Z/foo'] 20140101T0000Z/bar -triggered off ['20140101T0000Z/foo'] 20140101T0030Z/foo -triggered off ['20140101T0000Z/foo'] 20140101T0030Z/bar -triggered off ['20140101T0030Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/subhourly/flow.cylc0000664000175000017500000000040215202510242024430 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20131231T2300 final cycle point = 20140101T0050 [[graph]] PT30M = "foo[-PT30M] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/hourly/0000775000175000017500000000000015202510242022077 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/hourly/graph.plain.ref0000664000175000017500000000162215202510242025001 0ustar alastairalastairedge "20000229T2000Z/foo" "20000229T2000Z/bar" edge "20000229T2000Z/foo" "20000229T2100Z/foo" edge "20000229T2100Z/foo" "20000229T2100Z/bar" edge "20000229T2100Z/foo" "20000229T2200Z/foo" edge "20000229T2200Z/foo" "20000229T2200Z/bar" edge "20000229T2200Z/foo" "20000229T2300Z/foo" edge "20000229T2300Z/foo" "20000229T2300Z/bar" edge "20000229T2300Z/foo" "20000301T0000Z/foo" edge "20000301T0000Z/foo" "20000301T0000Z/bar" graph node "20000229T2000Z/bar" "bar\n20000229T2000Z" node "20000229T2000Z/foo" "foo\n20000229T2000Z" node "20000229T2100Z/bar" "bar\n20000229T2100Z" node "20000229T2100Z/foo" "foo\n20000229T2100Z" node "20000229T2200Z/bar" "bar\n20000229T2200Z" node "20000229T2200Z/foo" "foo\n20000229T2200Z" node "20000229T2300Z/bar" "bar\n20000229T2300Z" node "20000229T2300Z/foo" "foo\n20000229T2300Z" node "20000301T0000Z/bar" "bar\n20000301T0000Z" node "20000301T0000Z/foo" "foo\n20000301T0000Z" stop cylc-flow-8.6.4/tests/functional/cyclers/hourly/reference.log0000664000175000017500000000115515202510242024542 0ustar alastairalastairInitial point: 20000229T2000 Final point: 20000301 20000229T2000Z/foo -triggered off ['20000229T1900Z/foo'] 20000229T2000Z/bar -triggered off ['20000229T2000Z/foo'] 20000229T2100Z/foo -triggered off ['20000229T2000Z/foo'] 20000229T2100Z/bar -triggered off ['20000229T2100Z/foo'] 20000229T2200Z/foo -triggered off ['20000229T2100Z/foo'] 20000229T2200Z/bar -triggered off ['20000229T2200Z/foo'] 20000229T2300Z/foo -triggered off ['20000229T2200Z/foo'] 20000229T2300Z/bar -triggered off ['20000229T2300Z/foo'] 20000301T0000Z/foo -triggered off ['20000229T2300Z/foo'] 20000301T0000Z/bar -triggered off ['20000301T0000Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/hourly/flow.cylc0000664000175000017500000000037315202510242023725 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20000229T2000 final cycle point = 20000301 [[graph]] PT1H = "foo[-PT1H] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/05-multimonthly.t0000775000175000017500000000200315202510242023727 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="1000" # shellcheck disable=SC2034 FINALCP="1001" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/15-subhourly.t0000775000175000017500000000202515202510242023223 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # variables used in the sourced common script below. # shellcheck disable=SC2034 INITIALCP="20131231T2300" # shellcheck disable=SC2034 FINALCP="20140101T0050" source "$(dirname "$0")"/common cylc-flow-8.6.4/tests/functional/cyclers/weekly/0000775000175000017500000000000015202510242022055 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/weekly/graph.plain.ref0000664000175000017500000000212015202510242024751 0ustar alastairalastairedge "20050107T1200Z/foo" "20050107T1200Z/bar" edge "20050107T1200Z/foo" "20050114T1200Z/foo" edge "20050114T1200Z/foo" "20050114T1200Z/bar" edge "20050114T1200Z/foo" "20050121T1200Z/foo" edge "20050121T1200Z/foo" "20050121T1200Z/bar" edge "20050121T1200Z/foo" "20050128T1200Z/foo" edge "20050128T1200Z/foo" "20050128T1200Z/bar" edge "20050128T1200Z/foo" "20050204T1200Z/foo" edge "20050204T1200Z/foo" "20050204T1200Z/bar" edge "20050204T1200Z/foo" "20050211T1200Z/foo" edge "20050211T1200Z/foo" "20050211T1200Z/bar" graph node "20050107T1200Z/bar" "bar\n20050107T1200Z" node "20050107T1200Z/foo" "foo\n20050107T1200Z" node "20050114T1200Z/bar" "bar\n20050114T1200Z" node "20050114T1200Z/foo" "foo\n20050114T1200Z" node "20050121T1200Z/bar" "bar\n20050121T1200Z" node "20050121T1200Z/foo" "foo\n20050121T1200Z" node "20050128T1200Z/bar" "bar\n20050128T1200Z" node "20050128T1200Z/foo" "foo\n20050128T1200Z" node "20050204T1200Z/bar" "bar\n20050204T1200Z" node "20050204T1200Z/foo" "foo\n20050204T1200Z" node "20050211T1200Z/bar" "bar\n20050211T1200Z" node "20050211T1200Z/foo" "foo\n20050211T1200Z" stop cylc-flow-8.6.4/tests/functional/cyclers/weekly/reference.log0000664000175000017500000000134515202510242024521 0ustar alastairalastairInitial point: 2005W015T12 Final point: 2005W065T11-0100 20050107T1200Z/foo -triggered off ['20041231T1200Z/foo'] 20050107T1200Z/bar -triggered off ['20050107T1200Z/foo'] 20050114T1200Z/foo -triggered off ['20050107T1200Z/foo'] 20050114T1200Z/bar -triggered off ['20050114T1200Z/foo'] 20050121T1200Z/foo -triggered off ['20050114T1200Z/foo'] 20050121T1200Z/bar -triggered off ['20050121T1200Z/foo'] 20050128T1200Z/foo -triggered off ['20050121T1200Z/foo'] 20050128T1200Z/bar -triggered off ['20050128T1200Z/foo'] 20050204T1200Z/foo -triggered off ['20050128T1200Z/foo'] 20050204T1200Z/bar -triggered off ['20050204T1200Z/foo'] 20050211T1200Z/foo -triggered off ['20050204T1200Z/foo'] 20050211T1200Z/bar -triggered off ['20050211T1200Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/weekly/flow.cylc0000664000175000017500000000037715202510242023707 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2005W015T12 final cycle point = 2005W065T11-0100 [[graph]] P1W = "foo[-P1W] => foo => bar" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/rmany_reverse/0000775000175000017500000000000015202510242023436 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/rmany_reverse/graph.plain.ref0000664000175000017500000000115615202510242026342 0ustar alastairalastairedge "20150221T0000Z/foo" "20150221T0000Z/nonstop" edge "20150221T0600Z/foo" "20150221T0600Z/nonstop" edge "20150221T1200Z/foo" "20150221T1200Z/nonstop" edge "20150221T1800Z/foo" "20150221T1800Z/stop" graph node "20150221T0000Z/foo" "foo\n20150221T0000Z" node "20150221T0000Z/nonstop" "nonstop\n20150221T0000Z" node "20150221T0600Z/foo" "foo\n20150221T0600Z" node "20150221T0600Z/nonstop" "nonstop\n20150221T0600Z" node "20150221T1200Z/foo" "foo\n20150221T1200Z" node "20150221T1200Z/nonstop" "nonstop\n20150221T1200Z" node "20150221T1800Z/foo" "foo\n20150221T1800Z" node "20150221T1800Z/stop" "stop\n20150221T1800Z" stop cylc-flow-8.6.4/tests/functional/cyclers/rmany_reverse/reference.log0000664000175000017500000000067715202510242026111 0ustar alastairalastairInitial point: 20150221T0000Z Final point: 20150221T1800Z 20150221T0000Z/foo -triggered off [] 20150221T0600Z/foo -triggered off [] 20150221T1200Z/foo -triggered off [] 20150221T0000Z/nonstop -triggered off ['20150221T0000Z/foo'] 20150221T0600Z/nonstop -triggered off ['20150221T0600Z/foo'] 20150221T1200Z/nonstop -triggered off ['20150221T1200Z/foo'] 20150221T1800Z/foo -triggered off [] 20150221T1800Z/stop -triggered off ['20150221T1800Z/foo'] cylc-flow-8.6.4/tests/functional/cyclers/rmany_reverse/flow.cylc0000664000175000017500000000045615202510242025266 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2015-02-21T00 final cycle point = 2015-02-21T18 [[graph]] PT6H = foo R1/P0D = foo => stop R20/PT6H/-PT6H = foo => nonstop [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/cyclers/integer_exclusions_advanced/0000775000175000017500000000000015202510242026313 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclers/integer_exclusions_advanced/graph.plain.ref0000664000175000017500000000171315202510242031216 0ustar alastairalastairgraph node "1/bar" "bar\n1" node "3/bar" "bar\n3" node "5/bar" "bar\n5" node "7/bar" "bar\n7" node "9/bar" "bar\n9" node "11/bar" "bar\n11" node "13/bar" "bar\n13" node "15/bar" "bar\n15" node "3/cthulhu" "cthulhu\n3" node "5/cthulhu" "cthulhu\n5" node "9/cthulhu" "cthulhu\n9" node "11/cthulhu" "cthulhu\n11" node "15/cthulhu" "cthulhu\n15" node "1/foo" "foo\n1" node "4/foo" "foo\n4" node "5/foo" "foo\n5" node "6/foo" "foo\n6" node "8/foo" "foo\n8" node "9/foo" "foo\n9" node "10/foo" "foo\n10" node "11/foo" "foo\n11" node "12/foo" "foo\n12" node "13/foo" "foo\n13" node "14/foo" "foo\n14" node "15/foo" "foo\n15" node "16/foo" "foo\n16" node "2/qux" "qux\n2" node "4/qux" "qux\n4" node "10/qux" "qux\n10" node "12/qux" "qux\n12" node "14/qux" "qux\n14" node "16/qux" "qux\n16" node "2/woo" "woo\n2" node "4/woo" "woo\n4" node "6/woo" "woo\n6" node "8/woo" "woo\n8" node "10/woo" "woo\n10" node "12/woo" "woo\n12" node "14/woo" "woo\n14" node "16/woo" "woo\n16" stop cylc-flow-8.6.4/tests/functional/cyclers/integer_exclusions_advanced/reference.log0000664000175000017500000000201115202510242030746 0ustar alastairalastairInitial point: 1 Final point: 16 2/woo -triggered off [] 2/qux -triggered off [] 1/bar -triggered off [] 1/foo -triggered off [] 3/cthulhu -triggered off [] 4/qux -triggered off [] 5/cthulhu -triggered off [] 3/bar -triggered off [] 4/woo -triggered off [] 4/foo -triggered off [] 5/foo -triggered off [] 6/woo -triggered off [] 9/cthulhu -triggered off [] 10/qux -triggered off [] 5/bar -triggered off [] 8/woo -triggered off [] 11/cthulhu -triggered off [] 12/qux -triggered off [] 6/foo -triggered off [] 7/bar -triggered off [] 10/woo -triggered off [] 8/foo -triggered off [] 9/bar -triggered off [] 12/woo -triggered off [] 11/bar -triggered off [] 9/foo -triggered off [] 15/cthulhu -triggered off [] 13/bar -triggered off [] 10/foo -triggered off [] 14/woo -triggered off [] 14/qux -triggered off [] 16/woo -triggered off [] 15/bar -triggered off [] 11/foo -triggered off [] 16/qux -triggered off [] 12/foo -triggered off [] 13/foo -triggered off [] 14/foo -triggered off [] 15/foo -triggered off [] 16/foo -triggered off [] cylc-flow-8.6.4/tests/functional/cyclers/integer_exclusions_advanced/flow.cylc0000664000175000017500000000107615202510242030142 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1 final cycle point = +P15 # = 16 runahead limit = P11 cycling mode = integer [[graph]] R/P1!(2,3,7) = foo P1 ! P2 = woo P1 ! +P1/P2 = bar P1 !(P2,6,8) = qux R/1/P2!P3 = cthulhu [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message -- "${CYLC_WORKFLOW_ID}" "${CYLC_TASK_JOB}" 'the cheese is ready' """ [[[outputs]]] out1 = the cheese is ready cylc-flow-8.6.4/tests/functional/pre-initial/0000775000175000017500000000000015202510242021326 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/09-warm-iso.t0000664000175000017500000000317715202510242023507 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test pre-initial cycling works under warm starts . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" warm-start-iso #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --startcp=20130102T00 --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/pre-initial/04-warm.t0000664000175000017500000000320115202510242022676 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test pre-initial cycling works under warm starts . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" warm-start #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --startcp=20130101T00 \ --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/pre-initial/00-simple.t0000664000175000017500000000171315202510242023223 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test simple pre initial cycling dependencies removal . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/pre-initial/08-conditional-messaging.t0000664000175000017500000000172415202510242026222 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test removal of pre-initial cycle messages from conditionals. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/pre-initial/warm-start/0000775000175000017500000000000015202510242023427 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/warm-start/reference.log0000664000175000017500000000047115202510242026072 0ustar alastairalastairFinal point: 20130102T0000Z 20130101T0000Z/foo -triggered off ['20121231T1800Z/foo'] 20130101T0600Z/foo -triggered off ['20130101T0000Z/foo'] 20130101T1200Z/foo -triggered off ['20130101T0600Z/foo'] 20130101T1800Z/foo -triggered off ['20130101T1200Z/foo'] 20130102T0000Z/foo -triggered off ['20130101T1800Z/foo'] cylc-flow-8.6.4/tests/functional/pre-initial/warm-start/flow.cylc0000664000175000017500000000032515202510242025252 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20130101T00 final cycle point = 20130102T00 [[graph]] PT6H = "foo[-PT6H] => foo" [runtime] [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/pre-initial/07-simple-messaging/0000775000175000017500000000000015202510242025016 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/07-simple-messaging/reference.log0000664000175000017500000000025415202510242027460 0ustar alastairalastairInitial point: 20100808T0000Z Final point: 20100809T0000Z 20100808T0000Z/foo -triggered off ['20100807T0000Z/foo'] 20100809T0000Z/foo -triggered off ['20100808T0000Z/foo'] cylc-flow-8.6.4/tests/functional/pre-initial/07-simple-messaging/flow.cylc0000664000175000017500000000055015202510242026641 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100808T00 final cycle point = 20100809T00 [[graph]] T00 = "foo[-P1D]:restart1 => foo" [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message "restart files ready" """ [[[outputs]]] restart1 = "restart files ready" cylc-flow-8.6.4/tests/functional/pre-initial/warm-offset/0000775000175000017500000000000015202510242023560 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/warm-offset/reference.log0000664000175000017500000000043615202510242026224 0ustar alastairalastairInitial point: 20130101T0600Z Final point: 20130102T0000Z 20130101T0600Z/foo -triggered off ['20130101T0000Z/foo'] 20130101T1200Z/foo -triggered off ['20130101T0600Z/foo'] 20130101T1800Z/foo -triggered off ['20130101T1200Z/foo'] 20130102T0000Z/foo -triggered off ['20130101T1800Z/foo'] cylc-flow-8.6.4/tests/functional/pre-initial/warm-offset/flow.cylc0000664000175000017500000000032515202510242025403 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20130101T00 final cycle point = 20130102T00 [[graph]] PT6H = "foo[-PT6H] => foo" [runtime] [[foo]] script = "true" cylc-flow-8.6.4/tests/functional/pre-initial/05-warm-new-ict.t0000664000175000017500000000333315202510242024251 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test pre-initial cycling works under warm starts with a new initial cycle # time that is later than the flow.cylc initial cycle point. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" warm-offset #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --startcp=20130101T0600Z \ --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/pre-initial/12-warm-restart.t0000664000175000017500000000525015202510242024365 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test warm start persists across restarts . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 6 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" warm-start #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-run-paused workflow_run_ok "${TEST_NAME}" cylc play "${WORKFLOW_NAME}" --startcp=20130101T12 --pause #------------------------------------------------------------------------------- cylc stop --max-polls=10 --interval=2 "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-restart workflow_run_ok "${TEST_NAME}" cylc play "${WORKFLOW_NAME}" # Ensure workflow has started poll_workflow_running #------------------------------------------------------------------------------- # Check pre-reqs TEST_NAME=${TEST_NAME_BASE}-check-prereq run_ok "${TEST_NAME}" cylc show "${WORKFLOW_NAME}//20130101T1200Z/foo" --list-prereqs cmp_ok "${TEST_NAME}.stdout" <<'__OUT__' 20130101T0600Z/foo succeeded __OUT__ #------------------------------------------------------------------------------- # Stop workflow cylc stop --max-polls=10 --interval=2 "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- DB_FILE="${RUN_DIR}/${WORKFLOW_NAME}/log/db" NAME='database-entry' sqlite3 "${DB_FILE}" \ "SELECT value FROM workflow_params WHERE key=='startcp'" >"${NAME}" cmp_ok "${NAME}" <<<'20130101T12' #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/pre-initial/test_header0000777000175000017500000000000015202510242027643 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/warm-start-iso/0000775000175000017500000000000015202510242024217 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/warm-start-iso/reference.log0000664000175000017500000000106115202510242026656 0ustar alastairalastairInitial point: 20130101T0000Z Start point: 20130102T0000Z Final point: 20130103T0000Z 20130102T0000Z/qux -triggered off ['20130101T0000Z/foo', '20130101T1800Z/qux'] 20130102T0000Z/wibble -triggered off [] 20130102T0600Z/wibble -triggered off [] 20130102T0600Z/qux -triggered off ['20130101T0000Z/foo', '20130102T0000Z/qux'] 20130102T1200Z/qux -triggered off ['20130101T0000Z/foo', '20130102T0600Z/qux'] 20130102T1800Z/qux -triggered off ['20130101T0000Z/foo', '20130102T1200Z/qux'] 20130103T0000Z/qux -triggered off ['20130101T0000Z/foo', '20130102T1800Z/qux'] cylc-flow-8.6.4/tests/functional/pre-initial/warm-start-iso/flow.cylc0000664000175000017500000000053015202510242026040 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20130101T00 final cycle point = 20130103T00 [[graph]] R1 = "foo" R1/T18 = "bar => baz" R6//PT6H = "wibble" T00,T06,T12,T18 = "foo[^] & qux[-PT6H] => qux" [runtime] [[root]] script = "true" cylc-flow-8.6.4/tests/functional/pre-initial/00-simple/0000775000175000017500000000000015202510242023034 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/00-simple/reference.log0000664000175000017500000000024415202510242025475 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100102T0000Z 20100101T0000Z/a -triggered off ['20091231T0000Z/a'] 20100102T0000Z/a -triggered off ['20100101T0000Z/a'] cylc-flow-8.6.4/tests/functional/pre-initial/00-simple/flow.cylc0000664000175000017500000000031515202510242024656 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100102T00 [[graph]] T00 = "a[-PT24H] => a" [runtime] [[a]] script = true cylc-flow-8.6.4/tests/functional/pre-initial/07-simple-messaging.t0000664000175000017500000000171615202510242025210 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test removal of pre-initial cycle message dependencies. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/pre-initial/08-conditional-messaging/0000775000175000017500000000000015202510242026031 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pre-initial/08-conditional-messaging/reference.log0000664000175000017500000000051115202510242030467 0ustar alastairalastairInitial point: 20100808T0000Z Final point: 20100809T0000Z 20100808T0000Z/bar -triggered off [] 20100808T0000Z/foo -triggered off ['20100807T0000Z/foo', '20100808T0000Z/bar'] 20100809T0000Z/bar -triggered off [] 20100809T0000Z/foo -triggered off ['20100808T0000Z/foo'] 20100809T0000Z/handled -triggered off ['20100809T0000Z/bar'] cylc-flow-8.6.4/tests/functional/pre-initial/08-conditional-messaging/flow.cylc0000664000175000017500000000117215202510242027655 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [[events]] expected task failures = 20100809T0000Z/bar [scheduling] initial cycle point = 20100808T00 final cycle point = 20100809T00 [[graph]] T00 = """ foo[-P1D]:restart1 | bar? => foo bar:fail? => handled """ [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message "restart files ready" """ [[[outputs]]] restart1 = "restart files ready" [[bar]] script = [[ "$(cylc cycle-point)" == 20100808T0000Z ]] cylc-flow-8.6.4/tests/functional/lib/0000775000175000017500000000000015202510242017657 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/lib/bash/0000775000175000017500000000000015202510242020574 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/lib/bash/test_header0000664000175000017500000010773015202510242023016 0ustar alastairalastair#!/usr/bin/env bash # 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 . # # NAME # test_header # # SYNOPSIS # . $CYLC_REPO_DIR/tests/lib/bash/test_header # # DESCRIPTION # Interface for constructing tests under a TAP harness (the "prove" # command). # # FUNCTIONS # set_test_number N # echo a total number of tests for TAP to read. # ok TEST_NAME # echo a TAP OK message for TEST_NAME. # fail TEST_NAME # echo a TAP fail message for TEST_NAME. If $CYLC_TEST_DEBUG is set, # cat $TEST_NAME.stderr to stderr. # run_ok TEST_NAME COMMAND ... # Run COMMAND with any following options/arguments and store stdout # and stderr in TEST_NAME.stdout and TEST_NAME.stderr. # This is expected to have a return code of 0, which ok's the test. # run_fail TEST_NAME COMMAND ... # Run COMMAND with any following options/arguments and store stdout # and stderr in TEST_NAME.stdout and TEST_NAME.stderr. # This is expected to have a non-zero return code, which ok's the test. # run_graphql_ok TEST_NAME WORKFLOW JSON # Send a graphql query to the workflow, extract the result from # ${TEST_NAME}.stdout # cmp_ok FILE_TEST FILE_CONTROL # Compare FILE_TEST with a file or stdin given by FILE_CONTROL (stdin # if FILE_CONTROL is "-" or missing). By default, it uses "diff -u" to # compare files. However, if an alternate command such as "xxdiff -D" # is desirable (e.g. for debugging), # "export CYLC_TEST_DIFF_CMD=xxdiff -D". # cmp_ok_re FILE_TEST FILE_CONTROL # Match a file against a regex file line by line. # Reads in the test and control files line by line and checks the test # line re.match'es the control line. # cmp_json TEST_NAME FILE_TEST [FILE_CONTROL] # Alternative implementation to cmp_json_ok, compare two JSON files # and display any differences as a unified(ish) diff: # * Works with nested dictionaries. # * Impervious to dictionary order. # contains_ok FILE_TEST [FILE_CONTROL] # Make sure that each line in FILE_TEST is present in FILE_CONTROL # (stdin if FILE_CONTROL is "-" or missing). # grep_ok PATTERN FILE [$OPTS] # Run "grep [$OPTS] -q -e PATTERN FILE". # grep_workflow_log_ok TEST_NAME PATTERN [$OPTS] # Run "grep [$OPTS] -s -e PATTERN ". # named_grep_ok NAME PATTERN FILE [$OPTS] # Run grep_ok with a custom test name. # OPTS: put grep options like '-E' (extended regex) at end of line. # grep_fail PATTERN FILE [$OPTS] # Run "grep [$OPTS] -q -e PATTERN FILE", expect no match. # count_ok PATTERN FILE COUNT # Test that PATTERN occurs in exactly COUNT lines of FILE. # exists_ok FILE # Test that FILE exists # exists_fail FILE # Test that FILE does not exist # init_workflow TEST_NAME [SOURCE] [[RUN_NUMS]] # Create a workflow from SOURCE's "flow.cylc" called: # "cylctb-${CYLC_TEST_TIME_INIT}/${TEST_SOURCE_DIR##*tests/}/${TEST_NAME}" # Provides WORKFLOW_NAME and WORKFLOW_RUN_DIR variables. # RUN_NUMS (defaults to false): If false run cylc install --no-rundir # install_workflow TEST_NAME SOURCE [RUN_NUMS] # Same as init_workflow, but SOURCE must be a directory containing a # "flow.cylc" file. # log_scan TEST_NAME FILE ATTEMPTS DELAY PATTERN... # Monitor FILE polling every DELAY seconds for maximum of ATTEMPTS # tries grepping for each PATTERN in turn. Tests will only pass if the # PATTERNs appear in FILE in the correct order. Runs one test per # pattern, each prefixed by TEST_NAME. # set LOG_SCAN_GREP_OPTS in the environment, e.g. "-E" for "grep -E" # make_rnd_workflow # Create a randomly-named workflow source directory. # mock_smtpd_init # Start a mock SMTP server daemon for testing. Write host:port setting # to the variable TEST_SMTPD_HOST. Write pid of daemon to # TEST_SMTPD_PID. Write log to TEST_SMTPD_LOG. # mock_smtpd_kill # Kill the mock SMTP server daemon process. # poll COMMAND # Run COMMAND in 1 second intervals for a minute until COMMAND returns # a zero value, or exit 1 (abort test) with a timeout message. # poll_grep ... # Shorthand for 'poll grep -s ...'. # poll_grep_workflow_log ... # Shorthand for 'poll_grep ... "${WORKFLOW_RUN_DIR}/log/scheduler/log"' # poll_pid_done PID # Poll until process with PID is done. # poll_workflow_running # Shorthand for 'poll test -e "${WORKFLOW_RUN_DIR}/.service/contact"' # poll_workflow_stopped # Shorthand for 'poll "!" test -e "${WORKFLOW_RUN_DIR}/.service/contact"'. # Note that you might want to call poll_workflow_running before this as # it may return before contact file is created in the first place. # get_workflow_uuid # Extract the UUID from the contact file or echo '' if not present. # poll_workflow_restart [TIMEOUT [UUID]] # Wait for the workflow to restart. # Provide the UUID from the current run if workflow is not # guaranteed to be running. # purge [WORKFLOW_NAME [PLATFORM]] # Remove a workflow from the filesystem. # Defaults to removing $WORKFLOW_NAME on $CYLC_TEST_PLATFORM # purge_rnd_workflow # Remove a workflow source dir and rundir. # Typically used with make_rnd_workflow # skip N SKIP_REASON # echo "ok $((++T)) # skip SKIP_REASON" N times. # skip_all SKIP_REASON # echo "1..0 # SKIP SKIP_REASON" and exit. # ssh_install_cylc HOST # Install cylc on a remote host. # reftest [TEST_NAME [SOURCE]] # Install a reference workflow using `install_workflow`, run a validation # test on the workflow and run the reference workflow with `workflow_run_ok`. # Expect 2 OK tests. # install_and_validate # The first part of reftest, to allow separate use. # Expect 1 OK test. # reftest_run # The guts of reftest, to allow separate use. # Expect 1 OK test. # create_test_global_config [PRE [POST]] # Create a new global config file $PWD/etc from global-tests.cylc # with PRE and POST pre- and ap-pended. # PRE and POST are strings. # The global config starts with #!Jinja2 in case Jinja2 is used. # localhost_fqdn # Get the FQDN of the current host using the same mechanism Cylc uses. # get_fqdn [TARGET] # SSH to TARGET and return `hostname -f`. # dump_std TEST_NAME # Dump stdout and stderr of TEST_NAME to the test log dir. # delete_db [WORKFLOW_RUN_DIR] # Delete private/public workflow databases for a cold start. # # VARIABLES # LOG_INDENT # indentation width of secondary log lines # # TODO: document other variables used by test scripts # #------------------------------------------------------------------------------- set -eu shopt -s extglob # For indented secondary log lines; must match the width in cylc.flow.loggingutil export LOG_INDENT=" " FAILURES=0 SIGNALS="EXIT INT" TEST_DIR= TEST_RHOST_CYLC_DIR= FINALLY() { for S in ${SIGNALS}; do trap '' "${S}" done if [[ -n "${TEST_DIR}" ]]; then cd ~ rm -rf "${TEST_DIR}" fi if [[ -n "${TEST_RHOST_CYLC_DIR}" ]]; then # TEST_RHOST_CYLC_DIR is a local variable of this script # shellcheck disable=SC2029 ssh -oBatchMode=yes -oConnectTimeout=5 "${TEST_RHOST_CYLC_DIR%%:*}" \ "rm -fr ${TEST_RHOST_CYLC_DIR#*:}" fi if [[ -n "${TEST_SMTPD_PID:-}" ]]; then kill "${TEST_SMTPD_PID}" fi if ((FAILURES > 0)); then echo -e "\n stdout and stderr stored in: ${TEST_LOG_DIR}" >&2 if "${WORKFLOW_RUN_FAILS}"; then echo -e " workflow logs can be found under: ${WORKFLOW_LOG_DIR}/" >&2 fi fi } for S in ${SIGNALS}; do # Signal as argument to FINALLY # shellcheck disable=SC2064 trap "FINALLY ${S}" "${S}" done TEST_NUMBER=0 if command -v lsof >/dev/null; then HAS_LSOF=true else HAS_LSOF=false fi set_test_number() { echo "1..$1" } ok() { echo "ok $((++TEST_NUMBER)) - $*" } fail() { ((++FAILURES)) echo "not ok $((++TEST_NUMBER)) - $*" if [[ -n "${CYLC_TEST_DEBUG:-}" ]]; then echo >'/dev/tty' echo "${TEST_NAME_BASE} ${TEST_NAME}" >'/dev/tty' cat "${TEST_NAME}.stderr" >'/dev/tty' fi } dump_std() { local TEST_NAME="$1" mkdir -p "${TEST_LOG_DIR}" # directory may not exist if run fails very early if [[ -s "${TEST_NAME}.stdout" ]]; then cp -p "${TEST_NAME}.stdout" "${TEST_LOG_DIR}/${TEST_NAME}.stdout" fi cp -p "${TEST_NAME}.stderr" "${TEST_LOG_DIR}/${TEST_NAME}.stderr" } run_ok() { local TEST_NAME="$1" shift 1 if ! "$@" 1>"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr"; then fail "${TEST_NAME}" dump_std "${TEST_NAME}" return fi ok "${TEST_NAME}" } run_fail() { local TEST_NAME="$1" shift 1 if "$@" 1>"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr"; then fail "${TEST_NAME}" dump_std "${TEST_NAME}" return fi ok "${TEST_NAME}" } run_graphql_ok () { # this is a thin wrapper to `cylc client` which strips newlines from $2 to # make it valid JSON local TEST_NAME="$1" local WORKFLOW="$2" local JSON="$3" # shellcheck disable=SC2086 run_ok "${TEST_NAME}" cylc client \ "${WORKFLOW}" \ 'graphql' \ < <(echo ${JSON}) } workflow_run_ok() { local TEST_NAME="$1" shift 1 if "$@" 1>"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr"; then ok "${TEST_NAME}" return fi fail "${TEST_NAME}" WORKFLOW_RUN_FAILS=true WORKFLOW_LOG_DIR="${WORKFLOW_RUN_DIR}/log/scheduler" mkdir -p "${WORKFLOW_LOG_DIR}" # directory may not exist if run fails very early dump_std "${TEST_NAME}" } workflow_run_fail() { local TEST_NAME="$1" shift 1 if ! "$@" 1>"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr"; then ok "${TEST_NAME}" return fi fail "${TEST_NAME}" WORKFLOW_RUN_FAILS=true WORKFLOW_LOG_DIR="${WORKFLOW_RUN_DIR}/log/scheduler" mkdir -p "${WORKFLOW_LOG_DIR}" # directory may not exist if run fails very early dump_std "${TEST_NAME}" } cmp_ok() { local FILE_TEST="$1" local FILE_CONTROL="${2:--}" local TEST_NAME TEST_NAME="$(basename "${FILE_TEST}")-cmp-ok" local DIFF_CMD=${CYLC_TEST_DIFF_CMD:-'diff -u'} if ${DIFF_CMD} "${FILE_CONTROL}" "${FILE_TEST}" >"${TEST_NAME}.stderr" 2>&1 then ok "${TEST_NAME}" return else cat "${TEST_NAME}.stderr" >&2 fi dump_std "$TEST_NAME" fail "${TEST_NAME}" } cmp_ok_re() { local FILE_TEST="$1" local FILE_CONTROL="${2:--}" local TEST_NAME TEST_NAME="$(basename "${FILE_TEST}")-cmp-ok" if python3 -c ' import re import sys def compare(test_file, control_file): for ind, (test_line, control_line) in enumerate( zip(test_file, control_file) ): if test_line is None: sys.stderr.write("Test file is missing lines") sys.exit(1) if control_line is None: sys.stderr.write("Test file has extra lines") sys.exit(1) if not re.match(control_line, test_line): sys.stderr.write( f"Line {ind+1}: \"{test_line}\" does not match /{control_line}/" .replace("\n", "") ) sys.exit(1) test_filename, control_filename = sys.argv[1:3] with open(test_filename, "r") as test_file: if control_filename == "-": compare(test_file, sys.stdin) else: with open(control_filename, "r") as control_file: compare(test_file, control_file) ' "${FILE_TEST}" "${FILE_CONTROL}"; then ok "${TEST_NAME}" else ( echo -e '\n# Test file:' cat "${FILE_TEST}" ) >&2 fail "${TEST_NAME}" fi } cmp_json() { local TEST_NAME="$1" local FILE_TEST="$2" local FILE_CONTROL="${3:--}" if python3 "$CYLC_REPO_DIR/tests/f/lib/python/diffr.py" \ "$(cat "$FILE_CONTROL")" "$(cat "$FILE_TEST")" \ >"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr" then ok "${TEST_NAME}" return else cat "${TEST_NAME}.stderr" >&2 fi dump_std "$TEST_NAME" fail "${TEST_NAME}" } contains_ok() { local FILE_TEST="$1" local FILE_CONTROL="${2:--}" local TEST_NAME TEST_NAME="$(basename "${FILE_TEST}")-contains-ok" LANG=C comm -13 <(LANG=C sort "${FILE_TEST}") <(LANG=C sort "${FILE_CONTROL}") \ 1>"${TEST_NAME}.stdout" 2>"${TEST_NAME}.stderr" if [[ -s "${TEST_NAME}.stdout" ]]; then mkdir -p "${TEST_LOG_DIR}" { echo 'Missing lines:' cat "${TEST_NAME}.stdout" echo 'Test File:' cat "${FILE_TEST}" } >>"${TEST_NAME}.stderr" cp -p "${TEST_NAME}.stderr" "${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" return fi ok "${TEST_NAME}" } count_ok() { local BRE="$1" local FILE="$2" local COUNT="$3" local TEST_NAME NEW_COUNT TEST_NAME="$(basename "${FILE}")-count-ok" NEW_COUNT=$(grep -c "${BRE}" "${FILE}") if (( NEW_COUNT == COUNT )); then ok "${TEST_NAME}" return fi mkdir -p "${TEST_LOG_DIR}" echo "Found ${NEW_COUNT} (not ${COUNT}) of ${BRE} in ${FILE}" \ >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" } grep_ok() { # (Put extra grep options like '-E' at end of the command line) local BRE="$1" local FILE="$2" shift 2 OPTS="$*" named_grep_ok "$(basename "${FILE}")-grep-ok" "$BRE" "$FILE" "$OPTS" } grep_workflow_log_ok() { local TEST_NAME="$1" local PATTERN="$2" shift 2 OPTS="$*" local LOG_FILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # shellcheck disable=SC2086 if grep ${OPTS} -s -e "$PATTERN" "$LOG_FILE"; then ok "${TEST_NAME}" return fi mkdir -p "${TEST_LOG_DIR}" { cat <<__ERR__ Can't find: =========== ${PATTERN} =========== in: =========== __ERR__ cat "$LOG_FILE" } >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" } named_grep_ok() { # (Put extra grep options like '-E' at end of the command line) local NAME="$1" local BRE="$2" local FILE="$3" shift 3 local OPTS="$*" local TEST_NAME TEST_NAME="grep-ok: ${NAME}" # shellcheck disable=SC2086 if grep ${OPTS} -q -e "${BRE}" "${FILE}"; then ok "${TEST_NAME}" return fi mkdir -p "${TEST_LOG_DIR}" { cat <<__ERR__ Can't find: =========== ${BRE} =========== in: =========== __ERR__ cat "${FILE}" } >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" } grep_fail() { local BRE="$1" local FILE="$2" shift 2 local OPTS="$*" local TEST_NAME TEST_NAME="$(basename "${FILE}")-grep-fail" if [[ ! -f "${FILE}" ]]; then fail "${TEST_NAME}-file to be grepped does not exist." return fi # shellcheck disable=SC2086 if grep ${OPTS} -q -e "${BRE}" "${FILE}"; then mkdir -p "${TEST_LOG_DIR}" echo "ERROR: Found ${BRE} in ${FILE}" \ >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" return fi ok "${TEST_NAME}" } exists_ok() { local FILE="$1" local TEST_NAME TEST_NAME="$(basename "${FILE}")-file-exists-ok" if [[ -e $FILE ]]; then ok "${TEST_NAME}" return fi mkdir -p "${TEST_LOG_DIR}" echo "Does not exist: ${FILE}" >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" } exists_fail() { local FILE="$1" local TEST_NAME TEST_NAME="$(basename "${FILE}")-file-exists-fail" if [[ ! -e "${FILE}" ]]; then ok "${TEST_NAME}" return fi mkdir -p "${TEST_LOG_DIR}" echo "Exists: ${FILE}" >"${TEST_LOG_DIR}/${TEST_NAME}.stderr" fail "${TEST_NAME}" } graph_workflow() { # Generate a graphviz "plain" format graph of a workflow. local WORKFLOW_NAME="${1}" local OUTPUT_FILE="${2}" shift 2 mkdir -p "$(dirname "${OUTPUT_FILE}")" cylc graph --reference "${WORKFLOW_NAME}" "$@" \ >"${OUTPUT_FILE}" 2>"${OUTPUT_FILE}".err } workflow_id() { local TEST_NAME="${1:-${TEST_NAME_BASE}}" local TEST_SOURCE_BASE="${2:-${TEST_NAME}}" echo "${CYLC_TEST_REG_BASE}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME}" } init_workflow() { local TEST_NAME="$1" local FLOW_CONFIG="${2:--}" local RUN_NUMS="${3:-false}" WORKFLOW_NAME="$(workflow_id "${TEST_NAME}")" WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" mkdir -p "${TEST_DIR}/${WORKFLOW_NAME}/" cat "${FLOW_CONFIG}" >"${TEST_DIR}/${WORKFLOW_NAME}/flow.cylc" cd "${TEST_DIR}/${WORKFLOW_NAME}" if "${RUN_NUMS}"; then cylc install "${TEST_DIR}/${WORKFLOW_NAME}" --workflow-name="${WORKFLOW_NAME}" else cylc install "${TEST_DIR}/${WORKFLOW_NAME}" --no-run-name --workflow-name="${WORKFLOW_NAME}" fi } install_workflow() { local TEST_NAME="${1:-${TEST_NAME_BASE}}" local TEST_SOURCE_BASE="${2:-${TEST_NAME}}" local RUN_NUMS="${3:-false}" WORKFLOW_NAME="$(workflow_id "${TEST_NAME}" "${TEST_SOURCE_BASE}")" WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" mkdir -p "${TEST_DIR}/${WORKFLOW_NAME}/" # make source dir cp -r "${TEST_SOURCE_DIR}/${TEST_SOURCE_BASE}/"* "${TEST_DIR}/${WORKFLOW_NAME}/" cd "${TEST_DIR}/${WORKFLOW_NAME}" if "$RUN_NUMS"; then cylc install "${TEST_DIR}/${WORKFLOW_NAME}" --workflow-name="${WORKFLOW_NAME}" else cylc install "${TEST_DIR}/${WORKFLOW_NAME}" --no-run-name --workflow-name="${WORKFLOW_NAME}" fi } log_scan () { local TEST_NAME="$1" local FILE="$2" local REPS=$3 local DELAY=$4 local OPTS=${LOG_SCAN_GREP_OPTS:-} if ${CYLC_TEST_DEBUG:-false}; then local ERR=2 else local ERR=1 fi shift 4 local count=0 local success=false local position=0 local newposition= for pattern in "$@"; do count=$(( count + 1 )) success=false echo -n "scanning for '${pattern:0:30}'" >& $ERR for _ in $(seq 1 "${REPS}"); do echo -n '.' >& $ERR # shellcheck disable=SC2086 newposition=$(grep -n $OPTS "$pattern" "$FILE" | \ tail -n 1 | cut -d ':' -f 1) if (( newposition > position )); then position=$newposition echo ': succeeded' >& $ERR ok "${TEST_NAME}-${count}" success=true break fi sleep "${DELAY}" done shift if ! "${success}"; then echo ': failed' >& $ERR fail "${TEST_NAME}-${count}" if "${CYLC_TEST_DEBUG:-false}"; then cat "${FILE}" >&2 fi mkdir -p "${TEST_LOG_DIR}" ERR_FILE="${TEST_LOG_DIR}/${TEST_NAME}.stderr" echo "ERR_FILE=$ERR_FILE" >&2 echo "Could not find: ${pattern}" > "${ERR_FILE}" echo -e '\n=======================\n' >> "${ERR_FILE}" cat "${FILE}" >> "${ERR_FILE}" skip "$#" return 1 fi done } make_rnd_workflow() { # Create a randomly-named workflow source directory. # Define its run directory. # TODO: export a WORKFLOW_NAME variable so tests can get cylc-install # to install under the regular test hierarchy RND_WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" RND_WORKFLOW_SOURCE="$PWD/${RND_WORKFLOW_NAME}" mkdir -p "${RND_WORKFLOW_SOURCE}" touch "${RND_WORKFLOW_SOURCE}/flow.cylc" RND_WORKFLOW_RUNDIR="${RUN_DIR}/${RND_WORKFLOW_NAME}" export RND_WORKFLOW_NAME export RND_WORKFLOW_SOURCE export RND_WORKFLOW_RUNDIR } # shellcheck disable=2120 # these arguments are intentionally optional purge () { # workflow to remove - defaults to $WORKFLOW_NAME local WORKFLOW_NAME="${1:-$WORKFLOW_NAME}" # job platform to remove it on - defaults to $CYLC_TEST_PLATFORM local PLATFORM="${2:-$CYLC_TEST_PLATFORM}" if ((FAILURES != 0)); then # if tests have failed then don't clean up return 0 fi if [[ -z $WORKFLOW_NAME ]]; then echo 'no flow to purge' >&2 return 1 fi local WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" # wait for local processes to let go of their file handles if ${HAS_LSOF}; then # NOTE: lsof can hang, so call with "timeout". # NOTE: lsof can raise warnings with some filesystems so ignore stderr # shellcheck disable=SC2034 for try in $(seq 1 5); do if ! grep -q "${WORKFLOW_RUN_DIR}" < <(timeout 5 lsof 2>/dev/null); then break fi sleep 1 done fi # shellcheck disable=SC2016 # shellcheck disable=SC2089 local CMD=' cd '~/cylc-run' && rm -rf "'"$WORKFLOW_NAME"'" && rmdir -p "$(dirname "'"$WORKFLOW_NAME"'")" ' # remove the workflow run directory on the platform if remote if [[ -n "$PLATFORM" && "${PLATFORM}" != _local* ]]; then local HOST HOST="$(get_host_from_platform "$PLATFORM")" # shellcheck disable=SC2029 # we want this to expand client side (ssh "$HOST" "$CMD" 2>/dev/null) || true fi # remove the workflow run directory tree locally - if empty # NOTE: do this after remote purge as this removes the test config too bash -c "$CMD" 2>/dev/null || true } purge_rnd_workflow() { if ((FAILURES != 0)); then # if tests have failed then don't clean up return 0 fi # Remove the workflow source created by make_rnd_workflow(). # And remove its run-directory too. rm -rf "${RND_WORKFLOW_SOURCE}" rm -rf "${RND_WORKFLOW_RUNDIR}" } poll() { local TIMEOUT="$(($(date +%s) + 60))" # wait 1 minute while ! "$@"; do sleep 1 if (($(date +%s) > TIMEOUT)); then echo "ERROR: poll timed out: $*" >&2 exit 1 fi done } poll_grep() { poll grep -s "$@" } poll_grep_workflow_log() { poll grep -s "$@" "${WORKFLOW_RUN_DIR}/log/scheduler/log" } poll_pid_done() { local TIMEOUT="$(($(date +%s) + 60))" # wait 1 minute while ps --no-headers "$@" 1>'/dev/null' 2>&1; do sleep 1 if (($(date +%s) > TIMEOUT)); then echo "ERROR: poll timed out: ! ps --no-headers $*" >&2 exit 1 fi done } poll_workflow_running() { poll test -e "${WORKFLOW_RUN_DIR}/.service/contact" } poll_workflow_stopped() { poll test '!' -e "${WORKFLOW_RUN_DIR}/.service/contact" } get_workflow_uuid() { sed -n 's|CYLC_WORKFLOW_UUID=\(.*\)|\1|p' "${WORKFLOW_RUN_DIR}/.service/contact" 2>/dev/null } poll_workflow_restart() { TIMEOUT="${1:-10}" STEP=1 UUID="${2:-$(get_workflow_uuid)}" TIME=0 while true; do if [[ ${TIME} -gt $TIMEOUT ]]; then return 1 fi if [[ $(get_workflow_uuid) != "${UUID}" ]]; then return 0 else TIME=$(( TIME + STEP )) sleep "${STEP}" fi done } skip() { local N_TO_SKIP="$1" shift 1 local COUNT=0 while ((COUNT++ < N_TO_SKIP)); do echo "ok $((++TEST_NUMBER)) # skip $*" done } skip_all() { echo "1..0 # SKIP $*" exit } skip_macos_gh_actions() { # https://github.com/cylc/cylc-flow/issues/6276 if [[ "${CI:-}" && "$OSTYPE" == "darwin"* ]]; then skip_all "Skipped due to performance issues on GH Actions MacOS runner" fi } ssh_install_cylc() { local RHOST="$1" local RHOST_CYLC_DIR= RHOST_CYLC_DIR=$(_ssh_mkdtemp_cylc_dir "${RHOST}") TEST_RHOST_CYLC_DIR="${RHOST}:${RHOST_CYLC_DIR}" rsync -a '--exclude=*.pyc' "${CYLC_REPO_DIR}"/* "${RHOST}:${RHOST_CYLC_DIR}/" # RHOST_CYLC_DIR is a variable of this function # shellcheck disable=SC2029 ssh -n -oBatchMode=yes -oConnectTimeout=5 "${RHOST}" \ "make -C '${RHOST_CYLC_DIR}' 'version'" 1>'/dev/null' 2>&1 } _ssh_mkdtemp_cylc_dir() { local RHOST="$1" ssh -oBatchMode=yes -oConnectTimeout=5 "${RHOST}" python3 - <<'__PYTHON__' import os from tempfile import mkdtemp print(mkdtemp(dir=os.path.expanduser("~"), prefix="cylc-")) __PYTHON__ } mock_smtpd_init() { # Logic borrowed from Rose local SMTPD_PORT= # Try several ports in case any are in use: for SMTPD_PORT in 8025 8125 8225 8325 8425 8525 8625 8725 8825 8925; do local SMTPD_LOG="${TEST_DIR}/smtpd.log" local SMTPD_HOST="localhost:${SMTPD_PORT}" # Set up fake SMTP server to catch outgoing mail & redirect to log: python3 -u -m 'aiosmtpd' \ --class aiosmtpd.handlers.Debugging stdout \ --debug --nosetuid \ --listen "${SMTPD_HOST}" \ 1>"${SMTPD_LOG}" 2>&1 & # Runs in background local SMTPD_PID="$!" while ! grep -q 'is listening' "${SMTPD_LOG}" 2>'/dev/null' do if ps "${SMTPD_PID}" 1>/dev/null 2>&1; then sleep 1 # Still waiting for fake server to start else # Server process failed rm -f "${SMTPD_LOG}" unset SMTPD_HOST SMTPD_LOG SMTPD_PID break fi done if [[ -n "${SMTPD_PID:-}" ]]; then # Variables used by tests # shellcheck disable=SC2034 TEST_SMTPD_HOST="${SMTPD_HOST}" # shellcheck disable=SC2034 TEST_SMTPD_LOG="${SMTPD_LOG}" TEST_SMTPD_PID="${SMTPD_PID}" break fi done } mock_smtpd_kill() { # Logic borrowed from Rose if [[ -n "${TEST_SMTPD_PID:-}" ]] && ps "${TEST_SMTPD_PID}" >'/dev/null' 2>&1 then kill "${TEST_SMTPD_PID}" wait "${TEST_SMTPD_PID}" 2>/dev/null || true unset TEST_SMTPD_HOST TEST_SMTPD_LOG TEST_SMTPD_PID fi } install_and_validate() { # First part of the reftest function, to allow separate use. # Expect 1 OK test. local TEST_NAME="${1:-${TEST_NAME_BASE}}" install_workflow "$@" run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" } reftest_run() { # Guts of the reftest function, to allow separate use. # Expect 1 OK test. local TEST_NAME="${1:-${TEST_NAME_BASE}}-run" if [[ -n "${REFTEST_OPTS:-}" ]]; then # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach \ ${REFTEST_OPTS} "${WORKFLOW_NAME}" else workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach \ "${WORKFLOW_NAME}" fi } reftest() { # Install, validate, run, and purge, a reference test. # Expect 2 OK tests. install_and_validate "$@" reftest_run "$@" # shellcheck disable=SC2119 purge } _get_test_config_file() { # print the path to the most specific global-tests.cylc on the system # (or print nothing if no test configs are present) python3 -c " from pathlib import Path from cylc.flow.cfgspec.globalcfg import get_version_hierarchy for version in reversed(get_version_hierarchy('$(cylc version)')): path = Path('${HOME}/.cylc/flow', version, 'global-tests.cylc') if path.exists(): print(path) break " } create_test_global_config() { # (Documented in file header). local PRE= local POST= if (( $# == 1 )); then PRE=$1 elif (( $# == 2 )); then PRE=$1 POST=$2 elif (( $# > 2 )); then echo 'ERROR, create_test_global_config: too many args' >&2 exit 1 fi # Tidy in case of previous use of this function. rm -fr 'etc' mkdir 'etc' # Start with Jinja2 hashbang. Harmless if not needed. echo "#!Jinja2" >'etc/global.cylc' echo "$PRE" >>'etc/global.cylc' # add defaults cat >>'etc/global.cylc' <<__HERE__ # set a default timeout for all flow runs to avoid hanging tests [scheduler] [[events]] inactivity timeout = PT5M stall timeout = PT5M abort on inactivity timeout = true abort on workflow timeout = true [install] max depth = 5 __HERE__ # add global-tests.cylc USER_TESTS_CONF_FILE="$(_get_test_config_file)" if [[ -n "${USER_TESTS_CONF_FILE}" ]]; then cat "${USER_TESTS_CONF_FILE}" >>'etc/global.cylc' fi # add platform config if [[ -n "${CYLC_TEST_PLATFORM:-}" ]]; then _add_platform_to_test_global_conf "$CYLC_TEST_PLATFORM" fi echo "$POST" >>'etc/global.cylc' export CYLC_CONF_PATH="${PWD}/etc" } _parse_platform_spec () { # the requirement string: e.g. comms:tcp local requirement="$1" local locality='local' local job_runner='*' local file_system='*' local comms='*' for req in ${requirement}; do case "$req" in loc:*) locality="${req/loc:/}" ;; runner:*) job_runner="${req/runner:/}" ;; fs:*) file_system="${req/fs:/}" ;; comms:*) comms="${req/comms:/}" ;; *) echo "Invalid option $req" >&2 return 1 ;; esac done echo "_${locality}_${job_runner}_${file_system}_${comms}" } _get_test_platform () { # the requirement string: e.g. comms:tcp local requirement="$1" local platform local required_platform required_platform="$(_parse_platform_spec "${requirement}")" for platform in "${PLATFORMS[@]}"; do # we are purposefully allowing pattern matching here # shellcheck disable=SC2053 if [[ "$platform" == $required_platform ]]; then CYLC_TEST_PLATFORM="$platform" CYLC_TEST_JOB_RUNNER="$(cut -d '_' -f 3 <<< "$platform")" export CYLC_TEST_PLATFORM CYLC_TEST_JOB_RUNNER return 0 fi done return 1 } _get_test_host () { # extract a test host from the test platform # NOTE: this will cause a failure if the platform is not configured CYLC_TEST_HOST="$(get_host_from_platform "$CYLC_TEST_PLATFORM")" if [[ -z "${CYLC_TEST_HOST}" ]]; then echo "Could not find host for platform $CYLC_TEST_PLATFORM" >&2 exit 1 fi export CYLC_TEST_HOST } _get_test_install_target () { # extract a test host from the test platform # NOTE: this will cause a failure if the platform is not configured CYLC_TEST_INSTALL_TARGET="$( get_install_target_from_platform "$CYLC_TEST_PLATFORM" )" if [[ -z "${CYLC_TEST_INSTALL_TARGET}" ]]; then CYLC_TEST_INSTALL_TARGET="$CYLC_TEST_PLATFORM" fi export CYLC_TEST_INSTALL_TARGET } get_host_from_platform () { local platform="$1" cylc config -i "[platforms][${platform}]hosts" \ | cut -d ',' -f 1 } get_install_target_from_platform () { local platform="$1" cylc config -d -i "[platforms][${platform}]install target" \ | cut -d ',' -f 1 } localhost_fqdn () { python -c " from cylc.flow.hostuserutil import get_fqdn_by_host print(get_fqdn_by_host(None)) " } get_fqdn () { local HOST="${1}" ssh "$HOST" hostname -f } _get_test_platforms () { CYLC_CONF_PATH='' cylc config -i '[platforms]' \ | sed -n 's/\[\[\(_.*\)\]\]/\1/p' } _expand_test_platforms () { # convert sting of space separated platforms to array read -r -a PLATFORMS <<< "${1}" local ret=() local pattern local platform local all_platforms # get list of all configured test platforms # NOTE: when we drop bash3 convert to # mapfile -t all_platforms < <(_get_test_platforms) all_platforms=() while IFS='' read -r line; do all_platforms+=("$line"); done < <(_get_test_platforms) # perform pattern matching on the PLATFORMS variable if [[ "${#PLATFORMS[@]}" -eq 0 ]]; then # default to all local platforms PLATFORMS=('_local_background*' '_local_at*') fi # perform pattern matching for pattern in "${PLATFORMS[@]}"; do for platform in "${all_platforms[@]}"; do # shellcheck disable=SC2053 # we are purposefully performing glob matching here if [[ ${platform} == ${pattern} ]]; then ret+=("${platform}") fi done done # set PLATFORMS to the unique results # NOTE: when we drop bash3 convert to # mapfile -t PLATFORMS < <( PLATFORMS=() while IFS='' read -r line; do PLATFORMS+=("$line"); done < <( for platform in "${ret[@]}"; do echo "${platform}"; done | sort -u ) export PLATFORMS } _check_test_requirements () { # check the test requirements against the array of test platforms if [[ -z "${REQUIRE_PLATFORM:-}" ]]; then # if no requirement string is provided default to local, background REQUIRE_PLATFORM='loc:local runner:background' fi if ! _get_test_platform "${REQUIRE_PLATFORM}"; then skip_all "requires $REQUIRE_PLATFORM" fi _add_platform_to_test_global_conf "$CYLC_TEST_PLATFORM" } _add_platform_to_test_global_conf () { # add a test platform from the global config to the test global config # NOTE: Uses global.cylc NOT global-tests.cylc # Do not configure test platforms in the global-tests.cylc file PLATFORM="$1" cat >> "${CYLC_CONF_PATH}/global.cylc" <<__HERE__ [platforms] [[$PLATFORM]] $( CYLC_CONF_PATH='' cylc config \ -i "[platforms][$PLATFORM]" \ | sed 's/^/ /' ) __HERE__ } delete_db() { # Delete private/public workflow databases for a cold start local WORKFLOW_RUN_DIR="${1:-$WORKFLOW_RUN_DIR}" rm "${WORKFLOW_RUN_DIR}/.service/db" "${WORKFLOW_RUN_DIR}/log/db" } # tags PATH="${CYLC_REPO_DIR}/bin:${PATH}" RUN_DIR="${HOME}/cylc-run" export RUN_DIR TEST_NAME_BASE="$(basename "$0" '.t')" TEST_SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)" TEST_DIR="$(mktemp -d)" cd "${TEST_DIR}" TEST_SOURCE_DIR_BASE="${TEST_SOURCE_DIR##*tests/}" # this prevents test runs started at the same time from conflicting TEST_LOG_DIR_BASE="${TMPDIR:-/tmp}/${USER}/${CYLC_TEST_REG_BASE}" TEST_LOG_DIR="${TEST_LOG_DIR_BASE}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME_BASE}" WORKFLOW_RUN_FAILS=false CYLC_TEST_SKIP="${CYLC_TEST_SKIP:-}" # Is this test in the skip list? THIS="${0#./}" THIS_DIR="$(dirname "${THIS}")" for SKIP in ${CYLC_TEST_SKIP}; do RSKIP="${SKIP#./}" if [[ "${THIS}" == "${RSKIP}" || "${THIS_DIR%/}" == "${RSKIP%/}" ]]; then # Deliberately print variable substitution syntax unexpanded # shellcheck disable=SC2016 skip_all 'this test is in $CYLC_TEST_SKIP.' fi done # Ignore the normal site/user global config, use global-tests.cylc. create_test_global_config "$@" # get array of test platforms to run tests with _expand_test_platforms "${PLATFORMS}" # run or skip tests based on the test requirements and provided test platforms # provides: CYLC_TEST_PLATFORM _check_test_requirements # extract a test host from the test platform # provides: CYLC_TEST_HOST _get_test_host # extract a test install target from the test platform # provides: CYLC_TEST_INSTALL_TARGET _get_test_install_target if ${CYLC_SHOW_TEST_EXECUTION:-false}; then # write the name of tests as they are run # (helpful for debugging hanging tests) echo "# $TEST_NAME_BASE" >&2 fi set +x set +e cylc-flow-8.6.4/tests/functional/lib/python/0000775000175000017500000000000015202510242021200 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/lib/python/diffr.py0000664000175000017500000002265615202510242022657 0ustar alastairalastair# 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 . """Quick and simple JSON diff util. Examples: >>> bool(Diff({}, {})) True >>> diff = Diff({'a': 1, 'b': 2}, ... {'b': 3, 'd': 4}) >>> bool(diff) False >>> list(diff.added()) [(['d'], 4)] >>> list(diff.removed()) [(['a'], 1)] >>> list(diff.modified()) [(['b'], 2, 3)] >>> diff = Diff({'x': {'y': {'z': 1}}}, ... {'x': {'y': {'z': 2}}}) >>> list(diff.modified()) [(['x', 'y', 'z'], 1, 2)] """ import argparse import json import sys from typing import Callable, Optional, Tuple, Type class Diff: """Representation of a diff between two dictionaries.""" BRACES = { dict: ('{', '}'), list: ('[', ']') } def __init__(self, this, that, this_name='expected', that_name='got'): self.typ, self.changed = self.compute_diff(this, that) self.this_name = this_name self.that_name = that_name @classmethod def _diff_method( cls, this: object, that: object ) -> Tuple[Optional[Type], Optional[Callable]]: if isinstance(this, list) and isinstance(that, list): return list, cls.diff_list if isinstance(this, dict) and isinstance(that, dict): return dict, cls.diff_dict return None, None @classmethod def compute_diff(cls, this, that): """Return a list of differences between this and that. Entries take the form:: (symbol, (key, *value)) Where: symbol: * ``+``: for items in that but not in this. * ``-``: for items in this but not in that. * ``?``: for items common to both but with different values. key: * ``dict``: The key of a key, value pair. * ``list``: The index of an element. value: A tuple containing information about the change: * ``+ (key, value)`` * ``- (key, value)`` * ``? (key, this_value, that_value)`` """ typ, meth = cls._diff_method(this, that) if meth: return typ, meth(this, that) raise TypeError('%s Cannot compare %s and %s' % ( cls.__name__, type(this), type(that))) @classmethod def diff_list(cls, this, that): """Return differences between two lists. So unsurprisingly differencing nested lists is actually a little tricky so this is the simple and crude method, just mark the items as modified rather than going through the mess of working out how the lists could fit together. """ changed = [] for index, (this_item, that_item) in enumerate(zip(this, that)): if this_item != that_item: if cls._diff_method(this_item, that_item): changed.append((index, cls(this_item, that_item))) else: changed.append(('?', (index, this_item, that_item))) if len(this) > len(that): symbol = '-' additional = this else: symbol = '+' additional = that for index in range(min(len(this), len(that)), max(len(this), len(that))): changed.append((symbol, (index, additional[index],))) return changed @classmethod def diff_dict(cls, this, that): """Return differences between two dictionaries. As this is a JSON comparison tool ignore the order of keys. """ changed = [] for key, value in that.items(): if key not in this: changed.append(('+', (key, value))) elif isinstance(value, (dict, list)): if cls._diff_method(this[key], that[key]): this[key] = cls(this[key], that[key]) if this[key].changed: changed.append((key, this[key])) else: changed.append(('?', (key, this[key], value))) elif value != this[key]: changed.append(('?', (key, this[key], value))) for key, value in this.items(): if key not in that: changed.append(('-', (key, value))) return changed def __str__(self): return self.tostr() def tostr(self, indent=0): """Return unified(ish) diff.""" ret = '' if indent == 0: ret += '--- %s\n' % self.this_name ret += '+++ %s\n' % self.that_name ret += '============\n' ret += ' %s\n' % self.BRACES[self.typ][0] pre = ' ' * indent for itt, (symbol, item) in enumerate(self.changed): post = '' if itt != len(self.changed) - 1: post = ',' if isinstance(item, Diff): diff = item key = symbol ret += ' %s %s: %s\n' % (pre, key, self.BRACES[diff.typ][0]) ret += diff.tostr(indent + 1) ret += ' %s %s%s\n' % (pre, self.BRACES[diff.typ][1], post) else: if symbol == '?': key, before, after = item ret += f'{symbol}{pre} {key}: {before} => {after}{post}\n' elif symbol in ['+', '-']: if len(item) == 2: value = '%s: %s' % item else: value = item[0] ret += f'{symbol}{pre} {value}{post}\n' if indent == 0: ret += ' %s\n' % self.BRACES[self.typ][1] return ret def added(self): """Yield items present in this but not in that as (key, value).""" yield from self.filter('+') def removed(self): """Yield items present in that but not in this as (key, value).""" yield from self.filter('-') def modified(self): """Yield items change as (key, this_value, that_value).""" yield from self.filter('?') def filter(self, *symbols): """Filter items by change status (+,-,?).""" for symbol, item in self.changed: if symbol in symbols: yield ([item[0]], *item[1:]) elif isinstance(item, Diff): yield from (([symbol, *res[0]], *res[1:]) for res in item.filter(*symbols)) def __bool__(self): return not bool(self.changed) def __len__(self): return len(self.changed) def load_json(file1, file2=None): """Read in JSON, return python data structure. If file2 is None read from sys.stdin """ try: this = json.loads(file1) except json.decoder.JSONDecodeError as exc: sys.exit(f'Syntax error in file1: {exc}') try: if file2: that = json.loads(file2) else: that = json.load(sys.stdin) except json.decoder.JSONDecodeError as exc: sys.exit(f'Syntax error in file2: {exc}') return this, that def parse_args(): """Return CLI args.""" parser = argparse.ArgumentParser() parser.add_argument('file1') parser.add_argument('file2', nargs='?') parser.add_argument( '-1', action='store', dest='name1', default='expected', help=( 'name file 1, default=expected')) parser.add_argument( '-2', action='store', dest='name2', default='got', help=( 'name file 2, default=got')) parser.add_argument( '-c1', '--contains1', action='store_const', const=1, default=0, dest='contains', help=( 'test all (key, value) pairs in file1 are in file2')) parser.add_argument( '-c2', '--contains2', action='store_const', const=2, default=0, dest='contains', help=( 'test all (key, value) pairs in file2 are in file1')) parser.add_argument( '--color', '--colour', action='store', default='never', help=( 'Use color? Option in always, never'), dest='color') args = parser.parse_args() return args def main(args): """Implement dictdiff.""" this, that = load_json(args.file1, args.file2) if this == that: # skip all diff logic if possible sys.exit(0) if args.contains: # test if items from one file present in the other if args.contains == 2: this, that = that, this args.name1, args.name2 = args.name2, args.name1 diff = Diff(this, that, args.name1, args.name2) for _ in diff.filter('-', '?'): sys.stderr.write(str(diff)) sys.exit(1) sys.exit(0) diff = Diff(this, that, args.name1, args.name2) if diff: sys.exit(0) else: sys.stderr.write(str(diff)) sys.exit(len(diff)) if __name__ == '__main__': main(parse_args()) cylc-flow-8.6.4/tests/functional/lib/python/test_diffr.py0000664000175000017500000002607715202510242023717 0ustar alastairalastair# 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 . """Test the diff.py interface.""" import json from subprocess import Popen, PIPE from textwrap import dedent import unittest from diffr import Diff, __file__ as SCRIPT class TestDiff(unittest.TestCase): @staticmethod def call_cli(this, that, *args): cmd = ( ['python', SCRIPT, json.dumps(this), json.dumps(that)] + list(args) ) proc = Popen(cmd, stdout=PIPE, stderr=PIPE) stdout, stderr = proc.communicate() return [proc.returncode, stdout.decode(), stderr.decode()] def test_dict_no_diff(self): for this in [ {'a': 1}, {'a': {'b': 1}}, {'a': {'b': 1, 'c': 2}}, {'a': [1, {'b': 2}]} ]: with self.subTest(this): self.assertTrue(Diff(this, this)) def test_dict_added(self): for this, that in [ ({}, {'b': 2}), ({'a': 1}, {'b': 2}), ({'a': 1}, {'a': 1, 'b': 2}) ]: with self.subTest((this, that)): diff = Diff(this, that) self.assertFalse(diff) self.assertEqual( list(diff.added()), [(['b'], 2)] ) def test_dict_removed(self): for this, that in [ ({}, {'b': 2}), ({'a': 1}, {'b': 2}), ({'a': 1}, {'a': 1, 'b': 2}) ]: with self.subTest((that, this)): diff = Diff(that, this) self.assertFalse(diff) self.assertEqual( list(diff.removed()), [(['b'], 2)] ) def test_dict_modified(self): for this, that in [ ({'a': 1}, {'a': 2}), ({'a': 1, 'b': 2}, {'a': 2, 'b': 2}), ]: with self.subTest((this, that)): diff = Diff(this, that) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [(['a'], 1, 2)] ) def test_dict_str(self): diff = Diff( {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 3, 'd': 4} ) self.assertFalse(diff) self.assertEqual( str(diff), dedent(''' +++ expected --- got ============ { ? b: 2 => 3, + d: 4, - c: 3 } ''').strip() + '\n' ) def test_dict_nested(self): diff = Diff( {'x': {'y': {'a': 1, 'b': 2, 'c': 3}}}, {'x': {'y': {'a': 1, 'b': 3, 'd': 4}}} ) self.assertFalse(diff) self.assertEqual( list(diff.added()), [(['x', 'y', 'd'], 4)] ) self.assertEqual( list(diff.removed()), [(['x', 'y', 'c'], 3)] ) self.assertEqual( list(diff.modified()), [(['x', 'y', 'b'], 2, 3)] ) self.assertEqual( str(diff), dedent(''' +++ expected --- got ============ { x: { y: { ? b: 2 => 3, + d: 4, - c: 3 } } } ''').strip() + '\n' ) def test_dict_types(self): diff = Diff( {'a': 4.2, 'b': False, 'c': 'a', 'd': 1, 'e': None}, {'a': 4.1, 'b': True, 'c': 'b', 'd': 2, 'e': 1} ) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [ (['a'], 4.2, 4.1), (['b'], False, True), (['c'], 'a', 'b'), (['d'], 1, 2), (['e'], None, 1) ] ) self.assertEqual( str(diff), dedent(''' +++ expected --- got ============ { ? a: 4.2 => 4.1, ? b: False => True, ? c: a => b, ? d: 1 => 2, ? e: None => 1 } ''').strip() + '\n' ) def test_dict_names(self): diff = Diff({'a': 1}, {'a': 2}, this_name='foo', that_name='bar') self.assertEqual( str(diff), dedent(''' +++ foo --- bar ============ { ? a: 1 => 2 } ''').strip() + '\n' ) def test_list_no_diff(self): for this in [ [1], [1, 2, 3], [1, {'a': 1}, 3] ]: with self.subTest(this): self.assertTrue(Diff(this, this)) def test_list_equal_length(self): diff = Diff([1, 2, 3], [4, 5, 6]) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [ ([0], 1, 4), ([1], 2, 5), ([2], 3, 6), ] ) diff = Diff([1, 2, 3], [3, 2, 1]) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [ ([0], 1, 3), ([2], 3, 1), ] ) def test_list_different_length(self): that, this = [1, 2, 3, 4, 5, 6], [-1, 2, 3] diff = Diff(that, this) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [ ([0], 1, -1), ] ) self.assertEqual( list(diff.added()), [] ) self.assertEqual( list(diff.removed()), [ ([3], 4), ([4], 5), ([5], 6) ] ) diff = Diff(this, that) self.assertFalse(diff) self.assertEqual( list(diff.modified()), [ ([0], -1, 1), ] ) self.assertEqual( list(diff.added()), [ ([3], 4), ([4], 5), ([5], 6) ] ) self.assertEqual( list(diff.removed()), [] ) def test_list_nested(self): diff = Diff( [1, [2, 3], 5, 6], [1, [2, 3, 4], 5] ) self.assertFalse(diff) self.assertEqual( list(diff.removed()), [ ([3], 6) ] ) self.assertEqual( list(diff.added()), [ ([1, 2], 4) ] ) self.assertEqual( list(diff.modified()), [] ) self.assertEqual( str(diff), dedent(''' +++ expected --- got ============ [ 1: [ + 2: 4 ], - 3: 6 ] ''').strip() + '\n' ) def test_mixed(self): diff = Diff( {'x': {'y': [0, 2, {'a': 1, 'b': 2, 'c': 3}]}}, {'x': {'y': [1, 2, {'a': 1, 'b': 3, 'd': 4}, 5]}} ) self.assertFalse(diff) self.assertEqual( list(diff.added()), [ (['x', 'y', 2, 'd'], 4), (['x', 'y', 3], 5) ] ) self.assertEqual( list(diff.removed()), [ (['x', 'y', 2, 'c'], 3), ] ) self.assertEqual( list(diff.modified()), [ (['x', 'y', 0], 0, 1), (['x', 'y', 2, 'b'], 2, 3) ] ) self.assertEqual( str(diff), dedent(''' +++ expected --- got ============ { x: { y: [ ? 0: 0 => 1, 2: { ? b: 2 => 3, + d: 4, - c: 3 }, + 3: 5 ] } } ''').strip() + '\n' ) def test_cli_no_diff(self): for this in [ {'a': 1}, {'a': {'b': 1}}, {'a': {'b': 1, 'c': 2}}, {'a': [1, {'b': 2}]}, [1, 2, 3], [1, {'a': 1}, 2] ]: ret, out, err = self.call_cli(this, this) self.assertEqual(ret, 0) self.assertEqual(out, '') self.assertEqual(err, '') def test_cli_nested(self): ret, out, err = self.call_cli( {'x': {'y': {'a': 1, 'b': 2, 'c': 3}}}, {'x': {'y': {'a': 1, 'b': 3, 'd': 4}}} ) self.assertEqual(ret, 1) self.assertEqual(out, '') self.assertEqual( err, dedent(''' +++ expected --- got ============ { x: { y: { ? b: 2 => 3, + d: 4, - c: 3 } } } ''').strip() + '\n' ) def test_cli_contains(self): this, that = ({'a': 1, 'b': 2}, {'a': 1}) ret, out, err = self.call_cli(this, that, '-c2') self.assertEqual(ret, 0) self.assertEqual(out, '') self.assertEqual(err, '') ret, out, err = self.call_cli(this, that, '-c1') # self.assertEqual(ret, 1) self.assertEqual(out, '') self.assertEqual( err, dedent(''' +++ expected --- got ============ { - b: 2 } ''').strip() + '\n' ) def test_cli_names(self): for args in [[], ['-c1']]: ret, out, err = self.call_cli( {'a': 1}, {'a': 2}, '-1', 'foo', '-2', 'bar', *args) self.assertEqual(ret, 1) self.assertEqual(out, '') self.assertEqual( err, dedent(''' +++ foo --- bar ============ { ? a: 1 => 2 } ''').strip() + '\n' ) cylc-flow-8.6.4/tests/functional/test_functions/0000775000175000017500000000000015202510242022160 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/test_functions/00-grep_fail.t0000664000175000017500000000267315202510242024522 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that grep_fail fails if test is not there: . "$(dirname "$0")/test_header" set_test_number 3 echo "The finger writes, and having writ moves on." > test_file.txt # It passes if file exists and search term not there: grep_fail 'foo' 'test_file.txt' # It fails if the file exists and the search term is there: grep_fail 'writ' 'test_file.txt' | tee "out1" > /dev/null grep_ok 'not ok 2 - test_file.txt-grep-fail' 'out1' # It fails if the file does not exist: grep_fail "writ" "test_file2.txt" | tee "out2" > /dev/null grep_ok 'not ok 3 - test_file2.txt-grep-fail-file to be grepped does not exist.' 'out2' cylc-flow-8.6.4/tests/functional/test_functions/test_header0000777000175000017500000000000015202510242030475 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-play/0000775000175000017500000000000015202510242021006 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-play/12-play-pause-paused.t0000664000175000017500000000310715202510242024753 0ustar alastairalastair#!/usr/bin/env bash # 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 . # # Test that running ``cylc play --pause`` on a paused workflow will _not_ # upause it, but will return a warning. # https://github.com/cylc/cylc-flow/issues/7006 . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = a __FLOW_CONFIG__ # It starts the workflow paused: run_ok "${TEST_NAME_BASE}-start-paused" \ cylc play "${WORKFLOW_NAME}" --pause # It fails to unpause the workflow: run_ok "${TEST_NAME_BASE}-start-paused-again" \ cylc play "${WORKFLOW_NAME}" --pause # It returns an informative error: grep_ok "Workflow already running: Remove --pause to resume" \ "${TEST_NAME_BASE}-start-paused-again.stderr" cylc stop "${WORKFLOW_NAME}" --now --now poll_workflow_stopped purge cylc-flow-8.6.4/tests/functional/cylc-play/02-format.t0000664000175000017500000000417415202510242022710 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # test the output of `cylc play` with different `--format` options . "$(dirname "$0")/test_header" set_test_number 8 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[dependencies]] R1 = foo __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" # format=plain TEST_NAME="${TEST_NAME_BASE}-run-format-plain" workflow_run_ok "${TEST_NAME}" cylc play --format plain "${WORKFLOW_NAME}" grep_ok "Copyright" "${TEST_NAME}.stdout" grep_ok "${WORKFLOW_NAME}:" "${TEST_NAME}.stdout" poll_workflow_running poll_workflow_stopped delete_db # format=json TEST_NAME="${TEST_NAME_BASE}-run-format-json" workflow_run_ok "${TEST_NAME}" cylc play --format json "${WORKFLOW_NAME}" run_ok "${TEST_NAME}-fields" python3 -c ' import json import sys data = json.load(open(sys.argv[1], "r")) print(list(sorted(data)), file=sys.stderr) assert list(sorted(data)) == [ "host", "pid", "pub_url", "url", "workflow"] ' "${TEST_NAME}.stdout" poll_workflow_running poll_workflow_stopped delete_db # quiet TEST_NAME="${TEST_NAME_BASE}-run-quiet" workflow_run_ok "${TEST_NAME}" cylc play --quiet "${WORKFLOW_NAME}" grep_ok "${WORKFLOW_NAME}:" "${TEST_NAME}.stdout" poll_workflow_running poll_workflow_stopped purge cylc-flow-8.6.4/tests/functional/cylc-play/07-timezones-compat.t0000664000175000017500000000363515202510242024724 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # Test for Timezone = Z # TODO remove deprecated suite.rc section at Cylc 8.x . "$(dirname "$0")/test_header" set_test_number 4 # integer cycling cat > suite.rc <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1000 [[dependencies]] [[[R1]]] graph = foo __FLOW_CONFIG__ WORKFLOW_NAME="${CYLC_TEST_REG_BASE}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME_BASE}" cylc install --no-run-name --workflow-name="${WORKFLOW_NAME}" # Pick a deliberately peculier timezone; export TZ=Australia/Eucla run_ok "${TEST_NAME_BASE}" cylc play "${WORKFLOW_NAME}" --no-detach --timestamp grep_ok "+08:45 INFO" "${TEST_NAME_BASE}.stderr" purge init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1000 [[graph]] R1 = foo __FLOW_CONFIG__ cylc install --no-run-name --workflow-name="${WORKFLOW_NAME}-foo" run_ok "${TEST_NAME_BASE}" cylc play "${WORKFLOW_NAME}-foo" --no-detach --timestamp grep_ok "+08:45 INFO" "${TEST_NAME_BASE}.stderr" purge exit cylc-flow-8.6.4/tests/functional/cylc-play/03-remote-run-host.t0000664000175000017500000000265715202510242024475 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Run a workflow with ``cylc play --host=somewhere-else`` export REQUIRE_PLATFORM='loc:remote fs:shared runner:background' . "$(dirname "$0")/test_header" set_test_number 2 # shellcheck disable=SC2016 init_workflow "${TEST_NAME_BASE}" <<< ' # A total non-entity workflow - just something to run. [scheduling] initial cycle point = 2020 [[graph]] R1 = Aleph [runtime] [[Aleph]] ' workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --host="${CYLC_TEST_HOST}" --no-detach grep_ok "Scheduler:.*${CYLC_TEST_HOST}" "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge exit cylc-flow-8.6.4/tests/functional/cylc-play/01-profiler.t0000664000175000017500000000264015202510242023235 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Ensure that the Scheduler is able to run in profile mode without falling # over. It should produce a profile file at the end. . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1 cycling mode = integer [[graph]] R1 = a __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" --profile exists_ok 'profile.prof' run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --profile --no-detach exists_ok "${RUN_DIR}/${WORKFLOW_NAME}/log/scheduler/profile.prof" purge cylc-flow-8.6.4/tests/functional/cylc-play/10-start-task-upgrade.t0000664000175000017500000000213215202510242025131 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ensure that legacy task ids are upgraded automatically when specified # with --start-task . "$(dirname "$0")/test_header" set_test_number 2 run_fail "${TEST_NAME_BASE}" \ cylc play Agrajag --start-task foo.123 --start-task bar.234 grep_ok \ 'Cylc7 format is deprecated, using: 123/foo 234/bar' \ "${TEST_NAME_BASE}.stderr" cylc-flow-8.6.4/tests/functional/cylc-play/08-provided-vars.t0000664000175000017500000000365715202510242024220 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # test the export of CYLC_WORKFLOW_ID and CYLC_WORKFLOW_ID . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config '' " [install] max depth = 5 " cat > flow.cylc <<'__FLOW_CONFIG__' [scheduler] cycle point format = %Y [[events]] stall timeout = PT0S [scheduling] initial cycle point = 1066 [[dependencies]] R1 = foo [runtime] [[foo]] script = """ echo "CYLC_WORKFLOW_ID is: ${CYLC_WORKFLOW_ID}" echo "CYLC_WORKFLOW_ID is: ${CYLC_WORKFLOW_ID}" """ __FLOW_CONFIG__ init_workflow "${TEST_NAME_BASE}" flow.cylc true run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-play" cylc play "${WORKFLOW_NAME}" --no-detach named_grep_ok \ "${TEST_NAME_BASE}-check-CYLC_WORKFLOW_ID" \ "CYLC_WORKFLOW_ID is:.* ${WORKFLOW_NAME}" \ "${WORKFLOW_RUN_DIR}/runN/log/job/1066/foo/NN/job.out" named_grep_ok \ "${TEST_NAME_BASE}-check-CYLC_WORKFLOW_ID" \ "CYLC_WORKFLOW_ID is:.* ${WORKFLOW_NAME}/run1" \ "${WORKFLOW_RUN_DIR}/runN/log/job/1066/foo/NN/job.out" cylc-flow-8.6.4/tests/functional/cylc-play/00-contact.t0000664000175000017500000000604715202510242023052 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test `cylc play` start up or resume under various contact scenarios: # - running, normally # - stopped, normally # - stopped, but orphaned contact file # - running, but can't be contacted . "$(dirname "$0")/test_header" set_test_number 10 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = false __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" # Not running: play should start it up. # (run like this "(cmd &) >/dev/null" to discard the process killed message) (cylc play --pause --no-detach "${WORKFLOW_NAME}" \ 1>"${TEST_NAME_BASE}.out" 2>&1 &) 2>/dev/null poll_workflow_running poll_grep_workflow_log "Pausing the workflow: Paused on start up" # Already running: play should resume. TEST_NAME="${TEST_NAME_BASE}-resume" run_ok "${TEST_NAME}" \ cylc play "${WORKFLOW_NAME}" grep_ok "Resuming already-running workflow" "${TEST_NAME}.stdout" poll_grep_workflow_log "RESUMING the workflow now" # Orphan the contact file # Play should timeout, remove the contact file, and start up. TEST_NAME="${TEST_NAME_BASE}-orphan" eval "$(cylc get-workflow-contact "${WORKFLOW_NAME}" | grep CYLC_WORKFLOW_PID)" kill -9 "${CYLC_WORKFLOW_PID}" > /dev/null 2>&1 run_ok "${TEST_NAME}" \ cylc play "${WORKFLOW_NAME}" --comms-timeout=1 --pause grep_ok "Connection timed out (1000.0 ms)" "${TEST_NAME}.stderr" grep_ok "Removed contact file" "${TEST_NAME}.stderr" poll_grep_workflow_log "Pausing the workflow: Paused on start up" # Mess with the port: play aborts as can't tell if workflow is running or not. # (The ping times out, then `cylc psutil` can't find the workflow). eval "$(cylc get-workflow-contact "${WORKFLOW_NAME}" | grep CYLC_WORKFLOW_PORT)" sed -i 's/CYLC_WORKFLOW_PORT=.*/CYLC_WORKFLOW_PORT=490001/' \ "$WORKFLOW_RUN_DIR/.service/contact" run_fail "${TEST_NAME}" \ cylc play "${WORKFLOW_NAME}" --comms-timeout=1 grep_ok "Connection timed out (1000.0 ms)" "${TEST_NAME}.stderr" grep_ok "Cannot tell if the workflow is running" "${TEST_NAME}.stderr" # Restore contact file and shut down. sed -i "s/CYLC_WORKFLOW_PORT=.*/CYLC_WORKFLOW_PORT=${CYLC_WORKFLOW_PORT}/" \ "$WORKFLOW_RUN_DIR/.service/contact" run_ok "${TEST_NAME}" \ cylc stop --now "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/cylc-play/11-remote-reinvoke-uncontactable.t0000664000175000017500000000400215202510242027341 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test that remote reinvocation exits non-zero if the remote host is # uncontactable. # https://github.com/cylc/cylc-flow/pull/6745 # Ideally we would test with multiple hosts in [scheduler][run hosts]available # to ensure each is tried, but that is not feasible. export REQUIRE_PLATFORM='loc:remote fs:shared' # (We need a valid remote host; the platform itself is not used) . "$(dirname "$0")/test_header" set_test_number 2 # create an SSH config that will fail to connect to a valid host mock_ssh_config_file="/var/tmp/cylc-test-ssh-config" cat > "${mock_ssh_config_file}" << __EOF__ Host ${CYLC_TEST_HOST}* User BorkedMushroom25 __EOF__ create_test_global_config "" " [scheduler] [[run hosts]] available = ${CYLC_TEST_HOST} ranking = [platforms] [[localhost]] ssh command = ssh -oBatchMode=yes -oStrictHostKeyChecking=no -F ${mock_ssh_config_file} " init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = a __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "$TEST_NAME" \ cylc play "${WORKFLOW_NAME}" --no-detach --mode=simulation grep_ok "Cylc could not establish SSH connection to the run hosts" \ "${TEST_NAME}.stderr" cylc-flow-8.6.4/tests/functional/cylc-play/test_header0000777000175000017500000000000015202510242027323 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-play/04-no-install.t0000664000175000017500000000276215202510242023503 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Run a workflow that was written directly in the cylc-run dir # (rather than being installed by cylc install) . "$(dirname "$0")/test_header" set_test_number 1 # write a flow in the cylc-run dir # (rather than using cylc-install to transfer it) WORKFLOW_NAME="cylctb-${CYLC_TEST_TIME_INIT}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME_BASE}" mkdir -p "${RUN_DIR}/${WORKFLOW_NAME}" cat > "${RUN_DIR}/${WORKFLOW_NAME}/flow.cylc" <<__HERE__ [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo __HERE__ # ensure it can be run with no further meddling workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach purge exit cylc-flow-8.6.4/tests/functional/cylc-play/09-invalid-cp-opt.t0000664000175000017500000000316515202510242024254 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ----------------------------------------------------------------------------- # Test that an invalid cycle point option does not cause an empty DB to be # created - https://github.com/cylc/cylc-flow/issues/4637 . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1066 [[graph]] R1 = foo __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Assert malformed stopcp causes failure: run_fail "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --no-detach --stopcp='potato' grep_ok "ERROR - Workflow shutting down .*potato" "${TEST_NAME_BASE}-run.stderr" # Check that we haven't got a database exists_ok "${WORKFLOW_RUN_DIR}/.service" exists_fail "${WORKFLOW_RUN_DIR}/.service/db" purge cylc-flow-8.6.4/tests/functional/cylc-play/06-warnif-scp-after-fcp.t0000664000175000017500000000366315202510242025344 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # test that cylc play warns if FCP before Stop Point. . "$(dirname "$0")/test_header" set_test_number 7 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [[events]] # The third subtest will restart with an empty task pool. # This ensures the workflow shuts down prompty. restart timeout = PT0S [scheduling] initial cycle point = 1 final cycle point = 2 cycling mode = integer [[graph]] P1 = foo __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" for SCP in 1 2 3; do TEST_NAME="${TEST_NAME_BASE}-play-integer-scp=${SCP}" workflow_run_ok "${TEST_NAME}" cylc play "${WORKFLOW_NAME}" \ --no-detach --stopcp="${SCP}" if [[ "${SCP}" -lt 3 ]]; then grep_ok "Stop cycle point '.*'.*after.*final cycle point '.*'" \ "${RUN_DIR}/${WORKFLOW_NAME}/log/scheduler/log" "-v" else grep_ok "Stop cycle point '.*'.*after.*final cycle point '.*'" \ "${RUN_DIR}/${WORKFLOW_NAME}/log/scheduler/log" fi done purge cylc-flow-8.6.4/tests/functional/cylc-trigger/0000775000175000017500000000000015202510242021504 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/01-queued/0000775000175000017500000000000015202510242023212 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/01-queued/reference.log0000664000175000017500000000012715202510242025653 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/cylc-trigger/01-queued/flow.cylc0000664000175000017500000000065715202510242025045 0ustar alastairalastair[scheduling] [[queues]] [[[my_queue]]] limit = 1 members = METASYNTACTIC [[graph]] R1 = "foo:start => bar" [runtime] [[METASYNTACTIC]] [[foo]] inherit = METASYNTACTIC script = """ cylc__job__wait_cylc_message_started cylc trigger "$CYLC_WORKFLOW_ID//1/bar" """ [[bar]] inherit = METASYNTACTIC script = true cylc-flow-8.6.4/tests/functional/cylc-trigger/04-filter-names.t0000775000175000017500000000253315202510242024506 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test triggering matching names in a cycle. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" # Ensure that 1/loser is only triggered once. JOB_LOG_DIR="$RUN_DIR/${WORKFLOW_NAME}/log/job" run_ok "${TEST_NAME_BASE}-loser-nn" \ test "$(readlink "${JOB_LOG_DIR}/1/loser/NN")" = '01' purge exit cylc-flow-8.6.4/tests/functional/cylc-trigger/00-compat/0000775000175000017500000000000015202510242023204 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/00-compat/reference.log0000664000175000017500000000012715202510242025645 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/cylc-trigger/00-compat/flow.cylc0000664000175000017500000000024615202510242025031 0ustar alastairalastair[scheduling] [[graph]] R1 = foo => bar [runtime] [[foo]] script = cylc trigger "${CYLC_WORKFLOW_ID}//^/bar" [[bar]] script = true cylc-flow-8.6.4/tests/functional/cylc-trigger/06-already-active.t0000664000175000017500000000226315202510242025011 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test triggering an already-active task just generates a warning. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/cylc-trigger/07-kill-trigger/0000775000175000017500000000000015202510242024324 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/07-kill-trigger/reference.log0000664000175000017500000000010015202510242026754 0ustar alastairalastair1/a -triggered off [] in flow 1 1/a -triggered off [] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-trigger/07-kill-trigger/flow.cylc0000664000175000017500000000201215202510242026142 0ustar alastairalastair[scheduler] [[events]] inactivity timeout = PT1M expected task failures = "1/a" [scheduling] [[graph]] R1 = a [runtime] [[a]] script = """ if [[ $CYLC_TASK_SUBMIT_NUMBER == 1 ]]; then # wait for the scheduler to receive the started message, # then kill the job cylc__job__poll_grep_workflow_log -E '1/a.*running' cylc kill "${CYLC_WORKFLOW_ID}//1/a" # do not succeed immediately after issuing the kill command or the # workflow may shut down as complete before registering task failure # (This polling grep will never complete, but you know what I mean.) cylc__job__poll_grep_workflow_log -E '1/a.*failed' fi """ [[[events]]] # when the scheduler receives the failed message, trigger the task # to run again, it should run instantly failed handlers = cylc trigger "%(workflow)s//1/a" cylc-flow-8.6.4/tests/functional/cylc-trigger/02-filter-failed/0000775000175000017500000000000015202510242024432 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/02-filter-failed/reference.log0000664000175000017500000000054615202510242027100 0ustar alastairalastairInitial point: 1 Final point: 1 1/fixable1 -triggered off [] 1/fixable2 -triggered off [] 1/fixable3 -triggered off [] 1/fixer -triggered off [] 1/fixable1 -triggered off [] 1/fixable2 -triggered off [] 1/fixable3 -triggered off [] 1/z1 -triggered off ['1/fixable1', '1/fixable2', '1/fixable3'] 1/z2 -triggered off ['1/fixable1', '1/fixable2', '1/fixable3'] cylc-flow-8.6.4/tests/functional/cylc-trigger/02-filter-failed/flow.cylc0000664000175000017500000000161415202510242026257 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/fixable1, 1/fixable2, 1/fixable3 [scheduling] [[graph]] # Unhandled failures stay around for retriggering by "fixer" R1 = """ fixer FIXABLES:succeed-all => Z """ [runtime] [[FIXABLES]] script = test "${CYLC_TASK_SUBMIT_NUMBER}" -eq 2 [[fixable1, fixable2, fixable3]] inherit = FIXABLES [[fixer]] script = """ cylc__job__wait_cylc_message_started cylc__job__poll_grep_workflow_log '\[1/fixable1/01:running\] failed/ERR' cylc__job__poll_grep_workflow_log '\[1/fixable2/01:running\] failed/ERR' cylc__job__poll_grep_workflow_log '\[1/fixable3/01:running\] failed/ERR' cylc trigger "${CYLC_WORKFLOW_ID}//1/fixable*:failed" """ [[Z]] script = true [[z1, z2]] inherit = Z cylc-flow-8.6.4/tests/functional/cylc-trigger/test_header0000777000175000017500000000000015202510242030021 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/04-filter-names/0000775000175000017500000000000015202510242024313 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/04-filter-names/reference.log0000664000175000017500000000114615202510242026756 0ustar alastairalastairInitial point: 1 Final point: 1 1/fixable-1a -triggered off [] 1/fixable-1b -triggered off [] 1/fixable-2a -triggered off [] 1/fixable-2b -triggered off [] 1/fixable-3 -triggered off [] 1/loser -triggered off [] 1/fixer -triggered off [] 1/fixable-1a -triggered off [] 1/fixable-1b -triggered off [] 1/fixable-2a -triggered off [] 1/fixable-2b -triggered off [] 1/fixable-3 -triggered off [] 1/z1 -triggered off ['1/fixable-1a', '1/fixable-1b', '1/fixable-2a', '1/fixable-2b', '1/fixable-3', '1/loser'] 1/z2 -triggered off ['1/fixable-1a', '1/fixable-1b', '1/fixable-2a', '1/fixable-2b', '1/fixable-3', '1/loser'] cylc-flow-8.6.4/tests/functional/cylc-trigger/04-filter-names/flow.cylc0000664000175000017500000000247715202510242026150 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/fixable-1a, 1/fixable-1b, 1/fixable-2a, 1/fixable-2b, 1/fixable-3, 1/loser [scheduling] [[graph]] R1 = """ # Unhandled failures stay around for retriggering by "fixer" fixer FIXABLES:succeed-all & loser:fail => Z """ [runtime] [[FIXABLES]] script = test "${CYLC_TASK_SUBMIT_NUMBER}" -eq 2 [[FIXABLE-1, FIXABLE-2, FIXABLE-3]] inherit = FIXABLES [[fixable-1a, fixable-1b]] inherit = FIXABLE-1 [[fixable-2a, fixable-2b]] inherit = FIXABLE-2 [[fixable-3]] inherit = FIXABLE-3 [[fixer]] script = """ cylc__job__wait_cylc_message_started cylc__job__poll_grep_workflow_log -E '1/fixable-1a/01.* failed' cylc__job__poll_grep_workflow_log -E '1/fixable-1b/01.* failed' cylc__job__poll_grep_workflow_log -E '1/fixable-2a/01.* failed' cylc__job__poll_grep_workflow_log -E '1/fixable-2b/01.* failed' cylc__job__poll_grep_workflow_log -E '1/fixable-3/01.* failed' cylc trigger "${CYLC_WORKFLOW_ID}//" \ '//1/FIXABLE-1' '//1/fixable-2*' '//1/fixable-3' """ [[loser]] script = false [[Z]] script = true [[z1, z2]] inherit = Z cylc-flow-8.6.4/tests/functional/cylc-trigger/07-kill-trigger.t0000664000175000017500000000236515202510242024517 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test killing a job, then re-triggering it whilst it is "held". # See https://github.com/cylc/cylc-flow/issues/6398 . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/cylc-trigger/02-filter-failed.t0000775000175000017500000000200515202510242024617 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test triggering multiple failed tasks in a cycle point. # (Pre SoD this matched the task pool by failed state). . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-trigger/00-compat.t0000775000175000017500000000167015202510242023400 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test basic usage of "cylc trigger" . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-trigger/01-queued.t0000777000175000017500000000000015202510242025257 200-compat.tustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/06-already-active/0000775000175000017500000000000015202510242024621 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-trigger/06-already-active/flow.cylc0000664000175000017500000000125315202510242026445 0ustar alastairalastair# test triggering an already active task [scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = "triggeree:start & triggerer" [runtime] [[triggerer]] script = """ cylc__job__poll_grep_workflow_log "1/triggeree.* => running" -E cylc trigger "$CYLC_WORKFLOW_ID//1/triggeree" cylc__job__poll_grep_workflow_log \ "Job already in process - ignoring trigger" -E """ [[triggeree]] script = """ cylc__job__poll_grep_workflow_log \ "Job already in process - ignoring trigger" -E """ cylc-flow-8.6.4/tests/functional/retries/0000775000175000017500000000000015202510242020566 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/02-xtriggers/0000775000175000017500000000000015202510242023023 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/02-xtriggers/reference.log0000664000175000017500000000030415202510242025461 0ustar alastairalastairInitial point: 1 Final point: 1 1/retry -triggered off [] 1/retry -triggered off [] 1/retry -triggered off [] 1/retry -triggered off [] 1/retry -triggered off [] 1/test -triggered off ['1/retry'] cylc-flow-8.6.4/tests/functional/retries/02-xtriggers/flow.cylc0000664000175000017500000000277115202510242024655 0ustar alastairalastair[scheduling] [[graph]] R1 = retry => test [runtime] [[retry]] # capture task info - incl xtriggers pre-script = """ cylc show "${CYLC_WORKFLOW_ID}" \ "${CYLC_TASK_NAME}.${CYLC_TASK_CYCLE_POINT}" \ > "${CYLC_TASK_LOG_ROOT}-show" """ # fail four times then pass script = test "${CYLC_TASK_SUBMIT_NUMBER}" -ge 5 # stagger retries every two seconds [[[job]]] execution retry delays = 5*PT3S [[test]] script = """ cylc cat-log "${CYLC_WORKFLOW_ID}" > log # get a list of the times cylc says tasks will retry after mapfile -t RETRY_TIMES \ < <(sed -n 's/.*retrying.*after \(.*\)).*/\1/p' log) # get a list of the times when the xtriggers actually succeeded mapfile -t XTRIGGER_TIMES \ < <(sed -n 's/\(.*\) INFO.*xtrigger succeeded.*/\1/p' log) test "${#RETRY_TIMES[@]}" -eq 4 test "${#XTRIGGER_TIMES[@]}" -eq 4 # make sure that tasks retried when they said they would for N in $(seq 0 3); do INTERVAL="$(isodatetime \ "${RETRY_TIMES[$N]}" "${XTRIGGER_TIMES[$N]}" --as-total s)" echo "RETRY=${RETRY_TIMES[$N]}" echo "XTRIGGER=${XTRIGGER_TIMES[$N]}" echo "INTERVAL=${INTERVAL}" python3 -c "assert ${INTERVAL} >= 0.0" done """ cylc-flow-8.6.4/tests/functional/retries/02-xtriggers.t0000664000175000017500000000167115202510242023215 0ustar alastairalastair#!/bin/bash # THIS FILE IS PART OF THE CYLC WORKFLOW 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 . #------------------------------------------------------------------------------- # Test kill running jobs only . "$(dirname "$0")/test_header" set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/retries/03-upgrade/0000775000175000017500000000000015202510242022435 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/03-upgrade/flow.cylc0000664000175000017500000000066515202510242024267 0ustar alastairalastair[scheduler] [[events]] abort on inactivity timeout = True abort on stall timeout = True inactivity timeout = PT1M stall timeout = PT1M [scheduling] [[dependencies]] graph = """ a => b => c """ [runtime] [[b]] # fail four times then pass script = test "$CYLC_TASK_SUBMIT_NUMBER" -ge 3; [[[job]]] execution retry delays = 2*PT2S cylc-flow-8.6.4/tests/functional/retries/execution/0000775000175000017500000000000015202510242022571 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/execution/reference.log0000664000175000017500000000020015202510242025222 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] 1/foo -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/retries/execution/flow.cylc0000664000175000017500000000054015202510242024413 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = test "${CYLC_TASK_TRY_NUMBER}" '-ge' '4' [[[job]]] execution retry delays = 3*PT0S cylc-flow-8.6.4/tests/functional/retries/test_header0000777000175000017500000000000015202510242027103 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/submission/0000775000175000017500000000000015202510242022761 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/retries/submission/reference.log0000664000175000017500000000020015202510242025412 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] 1/foo -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/retries/submission/flow.cylc0000664000175000017500000000065715202510242024614 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M expected task failures = 1/foo [scheduling] [[graph]] R1 = "foo:submit-fail? => !foo" [runtime] [[foo]] script = true platform = nonsense-platform [[[job]]] submission retry delays = PT0S, PT0S, PT0S cylc-flow-8.6.4/tests/functional/retries/01-submission-retry.t0000664000175000017500000000337015202510242024532 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution retries are working . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" 'submission' create_test_global_config "" " [platforms] [[nonsense-platform]] hosts = notahost " #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- if ! command -v 'sqlite3' >'/dev/null'; then skip 1 'sqlite3 not installed?' else sqlite3 \ "$RUN_DIR/${WORKFLOW_NAME}/log/db" \ 'SELECT try_num, submit_num FROM task_jobs' >'select.out' cmp_ok 'select.out' <<'__OUT__' 1|1 1|2 1|3 1|4 __OUT__ fi #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/retries/00-execution-retry.t0000664000175000017500000000311615202510242024337 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution retries are working . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" 'execution' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" if command -v 'sqlite3' >'/dev/null'; then sqlite3 \ "$RUN_DIR/${WORKFLOW_NAME}/log/db" \ 'SELECT try_num, submit_num FROM task_jobs' >'select.out' cmp_ok 'select.out' <<'__OUT__' 1|1 2|2 3|3 4|4 __OUT__ else skip 1 "sqlite3 not installed?" fi #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/logging/0000775000175000017500000000000015202510242020537 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/logging/03-roll.t0000775000175000017500000000346115202510242022123 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test log rolling. . "$(dirname "$0")/test_header" set_test_number 11 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 20 [[graph]] P1 = t1 & t2 & t3 [runtime] [[t1, t2, t3]] script = true __FLOW_CONFIG__ create_test_global_config '' ' [scheduler] [[logging]] rolling archive length = 8 maximum size in bytes = 2048 ' run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" FILES="$(ls "${HOME}/cylc-run/${WORKFLOW_NAME}/log/scheduler/"*.log)" run_ok "${TEST_NAME_BASE}-n-logs" test 8 -eq "$(wc -l <<<"${FILES}")" for FILE in ${FILES}; do run_ok "${TEST_NAME_BASE}-log-size" test "$(stat -c'%s' "${FILE}")" -le 2048 done purge exit cylc-flow-8.6.4/tests/functional/logging/01-basic.t0000664000175000017500000000476115202510242022233 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test start/restart/reload config logs are created correctly . "$(dirname "$0")/test_header" set_test_number 7 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] [[events]] abort on stall timeout = true stall timeout = PT0S abort on inactivity timeout = true inactivity timeout = PT1M [scheduling] [[graph]] R1 = reloader1 => stopper => reloader2 [runtime] [[reloader1, reloader2]] script = """ cylc reload "${CYLC_WORKFLOW_ID}" # wait for the command to complete cylc__job__poll_grep_workflow_log 'Reload completed' """ [[stopper]] script = cylc stop --now --now "${CYLC_WORKFLOW_ID}" __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" # Check scheduler logs. ls "${WORKFLOW_RUN_DIR}/log/scheduler/" > schd_1.out cmp_ok schd_1.out << __EOF__ 01-start-01.log log __EOF__ # Check config logs. ls "${WORKFLOW_RUN_DIR}/log/config/" > conf_1.out cmp_ok conf_1.out << __EOF__ 01-start-01.cylc 02-reload-01.cylc flow-processed.cylc __EOF__ mv "$WORKFLOW_RUN_DIR/cylc.flow.main_loop.log_db.sql" "$WORKFLOW_RUN_DIR/01.cylc.flow.main_loop.log_db.sql" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" ls "${WORKFLOW_RUN_DIR}/log/scheduler/" > schd_2.out cmp_ok schd_2.out << __EOF__ 01-start-01.log 02-restart-02.log log __EOF__ ls "${WORKFLOW_RUN_DIR}/log/config/" > conf_2.out cmp_ok conf_2.out << __EOF__ 01-start-01.cylc 02-reload-01.cylc 03-restart-02.cylc 04-reload-02.cylc flow-processed.cylc __EOF__ purge cylc-flow-8.6.4/tests/functional/logging/04-dev_mode.t0000664000175000017500000000311415202510242022726 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test log dev-mode. . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 1 [[graph]] P1 = t1 [runtime] [[t1]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate-plain" \ cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-validate-vvv" \ cylc validate --timestamp -vvv "${WORKFLOW_NAME}" grep_ok " DEBUG - \[config:.*\]" "${TEST_NAME_BASE}-validate-vvv.stderr" run_ok "${TEST_NAME_BASE}-validate-vvv--no-timestamp" \ cylc validate -vvv "${WORKFLOW_NAME}" grep_ok "^DEBUG - \[config:.*\]" "${TEST_NAME_BASE}-validate-vvv--no-timestamp.stderr" purge exit cylc-flow-8.6.4/tests/functional/logging/02-duplicates/0000775000175000017500000000000015202510242023113 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/logging/02-duplicates/flow.cylc0000664000175000017500000000176515202510242024747 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] R1/1 = """ foo:fail? => bar foo? & bar => restart """ R1/2 = """ restart[-P1] => foo? foo:fail? => bar foo? & bar => pub """ [runtime] [[foo]] script = false [[bar]] script = """ cylc set --output=succeeded \ "${CYLC_WORKFLOW_ID}//${CYLC_TASK_CYCLE_POINT}/foo" """ [[restart]] script = """ cylc stop "${CYLC_WORKFLOW_ID}" """ [[pub]] script = """ # Extract timestamp lines from logs for file in $(find "${CYLC_WORKFLOW_RUN_DIR}/log/schedulerr/" -name '*.*'); do grep '.*-.*-.*' "${file}" | sort -u || true done | sort | uniq -d > 'log-duplication' """ cylc-flow-8.6.4/tests/functional/logging/test_header0000777000175000017500000000000015202510242027054 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/logging/02-duplicates.t0000664000175000017500000000305315202510242023301 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --no-detach "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-check" \ test -e "${WORKFLOW_RUN_DIR}/work/2/pub/log-duplication" run_fail "${TEST_NAME_BASE}-check" \ test -s "${WORKFLOW_RUN_DIR}/work/2/pub/log-duplication" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/logging/05-validation-detach.t0000664000175000017500000000243115202510242024526 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ---------------------------------------------------------------------------- # Test that validation errors on play are logged before daemonisation . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW__' [scheduler] horse = dorothy __FLOW__ run_fail "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "$TEST_NAME" cylc play "${WORKFLOW_NAME}" grep_ok "IllegalItemError: [scheduler]horse" "${TEST_NAME}.stderr" -F purge cylc-flow-8.6.4/tests/functional/periodicals/0000775000175000017500000000000015202510242021407 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/09-monthly-reorder.t0000664000175000017500000000401215202510242025151 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Monthly cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Monthly-reorder #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- delete_db TEST_NAME="${TEST_NAME_BASE}-run" # Swapping the Monthly and Hours-of-the-day cycling sections # should make no difference. perl -pi -e 'undef $/; s/( *\[\[\[00.*marker1)\n( *\[\[\[Monthly.*marker2)/${2}\n${1}/smg' "${TEST_DIR}/${WORKFLOW_NAME}/flow.cylc" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/README0000664000175000017500000000125315202510242022270 0ustar alastairalastairStepped Daily, Monthly, and Yearly cycling tests: Cycler(,) Each test workflow defines a single task on a 3-unit cycle: Daily(20100110,3): 20100110, 20100113, 20100116, ... Monthly(201001,3): 201001, 201004, 201007, ... Yearly(2010,3) : 2010, 2013, 2016, ... The reference log is for a test run starting one unit in from the anchor cycle time. Cylc should get exactly the same result for test runs that start one, two, or three units in because the initial cycle point gets adjusted up to the next on-sequence cycle time, so additional tests are done to confirm this - by modifying the test start time in the installed reference log on the fly. cylc-flow-8.6.4/tests/functional/periodicals/03-monthly.t0000664000175000017500000000171115202510242023506 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Monthly cycling . "$(dirname "$0")/test_header" set_test_number 2 reftest "${TEST_NAME_BASE}" 'Monthly' exit cylc-flow-8.6.4/tests/functional/periodicals/01-daily.t0000664000175000017500000000276615202510242023127 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Daily cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Daily #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}20140106T06/' "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/02-daily.t0000664000175000017500000000277415202510242023127 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Daily cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Daily #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}20140105T06/' \ "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/Monthly-reorder/0000775000175000017500000000000015202510242024501 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/Monthly-reorder/reference.log0000664000175000017500000000101115202510242027133 0ustar alastairalastairInitial point: 20130130T0000Z Final point: 20130202T0000Z 20130130T0000Z/dummy -triggered off ['20130129T0000Z/daily'] 20130130T0000Z/daily -triggered off [] 20130131T0000Z/dummy -triggered off ['20130130T0000Z/daily'] 20130131T0000Z/daily -triggered off [] 20130201T0000Z/dummy -triggered off ['20130131T0000Z/daily'] 20130201T0000Z/daily -triggered off [] 20130201T0000Z/monthly -triggered off ['20130201T0000Z/dummy'] 20130202T0000Z/dummy -triggered off ['20130201T0000Z/daily'] 20130202T0000Z/daily -triggered off [] cylc-flow-8.6.4/tests/functional/periodicals/Monthly-reorder/flow.cylc0000664000175000017500000000070615202510242026327 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20130130T00 final cycle point = 20130202T00 runahead limit = P0 [[graph]] # (this triggers a monthly task off the last daily task each month) T00 = """ daily daily[-PT24H] => dummy """ # marker1 R/01T/P1M = "dummy => monthly" # marker2 [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/periodicals/05-monthly.t0000664000175000017500000000277415202510242023522 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Monthly cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Monthly #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}2010-04/' \ "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/08-yearly.t0000664000175000017500000000276715202510242023342 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Yearly cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Yearly #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}2013/' \ "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/Monthly/0000775000175000017500000000000015202510242023041 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/Monthly/reference.log0000664000175000017500000000020015202510242025472 0ustar alastairalastairInitial point: 20100201T0000Z Final point: 20100801T0000Z 20100401T0000Z/a -triggered off [] 20100701T0000Z/a -triggered off [] cylc-flow-8.6.4/tests/functional/periodicals/Monthly/flow.cylc0000664000175000017500000000033015202510242024660 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 2010-02 final cycle point = 2010-08 runahead limit = P0 [[graph]] 2010-01/P3M = "a" [runtime] [[a]] script = true cylc-flow-8.6.4/tests/functional/periodicals/00-daily.t0000664000175000017500000000170515202510242023116 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Daily cycling . "$(dirname "$0")/test_header" set_test_number 2 reftest "${TEST_NAME_BASE}" 'Daily' exit cylc-flow-8.6.4/tests/functional/periodicals/test_header0000777000175000017500000000000015202510242027724 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/Yearly/0000775000175000017500000000000015202510242022654 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/Yearly/reference.log0000664000175000017500000000020015202510242025305 0ustar alastairalastairInitial point: 20110101T0000Z Final point: 20180101T0000Z 20130101T0000Z/a -triggered off [] 20160101T0000Z/a -triggered off [] cylc-flow-8.6.4/tests/functional/periodicals/Yearly/flow.cylc0000664000175000017500000000031715202510242024500 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 2011 final cycle point = 2018 runahead limit = P0 [[graph]] 2010/P3Y = "a" [runtime] [[a]] script = true cylc-flow-8.6.4/tests/functional/periodicals/04-monthly.t0000664000175000017500000000277415202510242023521 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Monthly cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Monthly #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}2010-03/' \ "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/periodicals/06-yearly.t0000664000175000017500000000170715202510242023331 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Yearly cycling . "$(dirname "$0")/test_header" set_test_number 2 reftest "${TEST_NAME_BASE}" 'Yearly' exit cylc-flow-8.6.4/tests/functional/periodicals/Daily/0000775000175000017500000000000015202510242022451 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/periodicals/Daily/reference.log0000664000175000017500000000024315202510242025111 0ustar alastairalastairInitial point: 20140107T0000Z Final point: 20140118T0000Z 20140110T0600Z/a -triggered off [] 20140113T0600Z/a -triggered off [] 20140116T0600Z/a -triggered off [] cylc-flow-8.6.4/tests/functional/periodicals/Daily/flow.cylc0000664000175000017500000000044615202510242024300 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20140107 final cycle point = 20140118 runahead limit = P0 [[graph]] # daily cycling *with a non-zero hour* is a more stringent test 20140110T06/P3D = "a" [runtime] [[a]] script = true cylc-flow-8.6.4/tests/functional/periodicals/07-yearly.t0000664000175000017500000000276715202510242023341 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test Yearly cycling . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" Yearly #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" perl -pi -e 's/(Initial point: ).*$/${1}2012/' \ "${TEST_DIR}/${WORKFLOW_NAME}/reference.log" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cylc-config/0000775000175000017500000000000015202510242021306 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/09-platforms.t0000775000175000017500000000452715202510242023743 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc config --platform-names and --platforms . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 7 #------------------------------------------------------------------------------- cat > "global.cylc" <<__HEREDOC__ [platforms] [[foo]] job runner = slurm hosts = of_melkor, of_valar [[bar]] job runner = slurm hosts = of_orcs, of_gondor [platform groups] [[FOO]] platforms = foo, bar [task events] # Just make sure this doesn't get included __HEREDOC__ export CYLC_CONF_PATH="${PWD}" TEST_NAME="${TEST_NAME_BASE}-names" run_ok "${TEST_NAME}" cylc config --platform-names cmp_ok "${TEST_NAME}.stdout" <<__HEREDOC__ localhost foo bar FOO __HEREDOC__ cmp_ok "${TEST_NAME}.stderr" <<__HEREDOC__ Configured names are regular expressions; any match is a valid platform. They are searched from the bottom up, until the first match is found. Platforms --------- Platform Groups -------------- __HEREDOC__ TEST_NAME="${TEST_NAME_BASE}-s" head -n 10 > just_platforms < global.cylc run_ok "${TEST_NAME}" cylc config --platforms cmp_ok "${TEST_NAME}.stdout" "just_platforms" #------------------------------------------------------------------------------- # test with no platforms or platform groups defined rm "global.cylc" touch "global.cylc" TEST_NAME="${TEST_NAME_BASE}-names" run_ok "${TEST_NAME}" cylc config --platform-names cmp_ok "${TEST_NAME}.stdout" <<__HEREDOC__ localhost __HEREDOC__ exit cylc-flow-8.6.4/tests/functional/cylc-config/00-simple.t0000775000175000017500000000656415202510242023217 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc config . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 19 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/flow.cylc" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-all" run_ok "${TEST_NAME}" cylc config -d "${WORKFLOW_NAME}" mkdir tmp_src cp "${TEST_NAME}.stdout" tmp_src/flow.cylc run_ok "${TEST_NAME}-validate" cylc validate --check-circular ./tmp_src cmp_ok "${TEST_NAME}.stderr" <'/dev/null' rm -rf tmp_src #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-section1" run_ok "${TEST_NAME}" cylc config -d --item=[scheduling] "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" "$TEST_SOURCE_DIR/${TEST_NAME_BASE}/section1.stdout" cmp_ok "${TEST_NAME}.stderr" - VAR __OUT__ cmp_ok "${TEST_NAME}.stderr" - VAR __OUT__ cmp_ok "${TEST_NAME}.stderr" - "flow_path.out" cmp_ok "flow_path.out" <<< "${WORKFLOW_RUN_DIR}/flow.cylc" purge exit cylc-flow-8.6.4/tests/functional/cylc-config/05-param-vars.t0000775000175000017500000000237515202510242024000 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test parameter vars are not defined with user env GitHub #2225 . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------ TEST_NAME="${TEST_NAME_BASE}-config" run_ok "${TEST_NAME}" \ cylc config -i "[runtime][foo_t1_right]environment" "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" - <<__END__ PARAM1 = \$CYLC_TASK_PARAM_t PARAM2 = \$CYLC_TASK_PARAM_u __END__ purge cylc-flow-8.6.4/tests/functional/cylc-config/07-failif-item-in-platforms-and-groups.t0000775000175000017500000000315715202510242030604 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that Platforms and Platform Groups return an error listing items # defined in both. # Not having additional tests for cases with no overlap or platforms only # becuase other tests will fail. # Not testing for platform groups only because the really should never happen. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 create_test_global_config '' ''' [platforms] [[Thomas]] [[Percy]] [[Edward]] [[Skarloey]] [platform groups] [[Thomas]] [[Percy]] [[Gordon]] [[Rhenas]] ''' run_fail "${TEST_NAME_BASE}" cylc config grep_ok "GlobalConfigError" "${TEST_NAME_BASE}.stderr" grep_ok "\* Thomas" "${TEST_NAME_BASE}.stderr" grep_ok "\* Percy" "${TEST_NAME_BASE}.stderr" exit cylc-flow-8.6.4/tests/functional/cylc-config/02-cycling/0000775000175000017500000000000015202510242023155 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/02-cycling/flow.cylc0000664000175000017500000000031415202510242024776 0ustar alastairalastair[scheduling] initial cycle point = 2020 final cycle point = 2030 [[graph]] P1Y = foo [runtime] [[foo]] script = """ echo this echo that """ cylc-flow-8.6.4/tests/functional/cylc-config/10-platform-expansion.t0000775000175000017500000000276015202510242025547 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc config expansion of platform section. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- cat > "global.cylc" <<__HEREDOC__ [platforms] [[ \ foo, bar..., \ baz\d\d, qux\S\S \ ]] hosts = of_melkor, of_valar job runner = slurm __HEREDOC__ export CYLC_CONF_PATH="${PWD}" TEST_NAME="${TEST_NAME_BASE}-names" run_ok "${TEST_NAME}" cylc config --platform-names cmp_ok "${TEST_NAME}.stdout" <<__HEREDOC__ localhost foo bar... baz\d\d qux\S\S __HEREDOC__ exit cylc-flow-8.6.4/tests/functional/cylc-config/03-icp.t0000775000175000017500000000255315202510242022476 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test for "cylc config --icp=CYCLE_POINT". . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True [scheduling] [[graph]] R1 = foo => bar [runtime] [[foo, bar]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc config --icp=20200101T0000Z "${WORKFLOW_NAME}" contains_ok "${TEST_NAME_BASE}.stdout" <<__OUT__ initial cycle point = 20200101T0000Z __OUT__ cmp_ok "${TEST_NAME_BASE}.stderr" <'/dev/null' purge exit cylc-flow-8.6.4/tests/functional/cylc-config/08-item-not-found.t0000775000175000017500000000440515202510242024573 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc config . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 9 #------------------------------------------------------------------------------- cat >>'global.cylc' <<__HERE__ [platforms] [[foo]] __HERE__ OLD="$CYLC_CONF_PATH" export CYLC_CONF_PATH="${PWD}" # Control Run run_ok "${TEST_NAME_BASE}-ok" cylc config -i "[platforms][foo]" # If item not settable in config (platforms is mis-spelled): run_fail "${TEST_NAME_BASE}-not-in-config-spec" cylc config -i "[platfroms][foo]" cmp_ok "${TEST_NAME_BASE}-not-in-config-spec.stderr" << __HERE__ InvalidConfigError: "platfroms" is not a valid configuration for global.cylc. __HERE__ # If item settable in config but not set. run_fail "${TEST_NAME_BASE}-not-defined" cylc config -i "[scheduler]" cmp_ok "${TEST_NAME_BASE}-not-defined.stderr" << __HERE__ ItemNotFoundError: You have not set "scheduler" in this config. __HERE__ run_fail "${TEST_NAME_BASE}-not-defined-2" cylc config -i "[platforms][bar]" cmp_ok "${TEST_NAME_BASE}-not-defined-2.stderr" << __HERE__ ItemNotFoundError: You have not set "[platforms]bar" in this config. __HERE__ run_fail "${TEST_NAME_BASE}-not-defined-3" cylc config -i "[platforms][foo]hosts" cmp_ok "${TEST_NAME_BASE}-not-defined-3.stderr" << __HERE__ ItemNotFoundError: You have not set "[platforms][foo]hosts" in this config. __HERE__ rm global.cylc export CYLC_CONF_PATH="$OLD" cylc-flow-8.6.4/tests/functional/cylc-config/01-no-final/0000775000175000017500000000000015202510242023227 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/01-no-final/flow.cylc0000664000175000017500000000027115202510242025052 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100808T06 final cycle point = [[graph]] T06 = foo [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/cylc-config/01-no-final.t0000775000175000017500000000304315202510242023417 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc config with a workflow with an explicitly empty final cycle point . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" "$TEST_SOURCE_DIR/${TEST_NAME_BASE}/flow.cylc" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-all run_ok "${TEST_NAME}" cylc config "${WORKFLOW_NAME}" --item='[scheduling]final cycle point' cmp_ok "${TEST_NAME}.stdout" - << __OUT__ __OUT__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-config/test_header0000777000175000017500000000000015202510242027623 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/05-param-vars/0000775000175000017500000000000015202510242023601 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/05-param-vars/flow.cylc0000664000175000017500000000050615202510242025425 0ustar alastairalastair[task parameters] t = 1..5 u = right,left [scheduling] [[graph]] R1 = foo [runtime] [[root]] script = true [[[environment]]] PARAM1 = 'something' [[foo]] [[[environment]]] PARAM1 = $CYLC_TASK_PARAM_t PARAM2 = $CYLC_TASK_PARAM_u cylc-flow-8.6.4/tests/functional/cylc-config/02-cycling.t0000775000175000017500000000237115202510242023350 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc config" on a cycling workflow, result should validate. . "$(dirname "$0")/test_header" set_test_number 2 init_workflow "${TEST_NAME_BASE}" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/flow.cylc" run_ok "${TEST_NAME_BASE}" cylc config "${WORKFLOW_NAME}" mkdir temp cp "${TEST_NAME_BASE}.stdout" temp/flow.cylc run_ok "${TEST_NAME_BASE}-validate" \ cylc validate --check-circular ./temp rm -rf temp purge exit cylc-flow-8.6.4/tests/functional/cylc-config/00-simple/0000775000175000017500000000000015202510242023014 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-config/00-simple/section1.stdout0000664000175000017500000000071615202510242026011 0ustar alastairalastairinitial cycle point = 1 final cycle point = 1 initial cycle point constraints = final cycle point constraints = hold after cycle point = stop after cycle point = cycling mode = integer runahead limit = P4 sequential xtriggers = False [[queues]] [[[default]]] limit = 100 members = [[special tasks]] clock-trigger = external-trigger = clock-expire = sequential = [[xtriggers]] [[graph]] R1 = OPS:finish-all => VAR cylc-flow-8.6.4/tests/functional/cylc-config/00-simple/section2.stdout0000664000175000017500000006360115202510242026014 0ustar alastairalastair[[root]] completion = platform = inherit = script = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[directives]]] [[[outputs]]] [[[parameter environment templates]]] [[OPS]] script = echo "RUN: run-ops.sh" completion = platform = inherit = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[directives]]] [[[outputs]]] [[[parameter environment templates]]] [[VAR]] script = echo "RUN: run-var.sh" completion = platform = inherit = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[directives]]] [[[outputs]]] [[[parameter environment templates]]] [[SERIAL]] completion = platform = inherit = script = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = serial [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[PARALLEL]] completion = platform = inherit = script = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = parallel [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[ops_s1]] script = echo "RUN: run-ops.sh" inherit = OPS, SERIAL completion = succeeded or failed platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = serial [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[ops_s2]] script = echo "RUN: run-ops.sh" inherit = OPS, SERIAL completion = succeeded or failed platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = serial [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[ops_p1]] script = echo "RUN: run-ops.sh" inherit = OPS, PARALLEL completion = succeeded or failed platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = parallel [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[ops_p2]] script = echo "RUN: run-ops.sh" inherit = OPS, PARALLEL completion = succeeded or failed platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = parallel [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[var_s1]] script = echo "RUN: run-var.sh" inherit = VAR, SERIAL completion = succeeded platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = serial [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[var_s2]] script = echo "RUN: run-var.sh" inherit = VAR, SERIAL completion = succeeded platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = serial [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[var_p1]] script = echo "RUN: run-var.sh" inherit = VAR, PARALLEL completion = succeeded platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = parallel [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] [[var_p2]] script = echo "RUN: run-var.sh" inherit = VAR, PARALLEL completion = succeeded platform = init-script = env-script = err-script = exit-script = pre-script = post-script = work sub-directory = execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = run mode = live [[[directives]]] job_type = parallel [[[meta]]] title = description = URL = [[[skip]]] outputs = disable task event handlers = True [[[simulation]]] default run length = PT10S speedup factor = time limit buffer = PT30S fail cycle points = fail try 1 only = True disable task event handlers = True [[[environment filter]]] include = exclude = [[[job]]] batch system = batch submit command template = [[[remote]]] host = owner = retrieve job logs = retrieve job logs max size = retrieve job logs retry delays = [[[events]]] handlers = handler events = handler retry delays = mail events = execution timeout = submission timeout = expired handlers = late offset = late handlers = submitted handlers = started handlers = succeeded handlers = failed handlers = submission failed handlers = warning handlers = critical handlers = retry handlers = submission retry handlers = execution timeout handlers = submission timeout handlers = custom handlers = [[[mail]]] from = to = [[[workflow state polling]]] interval = max-polls = message = alt-cylc-run-dir = verbose mode = [[[environment]]] [[[outputs]]] [[[parameter environment templates]]] cylc-flow-8.6.4/tests/functional/cylc-config/00-simple/flow.cylc0000664000175000017500000000164215202510242024642 0ustar alastairalastair[meta] title = "multiple inheritance example" description = """To see how multiple inheritance works: % cylc list -tb[m] WORKFLOW # list namespaces % cylc graph -n WORKFLOW # graph namespaces % cylc graph WORKFLOW # dependencies, collapse on first-parent namespaces % cylc config --item [runtime]ops_s1 WORKFLOW % cylc config --item [runtime]var_p2 foo""" [scheduling] [[graph]] R1 = "OPS:finish-all => VAR" [runtime] [[root]] [[OPS]] script = echo "RUN: run-ops.sh" [[VAR]] script = echo "RUN: run-var.sh" [[SERIAL]] [[[directives]]] job_type = serial [[PARALLEL]] [[[directives]]] job_type = parallel [[ops_s1, ops_s2]] inherit = OPS, SERIAL [[ops_p1, ops_p2]] inherit = OPS, PARALLEL [[var_s1, var_s2]] inherit = VAR, SERIAL [[var_p1, var_p2]] inherit = VAR, PARALLEL cylc-flow-8.6.4/tests/functional/cylc-config/06-compat.t0000664000175000017500000000326415202510242023206 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test compat following https://github.com/cylc/cylc-flow/pull/3191 . "$(dirname "$0")/test_header" set_test_number 3 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = next(T00) [[dependencies]] [[[R1]]] graph = r1 [[[R1]]] graph = 'r2' [[[ R1 ]]] graph = """r3""" [[[R1]]] graph = """ r4 => r5 r6 => r7 """ [[[T06, T12]]] graph = t1 => t2 [[[ P1D!(01T, 11T) ]]] graph = t3 __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate . run_ok "${TEST_NAME_BASE}-dependencies" \ cylc config --item='[scheduling][graph]' . cmp_ok "${TEST_NAME_BASE}-dependencies.stdout" <<'__OUT__' R1 = """ r1 r2 r3 r4 => r5 r6 => r7 """ T06, T12 = t1 => t2 P1D!(01T, 11T) = t3 __OUT__ exit cylc-flow-8.6.4/tests/functional/shutdown/0000775000175000017500000000000015202510242020764 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/07-task-fail/0000775000175000017500000000000015202510242023063 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/07-task-fail/flow.cylc0000664000175000017500000000047615202510242024715 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT1M [scheduling] [[graph]] R1 = t1:finish => t2 [runtime] [[t1]] script = false [[[events]]] failed handlers = echo 'Unfortunately %(id)s %(event)s' [[t2]] script = true cylc-flow-8.6.4/tests/functional/shutdown/03-bad-cycle/0000775000175000017500000000000015202510242023027 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/03-bad-cycle/flow.cylc0000664000175000017500000000023315202510242024650 0ustar alastairalastair[scheduling] initial cycle point = 20100101T0 # missing a zero final cycle point = 201001050T0 [[graph]] T00 = t1 [runtime] [[t1]] cylc-flow-8.6.4/tests/functional/shutdown/22-stop-now.t0000664000175000017500000000303315202510242023157 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ``cylc stop --now`` shuts down the scheduler leaving orphaned tasks running. . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 1 cycling mode = integer [[graph]] R1 = foo [runtime] [[foo]] script = """ cylc stop --now "$CYLC_WORKFLOW_ID" sleep 60 # if the stop --kill fails then the job succeeds """ __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}" cylc play "${WORKFLOW_NAME}" --no-detach --debug WORKFLOW_LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" log_scan "${TEST_NAME_BASE}-orphaned" "${WORKFLOW_LOG}" 1 1 \ 'Orphaned tasks.*' \ '1/.*foo' purge cylc-flow-8.6.4/tests/functional/shutdown/07-task-fail.t0000775000175000017500000000316015202510242023253 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test task event handler runs after "--abort-if-any-task-fails". . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" grep_ok "ERROR - Workflow shutting down - AUTOMATIC(ON-TASK-FAILURE)" \ "${LOGD}/scheduler/log" JLOGD="${LOGD}/job/1/t1/01" # Check that 1/t1 event handler runs run_ok "${TEST_NAME_BASE}-activity-log" \ grep -q -F \ "[(('event-handler-00', 'failed'), 1) out] Unfortunately 1/t1 failed" \ "${JLOGD}/job-activity.log" # Check that t2.1 did not run exists_fail "${LOGD}/1/t2" purge exit cylc-flow-8.6.4/tests/functional/shutdown/06-kill-fail/0000775000175000017500000000000015202510242023053 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/06-kill-fail/flow.cylc0000664000175000017500000000027215202510242024677 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT1M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = sleep 60 cylc-flow-8.6.4/tests/functional/shutdown/15-bad-port-file-check-globalcfg0000777000175000017500000000000015202510242032406 212-bad-port-file-checkustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/13-no-port-file-check.t0000775000175000017500000000321015202510242024757 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down with error on missing port file . "$(dirname "$0")/test_header" set_test_number 3 OPT_SET= create_test_global_config "" " [scheduler] [[main loop]] # plugins = health check [[[health check]]] interval = PT11S" OPT_SET='-s GLOBALCFG=True' install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --abort-if-any-task-fails ${OPT_SET} "${WORKFLOW_NAME}" SRVD="$RUN_DIR/${WORKFLOW_NAME}/.service" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" grep_ok \ "${SRVD}/contact: contact file corrupted/modified and may be left" \ "${LOGD}/scheduler/log" purge exit cylc-flow-8.6.4/tests/functional/shutdown/02-no-dir.t0000775000175000017500000000330715202510242022566 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow can shutdown successfully if its run dir is deleted . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" create_test_global_config "" " [scheduler] [[main loop]] [[[health check]]] interval = PT10S" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Workflow run directory is now a symbolic link, so we can easily delete it. SYM_WORKFLOW_RUND="${WORKFLOW_RUN_DIR}-sym" SYM_WORKFLOW_NAME="${WORKFLOW_NAME}-sym" ln -s "$(basename "${WORKFLOW_NAME}")" "${SYM_WORKFLOW_RUND}" run_fail "${TEST_NAME_BASE}-run" cylc play "${SYM_WORKFLOW_NAME}" --debug --no-detach grep_ok 'CRITICAL - Workflow shutting down' "${WORKFLOW_RUN_DIR}/log/scheduler/"*.log grep_ok 'unable to open database file' "${WORKFLOW_RUN_DIR}/log/scheduler/"*.log rm -f "${SYM_WORKFLOW_RUND}" purge exit cylc-flow-8.6.4/tests/functional/shutdown/13-no-port-file-check/0000775000175000017500000000000015202510242024573 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/13-no-port-file-check/flow.cylc0000664000175000017500000000106215202510242026415 0ustar alastairalastair#!Jinja2 [scheduler] {% if GLOBALCFG is not defined %} [[main loop]] [[[health check]]] interval = PT10S {% endif %}{# not GLOBALCFG is not defined #} [[events]] abort on stall timeout = False stall timeout = PT0S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started # Remove contact file and don't report back to workflow rm -f "${CYLC_WORKFLOW_RUN_DIR}/.service/contact" exit 1 """ cylc-flow-8.6.4/tests/functional/shutdown/17-no-dir-check-globalcfg.t0000777000175000017500000000000015202510242030424 214-no-dir-check.tustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/14-no-dir-check.t0000775000175000017500000000410015202510242023634 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down with error on missing run directory . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" create_test_global_config "" " [scheduler] [[main loop]] [[[health check]]] interval = PT10S" OPT_SET='-s GLOBALCFG=True' # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # Workflow run directory is now a symbolic link, so we can easily delete it. SYM_WORKFLOW_RUND="${WORKFLOW_RUN_DIR}-sym" SYM_WORKFLOW_NAME="${WORKFLOW_NAME}-sym" ln -s "$(basename "${WORKFLOW_NAME}")" "${SYM_WORKFLOW_RUND}" # shellcheck disable=SC2086 workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --abort-if-any-task-fails ${OPT_SET} "${SYM_WORKFLOW_NAME}" # Possible failure modes: # - health check detects missing run directory # - DB housekeeping cannot access DB because run directory missing # - (TODO: if other failure modes show up, add to the list here!) FAIL1="Workflow run directory does not exist: ${SYM_WORKFLOW_RUND}" FAIL2="sqlite3.OperationalError: unable to open database file" grep_ok "(${FAIL1}|${FAIL2})" "${WORKFLOW_RUN_DIR}/log/scheduler/"*.log -E rm -f "${SYM_WORKFLOW_RUND}" purge exit cylc-flow-8.6.4/tests/functional/shutdown/12-bad-port-file-check/0000775000175000017500000000000015202510242024704 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/12-bad-port-file-check/flow.cylc0000664000175000017500000000114715202510242026532 0ustar alastairalastair#!Jinja2 [scheduler] {% if GLOBALCFG is not defined %} [[main loop]] [[[health check]]] interval = PT10S {% endif %}{# not GLOBALCFG is not defined #} [[events]] abort on stall timeout = False stall timeout = PT0S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started # Corrupt port file and don't report back to workflow SRVD="${CYLC_WORKFLOW_RUN_DIR}/.service" echo 'Haha! I have corrupted the port file!' >"${SRVD}/contact" exit 1 """ cylc-flow-8.6.4/tests/functional/shutdown/05-auto.t0000775000175000017500000000232115202510242022344 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test auto shutdown after all tasks have finished. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME=${TEST_NAME_BASE}-auto-stop workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/shutdown/18-client-on-dead-workflow.t0000775000175000017500000000342615202510242026042 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down with error on missing contact file # And correct behaviour with client on the next 2 connection attempts. . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" cylc play --pause --no-detach "${WORKFLOW_NAME}" 1>'cylc-run.out' 2>&1 & MYPID=$! poll_workflow_running kill "${MYPID}" # Should leave behind the contact file wait "${MYPID}" 1>'/dev/null' 2>&1 || true run_fail "${TEST_NAME_BASE}-1" cylc ping "${WORKFLOW_NAME}" contains_ok "${TEST_NAME_BASE}-1.stderr" <<__ERR__ WorkflowStopped: ${WORKFLOW_NAME} is not running __ERR__ purge exit cylc-flow-8.6.4/tests/functional/shutdown/04-kill/0000775000175000017500000000000015202510242022140 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/04-kill/reference.log0000664000175000017500000000012415202510242024576 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/shutdown/04-kill/flow.cylc0000664000175000017500000000052115202510242023761 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = """t1:start => t2""" [runtime] [[t1]] script = sleep 60 [[t2]] script = """ cylc shutdown "${CYLC_WORKFLOW_ID}" sleep 1 cylc kill "${CYLC_WORKFLOW_ID}//*/t1" """ cylc-flow-8.6.4/tests/functional/shutdown/04-kill.t0000775000175000017500000000173015202510242022331 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test kill OK after shutdown. Workflow will timeout if kill not OK. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/shutdown/16-no-port-file-check-globalcfg.t0000777000175000017500000000000015202510242032670 213-no-port-file-check.tustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/17-no-dir-check-globalcfg0000777000175000017500000000000015202510242027720 214-no-dir-checkustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/20-stop-after-runN-play.t0000664000175000017500000000406215202510242025341 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down, having been started with cylc play flow/runN . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 cat > 'flow.cylc' <<__FLOW_CONFIG__ [scheduler] [[events]] abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-install" cylc install 2>'/dev/null' popd || exit 1 run_ok "${TEST_NAME_BASE}-validate" cylc validate "${RND_WORKFLOW_RUNDIR}/runN" run_ok "${TEST_NAME_BASE}-play" cylc play "${RND_WORKFLOW_NAME}/runN" --pause LOG="${RND_WORKFLOW_RUNDIR}/run1/log/scheduler/log" run_ok "${TEST_NAME_BASE}-stop" cylc stop --now --now "${RND_WORKFLOW_NAME}/run1" log_scan "${TEST_NAME_BASE}-log-stop" "${LOG}" 20 1 \ "INFO - Workflow shutting down - REQUEST(NOW-NOW)" # stop workflow - workflow should already by stopped but just to be safe cylc stop --max-polls=10 --interval=2 --kill "${RND_WORKFLOW_NAME}/runN" 2>'/dev/null' purge_rnd_workflow cylc-flow-8.6.4/tests/functional/shutdown/01-task.t0000775000175000017500000000167315202510242022343 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test shutdown after a specific task. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/shutdown/09-now2.t0000775000175000017500000000377415202510242022302 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc stop --now --now". . "$(dirname "$0")/test_header" set_test_number 9 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" grep_ok 'INFO - Workflow shutting down - REQUEST(NOW-NOW)' "${LOGD}/scheduler/log" grep_ok 'WARNING - Orphaned tasks' "${LOGD}/scheduler/log" grep_ok '\* 1/t1 (running)' "${LOGD}/scheduler/log" JLOGD="${LOGD}/job/1/t1/01" # Check that 1/t1 event handler runs run_fail "${TEST_NAME_BASE}-activity-log-succeeded" \ grep -q -F \ "[(('event-handler-00', 'succeeded'), 1) out] Well done 1/t1 succeeded" \ "${JLOGD}/job-activity.log" run_fail "${TEST_NAME_BASE}-activity-log-started" \ grep -q -F \ "[(('event-handler-00', 'started'), 1) out] Hello 1/t1 started" \ "${JLOGD}/job-activity.log" # Check that 1/t2 did not run exists_fail "${LOGD}/job/1/t2" # In SoD the restart does not stall and abort, because 1/t1:failed can be removed # as handled. workflow_run_ok "${TEST_NAME_BASE}-restart" cylc play --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/shutdown/16-no-port-file-check-globalcfg0000777000175000017500000000000015202510242032164 213-no-port-file-checkustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/test_header0000777000175000017500000000000015202510242027301 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/02-no-dir/0000775000175000017500000000000015202510242022373 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/02-no-dir/flow.cylc0000664000175000017500000000042315202510242024215 0ustar alastairalastair[scheduling] [[graph]] R1 = bar [runtime] [[bar]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started sleep 2 cylc shutdown "${CYLC_WORKFLOW_ID}" rm -f "${CYLC_WORKFLOW_RUN_DIR}" exit 1 """ cylc-flow-8.6.4/tests/functional/shutdown/00-cycle.t0000775000175000017500000000167615202510242022502 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test shutdown after a particular cycle. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/shutdown/05-auto/0000775000175000017500000000000015202510242022156 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/05-auto/flow.cylc0000664000175000017500000000041115202510242023775 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] stall timeout = PT2M abort on stall timeout = true [scheduling] initial cycle point = 2020-01-01 final cycle point = 2020-01-01 [[graph]] P1D = foo [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/shutdown/08-now1.t0000775000175000017500000000306615202510242022272 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc stop --now" will wait for event handler. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play -v --no-detach "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" grep_ok 'INFO - Workflow shutting down - REQUEST(NOW)' "${LOGD}/scheduler/log" JLOGD="${LOGD}/job/1/t1/01" # Check that 1/t1 event handler runs run_ok "${TEST_NAME_BASE}-activity-log-started" \ grep -q -F \ "[(('event-handler-00', 'started'), 1) out] Hello 1/t1 started" \ "${JLOGD}/job-activity.log" # Check that t2.1 did not run exists_fail "${LOGD}/job/1/t2" purge exit cylc-flow-8.6.4/tests/functional/shutdown/08-now1/0000775000175000017500000000000015202510242022075 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/08-now1/flow.cylc0000664000175000017500000000206615202510242023724 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT1M [scheduling] [[graph]] R1 = t1 => t2 [runtime] [[t1]] script = """ # wait for the started message to be sent cylc__job__wait_cylc_message_started; # issue the stop command cylc stop --now "${CYLC_WORKFLOW_ID}" # wait for the stop command to be recieved cylc__job__poll_grep_workflow_log 'Command "stop" received' # send a message telling the started handler to exit cylc message -- stopping """ [[[events]]] # wait for the stopping message, sleep a bit, then echo some stuff started handlers = """ cylc workflow-state %(workflow)s//%(point)s/%(name)s:stopping --triggers >/dev/null && sleep 1 && echo 'Hello %(id)s %(event)s' """ [[[outputs]]] stopping = stopping [[t2]] script = true cylc-flow-8.6.4/tests/functional/shutdown/21-stop-kill.t0000664000175000017500000000313015202510242023304 0ustar alastairalastair#!/usr/bin/env bash # 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 . # ``cylc stop --kill`` kills running tasks before shutting down the scheduler . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 1 cycling mode = integer [[graph]] R1 = foo [runtime] [[foo]] script = """ cylc stop --kill "$CYLC_WORKFLOW_ID" sleep 60 # if the stop --kill fails then the job succeeds """ __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}" cylc play --no-detach "${WORKFLOW_NAME}" --debug WORKFLOW_LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" named_grep_ok 'jobs kill succeeded' "jobs-kill ret_code\] 0" "${WORKFLOW_LOG}" named_grep_ok 'jobs kill killed 1/foo' "jobs-kill out.*1/foo/01" "${WORKFLOW_LOG}" purge cylc-flow-8.6.4/tests/functional/shutdown/19-log-reference.t0000775000175000017500000000364415202510242024127 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down with reference log, specifically that there is no # issue in the shutdown method when the --reference-log option is used. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] [[events]] abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true __FLOW_CONFIG__ #------------------------------------------------------------------------------- workflow_run_ok "${TEST_NAME_BASE}-run-reflog" \ cylc play --debug --no-detach --reference-log "${WORKFLOW_NAME}" exists_ok "${HOME}/cylc-run/${WORKFLOW_NAME}/reference.log" delete_db workflow_run_ok "${TEST_NAME_BASE}-run-reftest" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/shutdown/03-bad-cycle.t0000775000175000017500000000311715202510242023221 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clean up of port file, on bad start with invalid initial cycle point. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- # N.B. No validate test because this workflow does not validate. TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play "${WORKFLOW_NAME}" --debug --no-detach RUND="$RUN_DIR/${WORKFLOW_NAME}" exists_fail "${RUND}/.service/contact" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/shutdown/14-no-dir-check/0000775000175000017500000000000015202510242023451 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/14-no-dir-check/flow.cylc0000664000175000017500000000116615202510242025300 0ustar alastairalastair#!Jinja2 [scheduler] {% if GLOBALCFG is not defined %} [[main loop]] [[[health check]]] interval = PT10S {% endif %}{# not GLOBALCFG is not defined #} [[events]] abort on stall timeout = False stall timeout = PT0S abort on stall timeout = True stall timeout = PT1M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started sleep 5 # Remove workflow run directory and don't report back to workflow rm -f "${CYLC_WORKFLOW_RUN_DIR}" exit 1 """ cylc-flow-8.6.4/tests/functional/shutdown/00-cycle/0000775000175000017500000000000015202510242022300 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/00-cycle/reference.log0000664000175000017500000000037115202510242024742 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100105T0000Z 20100101T0000Z/a -triggered off ['20091231T1800Z/a', '20091231T1800Z/c'] 20100101T0000Z/stopper -triggered off ['20100101T0000Z/a'] 20100101T0000Z/c -triggered off ['20100101T0000Z/stopper'] cylc-flow-8.6.4/tests/functional/shutdown/00-cycle/flow.cylc0000664000175000017500000000052315202510242024123 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100105T00 [[graph]] T00, T06, T12, T18 = "a[-PT6H] & c[-PT6H] => a => stopper => c" [runtime] [[a,c]] script = "true" [[stopper]] script = "cylc shutdown $CYLC_WORKFLOW_ID//2010-01-01; sleep 5" cylc-flow-8.6.4/tests/functional/shutdown/09-now2/0000775000175000017500000000000015202510242022077 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/09-now2/flow.cylc0000664000175000017500000000111715202510242023722 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT1M [scheduling] [[graph]] R1 = t1:finish => t2 [runtime] [[t1]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ sleep 1 cylc stop --now --now "${CYLC_WORKFLOW_ID}" exit 1 """ [[[events]]] started handlers = sleep 10 && echo 'Hello %(id)s %(event)s' succeeded handlers = echo 'Well done %(id)s %(event)s' [[t2]] script = true cylc-flow-8.6.4/tests/functional/shutdown/15-bad-port-file-check-globalcfg.t0000777000175000017500000000000015202510242033112 212-bad-port-file-check.tustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/12-bad-port-file-check.t0000775000175000017500000000324415202510242025077 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow shuts down with error on missing port file . "$(dirname "$0")/test_header" set_test_number 3 OPT_SET= if [[ "${TEST_NAME_BASE}" == *-globalcfg ]]; then create_test_global_config "" " [scheduler] [[main loop]] [[[health check]]] interval = PT10S" OPT_SET='-s GLOBALCFG=True' fi install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --abort-if-any-task-fails ${OPT_SET} "${WORKFLOW_NAME}" SRVD="$RUN_DIR/${WORKFLOW_NAME}/.service" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" grep_ok \ "${SRVD}/contact: contact file corrupted/modified and may be left" \ "${LOGD}/scheduler/log" purge exit cylc-flow-8.6.4/tests/functional/shutdown/01-task/0000775000175000017500000000000015202510242022144 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/shutdown/01-task/reference.log0000664000175000017500000000043215202510242024604 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100105T0000Z 20100101T0000Z/a -triggered off ['20091231T1800Z/c'] 20100101T0000Z/stopper -triggered off ['20100101T0000Z/a'] 20100101T0000Z/c -triggered off ['20100101T0000Z/stopper'] 20100101T0600Z/a -triggered off ['20100101T0000Z/c'] cylc-flow-8.6.4/tests/functional/shutdown/01-task/flow.cylc0000664000175000017500000000050115202510242023763 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100105T00 [[graph]] PT6H = "c[-PT6H] => a => stopper => c" [runtime] [[a,c]] script = "true" [[stopper]] script = """ cylc shutdown $CYLC_WORKFLOW_ID 20100101T06/a; sleep 5""" cylc-flow-8.6.4/tests/functional/cylc-lint/0000775000175000017500000000000015202510242021007 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-lint/01.lint-toml.t0000664000175000017500000000572315202510242023341 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test linting with a toml file present. . "$(dirname "$0")/test_header" set_test_number 12 # Set Up: rm etc/global.cylc LINE_LEN_NO=$(python -c "from cylc.flow.scripts.lint import LINE_LEN_NO; print(LINE_LEN_NO)") cat > flow.cylc <<__HERE__ # This is definitely not an OK flow.cylc file. \t[scheduler] [cylc] [[dependencies]] [runtime] [[foo]] inherit = hello [[[job]]] something\t __HERE__ mkdir sites cat > sites/niwa.cylc <<__HERE__ blatantly = not valid __HERE__ # Control tests TEST_NAME="it lints without toml file" run_fail "${TEST_NAME}" cylc lint TESTOUT="${TEST_NAME}.stdout" named_grep_ok "it returns error code" "S004" "${TESTOUT}" named_grep_ok "it returns error from subdirectory" "niwa.cylc" "${TESTOUT}" named_grep_ok "it returns a 728 upgrade code" "^\[U" "${TESTOUT}" # Add a pyproject.toml file cat > pyproject.toml <<__HERE__ [tool.cylc.lint] # Check against these rules rulesets = [ "style" ] # do not check for these errors ignore = [ "S004" ] # do not lint files matching # these globs: exclude = [ "sites/*.cylc", ] __HERE__ # Test that results are different: TEST_NAME="it_lints_with_toml_file" run_fail "${TEST_NAME}" cylc lint TESTOUT="${TEST_NAME}.stdout" grep_fail "S004" "${TESTOUT}" grep_fail "niwa.cylc" "${TESTOUT}" grep_fail "^\[U" "${TESTOUT}" # Add a max line length to the pyproject.toml. echo "" >> pyproject.toml echo "max-line-length = 4" >> pyproject.toml cat > flow.cylc <<__HERE__ script = """ How long a line is too long a line """ __HERE__ TEST_NAME="it_fails_if_max-line-length_set" run_fail "${TEST_NAME}" cylc lint named_grep_ok "${TEST_NAME}-line-too-long-message" \ "\[${LINE_LEN_NO}\] flow.cylc:2: line > 4 characters." \ "${TEST_NAME}.stdout" TEST_NAME="it_does_not_fail_if_max-line-length_set_but_ignored" cat > pyproject.toml <<__HERE__ [tool.cylc.lint] # Check against these rules rulesets = [ "style" ] # do not check for these errors ignore = [ "${LINE_LEN_NO}" ] exclude = [ "sites/*.cylc", ] max-line-length = 1 __HERE__ run_ok "${TEST_NAME}" cylc lint grep_ok "rules and found no issues" "${TEST_NAME}.stdout" cylc-flow-8.6.4/tests/functional/cylc-lint/00.lint.t0000664000175000017500000000563015202510242022364 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow installation . "$(dirname "$0")/test_header" set_test_number 20 cat > flow.cylc <<__HERE__ # This is definitely not an OK flow.cylc file. [cylc] [[parameters]] __HERE__ rm etc/global.cylc TEST_NAME="${TEST_NAME_BASE}.vanilla" run_fail "${TEST_NAME}" cylc lint . named_grep_ok "check-for-error-code" "S004" "${TEST_NAME}.stdout" TEST_NAME="${TEST_NAME_BASE}.pick-a-ruleset" run_fail "${TEST_NAME}" cylc lint . -r 728 named_grep_ok "check-for-error-code" "U998" "${TEST_NAME}.stdout" TEST_NAME="${TEST_NAME_BASE}.inplace" run_fail "${TEST_NAME}" cylc lint . -i named_grep_ok "check-for-error-code-in-file" "U998" flow.cylc rm flow.cylc cat > suite.rc <<__HERE__ # This is definitely not an OK flow.cylc file. {{FOO}} __HERE__ TEST_NAME="${TEST_NAME_BASE}.pick-a-ruleset-728" run_fail "${TEST_NAME}" cylc lint . -r 728 named_grep_ok "do-not-upgrade-check-if-compat-mode" "Lint after renaming" "${TEST_NAME}.stderr" TEST_NAME="${TEST_NAME_BASE}.pick-a-ruleset-728-exit-zero" run_ok "${TEST_NAME}" cylc lint . -r 728 --exit-zero TEST_NAME="${TEST_NAME_BASE}.pick-a-ruleset-all" run_fail "${TEST_NAME}" cylc lint . -r all TEST_NAME="${TEST_NAME_BASE}.exit-zero" run_ok "${TEST_NAME}" cylc lint --exit-zero . rm suite.rc cat > flow.cylc <<__HERE__ # This one is fine [scheduler] __HERE__ TEST_NAME="${TEST_NAME_BASE}.zero-issues" run_ok "${TEST_NAME}" cylc lint . named_grep_ok "message on no errors" "found no issues" "${TEST_NAME}.stdout" # It returns an error message if you attempt to lint a non-existant location TEST_NAME="it-fails-if-not-target" run_fail ${TEST_NAME} cylc lint "a-$(uuidgen)" grep_ok "Workflow ID not found" "${TEST_NAME}.stderr" # It returns a reference in reference mode TEST_NAME="it-returns-a-reference" run_ok "${TEST_NAME}" cylc lint --list-codes named_grep_ok "${TEST_NAME}-contains-style-codes" "^S001:" "${TEST_NAME}.stdout" TEST_NAME="it-returns-a-reference-style" run_ok "${TEST_NAME}" cylc lint --list-codes -r 'style' named_grep_ok "${TEST_NAME}-contains-style-codes" "^S001:" "${TEST_NAME}.stdout" grep_fail "^U" "${TEST_NAME}.stdout" rm flow.cylc cylc-flow-8.6.4/tests/functional/cylc-lint/test_header0000777000175000017500000000000015202510242027324 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/0000775000175000017500000000000015202510242021544 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1.t0000775000175000017500000000607515202510242023556 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test handle of SIGUSR1. (Handle a mock job vacation.) # Obviously, job vacation does not happen with background job, and the job # will no longer be poll-able after the kill. . "$(dirname "$0")/test_header" skip_all "TODO decide whether to re-instate this" run_tests() { set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" # Needs to be detaching: workflow_run_ok "${TEST_NAME}" cylc play --reference-test "${WORKFLOW_NAME}" # Make sure 1/t1's status file is in place T1_STATUS_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.status" poll_grep -E 'CYLC_JOB_ID=' "${T1_STATUS_FILE}" poll_grep -E 'CYLC_JOB_INIT_TIME=' "${T1_STATUS_FILE}" # Kill the job and see what happens T1_PID="$(awk -F= '$1=="CYLC_JOB_ID" {print $2}' "${T1_STATUS_FILE}")" kill -s 'USR1' "${T1_PID}" poll_grep -E 'WARNING|vacated/USR1' "${T1_STATUS_FILE}" poll_grep_workflow_log 'vacated/USR1' sleep 1 # a bit of extra time for workflow db update to complete sqlite3 "${WORKFLOW_RUN_DIR}/log/db" \ "SELECT status FROM task_states WHERE name=='t1';" \ >"${TEST_NAME}-db-t1" 2>'/dev/null' grep_ok "^\(submitted\|running\)$" "${TEST_NAME}-db-t1" # Start the job again and see what happens mkdir -p "${WORKFLOW_RUN_DIR}/work/1/t1/" touch "${WORKFLOW_RUN_DIR}/work/1/t1/file" # Allow t1 to complete "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job" <'/dev/null' >'/dev/null' 2>&1 & # Wait for workflow to complete poll_workflow_stopped # Test t1 status in DB sqlite3 "${WORKFLOW_RUN_DIR}/log/db" \ "SELECT status FROM task_states WHERE name=='t1';" >"${TEST_NAME}-db-t1" cmp_ok "${TEST_NAME}-db-t1" - <<<'succeeded' # Test reference grep_ok 'WORKFLOW REFERENCE TEST PASSED' "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge exit } # Programs running in some environment is unable to trap SIGUSR1. E.g.: # An environment documented in this comment: # https://github.com/cylc/cylc-flow/pull/1648#issuecomment-149348410 trap 'run_tests' 'SIGUSR1' kill -s 'SIGUSR1' "$$" sleep 1 skip_all 'Program not receiving SIGUSR1' cylc-flow-8.6.4/tests/functional/job-file-trap/01-loadleveler/0000775000175000017500000000000015202510242024260 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/01-loadleveler/reference.log0000664000175000017500000000011615202510242026717 0ustar alastairalastairInitial point: 1 Final point: 1 1/t2 -triggered off [] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/job-file-trap/01-loadleveler/flow.cylc0000664000175000017500000000073015202510242026103 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1 = """ t1 t2 """ [runtime] [[root]] script=true platform = {{environ["CYLC_TEST_PLATFORM"]}} [[[directives]]] class=serial job_type=serial notification=never resources=ConsumableCpus(1) ConsumableMemory(64mb) wall_clock_limit=180,120 [[t1]] [[t2]] [[[directives]]] restart=yes cylc-flow-8.6.4/tests/functional/job-file-trap/02-pipefail.t0000775000175000017500000000321015202510242023740 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test pipefail cylc/cylc-flow#1783 . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}-validate" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test "${WORKFLOW_NAME}" # Make sure status files are in place T1_STATUS_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.status" contains_ok "${T1_STATUS_FILE}" <<'__STATUS__' CYLC_JOB_EXIT=ERR __STATUS__ grep_ok 'CYLC_JOB_EXIT_TIME=' "${T1_STATUS_FILE}" T2_STATUS_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t2/01/job.status" contains_ok "${T2_STATUS_FILE}" <<'__STATUS__' CYLC_JOB_EXIT=EXIT __STATUS__ grep_ok 'CYLC_JOB_EXIT_TIME=' "${T2_STATUS_FILE}" purge exit cylc-flow-8.6.4/tests/functional/job-file-trap/02-pipefail/0000775000175000017500000000000015202510242023554 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/02-pipefail/reference.log0000664000175000017500000000006715202510242026220 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/job-file-trap/02-pipefail/flow.cylc0000664000175000017500000000046715202510242025406 0ustar alastairalastair#!jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 1/t1, 1/t2 [scheduling] [[graph]] R1=t1 & t2 [runtime] [[t1]] script=false|cat [[t2]] # Trigger SIGPIPE signal exit. script=yes|true cylc-flow-8.6.4/tests/functional/job-file-trap/03-user-trap.t0000664000175000017500000000533015202510242024074 0ustar alastairalastair#!/bin/bash # 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 . #------------------------------------------------------------------------------- # Test that a user script can have its own TERM signal trap. . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__WORKFLOW__' [cylc] [[events]] abort on inactivity timeout = True abort on stall timeout = True stall timeout = PT0S inactivity timeout = PT1M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ # ignore TERM signal for the next child trap "" TERM ( cylc__job__poll_grep_workflow_log "Workflow shutting down" sleep 5 echo "The undead child shall speak" >>"${CYLC_TASK_LOG_ROOT}.testout" ) & trap "echo 'TERM got trapped' >>'${CYLC_TASK_LOG_ROOT}.testout'; wait" TERM # this child will be terminated if job script is a process leader ( sleep 15 echo "You shall never see this" >>"${CYLC_TASK_LOG_ROOT}.testout" ) & echo "Here we go..." >>"${CYLC_TASK_LOG_ROOT}.testout" wait """ err-script = """ wait echo "Exit with code ${CYLC_TASK_USER_SCRIPT_EXITCODE:-unknown}" \ >>"${CYLC_TASK_LOG_ROOT}.testout" """ __WORKFLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # Needs to be detaching: workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" JOB_LOG_ROOT="${WORKFLOW_RUN_DIR}/log/job/1/foo/01/job" # Kill the job and see what happened poll_grep "CYLC_JOB_PID=" "${JOB_LOG_ROOT}.status" JOB_PID=$(awk -F= '/CYLC_JOB_PID=/{print $2}' "${JOB_LOG_ROOT}.status") kill -s "TERM" "${JOB_PID}" poll_workflow_stopped # Workflow is down after failed message grep_ok "CYLC_JOB_EXIT=TERM" "${JOB_LOG_ROOT}.status" # The job should be still running and waiting for the user script grep_fail "The undead child shall speak" "${JOB_LOG_ROOT}.testout" poll_grep "Exit with code" "${JOB_LOG_ROOT}.testout" cmp_ok "${JOB_LOG_ROOT}.testout" - <<__EOF__ Here we go... TERM got trapped The undead child shall speak Exit with code 143 __EOF__ purge exit cylc-flow-8.6.4/tests/functional/job-file-trap/test_header0000777000175000017500000000000015202510242030061 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/01-loadleveler.t0000775000175000017500000000453715202510242024461 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test whether job vacation trap is included in a loadleveler job or not. # A job for a task with the restart=yes directive will have the trap. # This does not test loadleveler job vacation itself, because the test will # require a site admin to pre-empt a job. # TODO Check this test on a dockerized system or VM. export REQUIRE_PLATFORM="runner:loadleveler" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" sleep 5 #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-t1.1" T1_JOB_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job" exists_ok "${T1_JOB_FILE}" run_fail "${TEST_NAME}" grep -q -e '^CYLC_VACATION_SIGNALS' "${T1_JOB_FILE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-t2.1" T2_JOB_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t2/01/job" exists_ok "${T2_JOB_FILE}" grep_ok '^CYLC_VACATION_SIGNALS' "${T2_JOB_FILE}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1/0000775000175000017500000000000015202510242023356 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1/python/0000775000175000017500000000000015202510242024677 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1/python/background_vacation.py0000664000175000017500000000222215202510242031252 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.job_runner_handlers.background import BgCommandHandler class MyBgCommandHandler(BgCommandHandler): """Job submission class for use by test battery. Allow a background submission to have a job vacation signal. """ VACATION_SIGNAL = "USR1" def get_vacation_signal(self, _): return self.VACATION_SIGNAL JOB_RUNNER_HANDLER = MyBgCommandHandler() cylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1/reference.log0000664000175000017500000000012415202510242026014 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/job-file-trap/00-sigusr1/flow.cylc0000664000175000017500000000045115202510242025201 0ustar alastairalastair#!jinja2 [scheduling] [[graph]] R1=t1=>t2 [runtime] [[t1]] script=""" TIMEOUT=$(($(date +%s) + 120)) while [[ ! -e file ]] && (($TIMEOUT > $(date +%s))); do sleep 1 done """ [[[job]]] batch system=background_vacation [[t2]] script=true cylc-flow-8.6.4/tests/functional/queues/0000775000175000017500000000000015202510242020420 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/queues/01-queuesize-5.t0000664000175000017500000000317415202510242023211 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with a queue with limit=5 . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" qsize #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -s 'q_size="5"' "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ -s 'q_size="5"' "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/queues/02-queueorder.t0000664000175000017500000000252415202510242023207 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job script sets dependencies in environment. . "$(dirname "${0}")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --reference-test --debug --no-detach --timestamp run_ok "${TEST_NAME_BASE}-test" bash -o pipefail -c " cylc cat-log '${WORKFLOW_NAME}' | grep 'proc_n.*submitted at' | sort --key=4,4 --check" purge exit cylc-flow-8.6.4/tests/functional/queues/qsize/0000775000175000017500000000000015202510242021553 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/queues/qsize/reference.log0000664000175000017500000000071015202510242024212 0ustar alastairalastairInitial point: 1 Final point: 1 1/monitor -triggered off [] 1/a -triggered off ['1/monitor'] 1/c -triggered off ['1/monitor'] 1/b -triggered off ['1/monitor'] 1/e -triggered off ['1/monitor'] 1/g -triggered off ['1/monitor'] 1/f -triggered off ['1/monitor'] 1/i -triggered off ['1/monitor'] 1/h -triggered off ['1/monitor'] 1/k -triggered off ['1/monitor'] 1/j -triggered off ['1/monitor'] 1/l -triggered off ['1/monitor'] 1/d -triggered off ['1/monitor'] cylc-flow-8.6.4/tests/functional/queues/qsize/flow.cylc0000664000175000017500000000123715202510242023401 0ustar alastairalastair#!Jinja2 [scheduling] [[ queues ]] [[[ q_fam ]]] limit = {{q_size}} # allow testing with various queue sizes members = monitor, FAM [[graph]] R1 = monitor:start => FAM [runtime] [[FAM]] script = true [[a,b,c,d,e,f,g,h,i,j,k,l]] inherit = FAM [[monitor]] script = """ N_SUCCEEDED=0 while ((N_SUCCEEDED < 12)); do sleep 1 N_RUNNING=$(cylc dump -l -t $CYLC_WORKFLOW_ID | grep running | wc -l) ((N_RUNNING <= {{q_size}})) # check N_SUCCEEDED=$(cylc workflow-state "${CYLC_WORKFLOW_ID}//*/*:succeeded" | wc -l) done """ cylc-flow-8.6.4/tests/functional/queues/test_header0000777000175000017500000000000015202510242026735 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/queues/02-queueorder/0000775000175000017500000000000015202510242023017 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/queues/02-queueorder/reference.log0000664000175000017500000000114415202510242025460 0ustar alastairalastairInitial point: 1 Final point: 1 1/delay_n1 -triggered off [] 1/hold -triggered off [] 1/delay_n2 -triggered off ['1/delay_n1'] 1/delay_n3 -triggered off ['1/delay_n2'] 1/delay_n4 -triggered off ['1/delay_n3'] 1/delay_n5 -triggered off ['1/delay_n4'] 1/proc_n1 -triggered off ['1/delay_n1'] 1/delay_n6 -triggered off ['1/delay_n5'] 1/proc_n2 -triggered off ['1/delay_n2'] 1/delay_n7 -triggered off ['1/delay_n6'] 1/proc_n3 -triggered off ['1/delay_n3'] 1/proc_n4 -triggered off ['1/delay_n4'] 1/proc_n5 -triggered off ['1/delay_n5'] 1/proc_n6 -triggered off ['1/delay_n6'] 1/proc_n7 -triggered off ['1/delay_n7'] cylc-flow-8.6.4/tests/functional/queues/02-queueorder/flow.cylc0000664000175000017500000000052615202510242024645 0ustar alastairalastair[task parameters] n = 1..7 [scheduling] [[queues]] [[[q1]]] limit = 1 members = proc, hold [[graph]] R1 = """ delay => delay delay => proc hold """ [runtime] [[delay]] [[proc]] [[hold]] script = sleep 7 cylc-flow-8.6.4/tests/functional/queues/00-queuesize-3.t0000664000175000017500000000317515202510242023207 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with a queue with limit=3 . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" qsize #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -s 'q_size="3"' "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ -s "q_size='3'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/0000775000175000017500000000000015202510242023233 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/00-basic/0000775000175000017500000000000015202510242024531 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/00-basic/reference.log0000664000175000017500000000007015202510242027167 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/00-basic/flow.cylc0000664000175000017500000000053515202510242026357 0ustar alastairalastair[meta] title = Test for the get-cylc-version command. description = """A task compares its own cylc version to that running the test workflow (should be the same).""" [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ diff -u <(cylc --version) <(cylc get-cylc-version "${CYLC_WORKFLOW_ID}") """ cylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/test_header0000777000175000017500000000000015202510242031550 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-cylc-version/00-basic.t0000664000175000017500000000166215202510242024723 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Basic get-cylc-version test. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-message/0000775000175000017500000000000015202510242021465 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-message/00-ssh/0000775000175000017500000000000015202510242022477 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-message/00-ssh/reference.log0000664000175000017500000000010315202510242025132 0ustar alastairalastairFinal point: 1 1/t0 -triggered off [] 1/t1 -triggered off ['1/t0'] cylc-flow-8.6.4/tests/functional/cylc-message/00-ssh/flow.cylc0000664000175000017500000000050715202510242024324 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True # Ignore DST [scheduling] [[graph]] R1 = t0 => t1 [runtime] [[t0]] script = """ cylc broadcast "${CYLC_WORKFLOW_ID}" --name=t1 --set=script="true" """ platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[t1]] script = false cylc-flow-8.6.4/tests/functional/cylc-message/test_header0000777000175000017500000000000015202510242030002 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-message/02-multi.t0000775000175000017500000000507615202510242023236 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test "cylc message" with multiple messages. export REQUIRE_PLATFORM='loc:* comms:?(tcp|ssh)' . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<__FLOW__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] platform = $CYLC_TEST_PLATFORM script = """ cylc__job__wait_cylc_message_started cylc message -p WARNING "\${CYLC_WORKFLOW_ID}" "\${CYLC_TASK_JOB}" \ "Warn this" "INFO: Greeting" - <<'__MESSAGES__' Warn that DEBUG: Remove stuffs such as badness slowness and other incorrectness. CUSTOM: whatever __MESSAGES__ """ __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --debug --no-detach "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" sed -r -n -e 's/^.* ([A-Z]+ .* \(received\).*$)/\1/p' \ -e '/badness|slowness|and other incorrectness/p' \ "${LOG}" >'sed.out' sed -i 's/\(^.*\) at .*$/\1/;' 'sed.out' # Note: the continuation bit gets printed twice, because the message gets a # warning as being unhandled. cmp_ok 'sed.out' <<__LOG__ DEBUG - [1/foo/01:submitted] (received)started WARNING - [1/foo/01:running] (received)Warn this INFO - [1/foo/01:running] (received)Greeting WARNING - [1/foo/01:running] (received)Warn that DEBUG - [1/foo/01:running] (received)Remove stuffs such as ${LOG_INDENT}badness ${LOG_INDENT}slowness ${LOG_INDENT}and other incorrectness. ${LOG_INDENT}badness ${LOG_INDENT}slowness ${LOG_INDENT}and other incorrectness. INFO - [1/foo/01:running] (received)whatever DEBUG - [1/foo/01:running] (received)succeeded __LOG__ purge exit cylc-flow-8.6.4/tests/functional/cylc-message/00-ssh.t0000775000175000017500000000256115202510242022673 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc message" in SSH mode, test needs to have compatible version # installed on the remote host. export REQUIRE_PLATFORM='loc:remote comms:ssh' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/cylc-message/01-newline.t0000775000175000017500000000403715202510242023540 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test "cylc message" with multi-line messages. The RE to strip 'at

=> bar

" [runtime] [[root]] script = true [[foo

]] [[bar

]] __WORKFLOW__ run_ok "${TEST_NAME_BASE}-13" cylc validate . cylc graph --reference . >'13.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/13.graph.ref" '13.graph' # Parameter as task name cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = 0..2 s = mercury, venus, earth, mars [[templates]] i = i%(i)d s = %(s)s [scheduling] [[graph]] R1 = """ foo => => bar foo => => bar """ [runtime] [[foo, bar, , ]] script = true __WORKFLOW__ run_ok "${TEST_NAME_BASE}-14" cylc validate . cylc graph --reference . >'14.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/14.graph.ref" '14.graph' # Parameter in middle of family name cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] s = mercury, venus, earth, mars [scheduling] [[graph]] R1 = XY [runtime] [[XY]] script = true [[xy]] inherit = XY __WORKFLOW__ run_ok "${TEST_NAME_BASE}-15" cylc validate . cylc graph --reference --group="" . >'15.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/15.graph.ref" '15.graph' # -ve offset on RHS cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] m = cat, dog [scheduling] [[graph]] R1 = "foo => foo" [runtime] [[root]] script = true [[foo]] __WORKFLOW__ run_ok "${TEST_NAME_BASE}-16" cylc validate . cylc graph --reference . >'16.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/16.graph.ref" '16.graph' # +ve offset cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] m = cat, dog [scheduling] [[graph]] R1 = "foo => foo" [runtime] [[root]] script = true [[foo]] __WORKFLOW__ run_ok "${TEST_NAME_BASE}-17" cylc validate . cylc graph --reference . >'17.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/17.graph.ref" '17.graph' # Negative integers cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] m = -12..12..6 [scheduling] [[graph]] R1 = "foo" [runtime] [[root]] script = true [[foo]] __WORKFLOW__ run_ok "${TEST_NAME_BASE}-18" cylc validate . cylc graph --reference . >'18.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/18.graph.ref" '18.graph' # Reference by value, with -+ meta characters cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] lang = c++, fortran-2008 [[templates]] lang = %(lang)s [scheduling] [[graph]] R1 = " => " [runtime] [[]] script = true [[]] [[[environment]]] CC = gcc [[]] [[[environment]]] FC = gfortran __WORKFLOW__ run_ok "${TEST_NAME_BASE}-19" cylc validate --debug . cylc graph --reference . >'19.graph' cmp_ok "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/19.graph.ref" '19.graph' # Note: This also demonstrates current badness of "cylc config"... # Inconsistence between graph/runtime whitespace handling. # Inconsistence between graph/runtime parameter expansion. cylc config . >'19.cylc' cmp_ok '19.cylc' <<'__FLOW_CONFIG__' [task parameters] lang = c++, fortran-2008 [[templates]] lang = %(lang)s [scheduling] initial cycle point = 1 final cycle point = 1 cycling mode = integer [[graph]] R1 = => [runtime] [[root]] [[c++]] script = true completion = succeeded [[[environment]]] CC = gcc [[fortran-2008]] script = true completion = succeeded [[[environment]]] FC = gfortran __FLOW_CONFIG__ exit cylc-flow-8.6.4/tests/functional/param_expand/01-basic/0000775000175000017500000000000015202510242023047 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/param_expand/01-basic/01.graph.ref0000664000175000017500000000242115202510242025064 0ustar alastairalastairedge "1/foo_cat" "1/bar_j1" edge "1/foo_cat" "1/bar_j2" edge "1/foo_cat" "1/bar_j3" edge "1/foo_cat" "1/bar_j4" edge "1/foo_cat" "1/bar_j5" edge "1/foo_dog" "1/bar_j1" edge "1/foo_dog" "1/bar_j2" edge "1/foo_dog" "1/bar_j3" edge "1/foo_dog" "1/bar_j4" edge "1/foo_dog" "1/bar_j5" edge "1/foo_fish" "1/bar_j1" edge "1/foo_fish" "1/bar_j2" edge "1/foo_fish" "1/bar_j3" edge "1/foo_fish" "1/bar_j4" edge "1/foo_fish" "1/bar_j5" edge "1/qux_j1" "1/waz_k1" edge "1/qux_j1" "1/waz_k5" edge "1/qux_j1" "1/waz_k9" edge "1/qux_j2" "1/waz_k1" edge "1/qux_j2" "1/waz_k5" edge "1/qux_j2" "1/waz_k9" edge "1/qux_j3" "1/waz_k1" edge "1/qux_j3" "1/waz_k5" edge "1/qux_j3" "1/waz_k9" edge "1/qux_j4" "1/waz_k1" edge "1/qux_j4" "1/waz_k5" edge "1/qux_j4" "1/waz_k9" edge "1/qux_j5" "1/waz_k1" edge "1/qux_j5" "1/waz_k5" edge "1/qux_j5" "1/waz_k9" graph node "1/bar_j1" "bar_j1\n1" node "1/bar_j2" "bar_j2\n1" node "1/bar_j3" "bar_j3\n1" node "1/bar_j4" "bar_j4\n1" node "1/bar_j5" "bar_j5\n1" node "1/foo_cat" "foo_cat\n1" node "1/foo_dog" "foo_dog\n1" node "1/foo_fish" "foo_fish\n1" node "1/qux_j1" "qux_j1\n1" node "1/qux_j2" "qux_j2\n1" node "1/qux_j3" "qux_j3\n1" node "1/qux_j4" "qux_j4\n1" node "1/qux_j5" "qux_j5\n1" node "1/waz_k1" "waz_k1\n1" node "1/waz_k5" "waz_k5\n1" node "1/waz_k9" "waz_k9\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/17.graph.ref0000664000175000017500000000014415202510242025073 0ustar alastairalastairedge "1/foo_cat" "1/foo_dog" graph node "1/foo_cat" "foo_cat\n1" node "1/foo_dog" "foo_dog\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/13.graph.ref0000664000175000017500000000062315202510242025071 0ustar alastairalastairedge "1/foo%percent" "1/bar%percent" edge "1/foo+plus" "1/bar+plus" edge "1/foo-minus" "1/bar-minus" edge "1/foo@at" "1/bar@at" graph node "1/bar%percent" "bar%percent\n1" node "1/bar+plus" "bar+plus\n1" node "1/bar-minus" "bar-minus\n1" node "1/bar@at" "bar@at\n1" node "1/foo%percent" "foo%percent\n1" node "1/foo+plus" "foo+plus\n1" node "1/foo-minus" "foo-minus\n1" node "1/foo@at" "foo@at\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/16.graph.ref0000664000175000017500000000014415202510242025072 0ustar alastairalastairedge "1/foo_dog" "1/foo_cat" graph node "1/foo_cat" "foo_cat\n1" node "1/foo_dog" "foo_dog\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/04.graph.ref0000664000175000017500000000067515202510242025100 0ustar alastairalastairedge "1/foo_100" "1/bar_100" edge "1/foo_99+1" "1/bar_99+1" edge "1/foo_hundred" "1/bar_hundred" edge "1/foo_one-hundred" "1/bar_one-hundred" graph node "1/bar_100" "bar_100\n1" node "1/bar_99+1" "bar_99+1\n1" node "1/bar_hundred" "bar_hundred\n1" node "1/bar_one-hundred" "bar_one-hundred\n1" node "1/foo_100" "foo_100\n1" node "1/foo_99+1" "foo_99+1\n1" node "1/foo_hundred" "foo_hundred\n1" node "1/foo_one-hundred" "foo_one-hundred\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/18.graph.ref0000664000175000017500000000025315202510242025075 0ustar alastairalastairgraph node "1/foo_m+00" "foo_m+00\n1" node "1/foo_m+06" "foo_m+06\n1" node "1/foo_m+12" "foo_m+12\n1" node "1/foo_m-06" "foo_m-06\n1" node "1/foo_m-12" "foo_m-12\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/02.graph.ref0000664000175000017500000000233615202510242025072 0ustar alastairalastairedge "1/foo_i001" "1/bar_i001" edge "1/foo_i002" "1/bar_i002" edge "1/foo_i003" "1/bar_i003" edge "1/foo_i004" "1/bar_i004" edge "1/foo_i005" "1/bar_i005" edge "1/foo_i025" "1/bar_i025" edge "1/foo_i030" "1/bar_i030" edge "1/foo_i031" "1/bar_i031" edge "1/foo_i032" "1/bar_i032" edge "1/foo_i033" "1/bar_i033" edge "1/foo_i034" "1/bar_i034" edge "1/foo_i035" "1/bar_i035" edge "1/foo_i110" "1/bar_i110" graph node "1/bar_i001" "bar_i001\n1" node "1/bar_i002" "bar_i002\n1" node "1/bar_i003" "bar_i003\n1" node "1/bar_i004" "bar_i004\n1" node "1/bar_i005" "bar_i005\n1" node "1/bar_i025" "bar_i025\n1" node "1/bar_i030" "bar_i030\n1" node "1/bar_i031" "bar_i031\n1" node "1/bar_i032" "bar_i032\n1" node "1/bar_i033" "bar_i033\n1" node "1/bar_i034" "bar_i034\n1" node "1/bar_i035" "bar_i035\n1" node "1/bar_i110" "bar_i110\n1" node "1/foo_i001" "foo_i001\n1" node "1/foo_i002" "foo_i002\n1" node "1/foo_i003" "foo_i003\n1" node "1/foo_i004" "foo_i004\n1" node "1/foo_i005" "foo_i005\n1" node "1/foo_i025" "foo_i025\n1" node "1/foo_i030" "foo_i030\n1" node "1/foo_i031" "foo_i031\n1" node "1/foo_i032" "foo_i032\n1" node "1/foo_i033" "foo_i033\n1" node "1/foo_i034" "foo_i034\n1" node "1/foo_i035" "foo_i035\n1" node "1/foo_i110" "foo_i110\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/07.graph.ref0000664000175000017500000000024515202510242025074 0ustar alastairalastairedge "1/foo_a" "1/bar_a" edge "1/foo_b" "1/bar_b" graph node "1/bar_a" "bar_a\n1" node "1/bar_b" "bar_b\n1" node "1/foo_a" "foo_a\n1" node "1/foo_b" "foo_b\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/19.graph.ref0000664000175000017500000000014715202510242025100 0ustar alastairalastairedge "1/c++" "1/fortran-2008" graph node "1/c++" "c++\n1" node "1/fortran-2008" "fortran-2008\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/12.graph.ref0000664000175000017500000000100415202510242025062 0ustar alastairalastairedge "1/foo+%j001" "1/bar+%j001" edge "1/foo+%j002" "1/bar+%j002" edge "1/foo+%j003" "1/bar+%j003" edge "1/foo+%j004" "1/bar+%j004" edge "1/foo+%j005" "1/bar+%j005" graph node "1/bar+%j001" "bar+%j001\n1" node "1/bar+%j002" "bar+%j002\n1" node "1/bar+%j003" "bar+%j003\n1" node "1/bar+%j004" "bar+%j004\n1" node "1/bar+%j005" "bar+%j005\n1" node "1/foo+%j001" "foo+%j001\n1" node "1/foo+%j002" "foo+%j002\n1" node "1/foo+%j003" "foo+%j003\n1" node "1/foo+%j004" "foo+%j004\n1" node "1/foo+%j005" "foo+%j005\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/11.graph.ref0000664000175000017500000000071015202510242025064 0ustar alastairalastairedge "1/foo@001" "1/bar@001" edge "1/foo@002" "1/bar@002" edge "1/foo@003" "1/bar@003" edge "1/foo@004" "1/bar@004" edge "1/foo@005" "1/bar@005" graph node "1/bar@001" "bar@001\n1" node "1/bar@002" "bar@002\n1" node "1/bar@003" "bar@003\n1" node "1/bar@004" "bar@004\n1" node "1/bar@005" "bar@005\n1" node "1/foo@001" "foo@001\n1" node "1/foo@002" "foo@002\n1" node "1/foo@003" "foo@003\n1" node "1/foo@004" "foo@004\n1" node "1/foo@005" "foo@005\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/03.graph.ref0000664000175000017500000000027515202510242025073 0ustar alastairalastairedge "1/foo_a-t" "1/bar_a-t" edge "1/foo_c-g" "1/bar_c-g" graph node "1/bar_a-t" "bar_a-t\n1" node "1/bar_c-g" "bar_c-g\n1" node "1/foo_a-t" "foo_a-t\n1" node "1/foo_c-g" "foo_c-g\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/15.graph.ref0000664000175000017500000000021515202510242025070 0ustar alastairalastairgraph node "1/X_earthY" "X_earthY\n1" node "1/X_marsY" "X_marsY\n1" node "1/X_mercuryY" "X_mercuryY\n1" node "1/X_venusY" "X_venusY\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/01-basic/14.graph.ref0000664000175000017500000000101715202510242025070 0ustar alastairalastairedge "1/earth" "1/bar" edge "1/foo" "1/earth" edge "1/foo" "1/i0" edge "1/foo" "1/i1" edge "1/foo" "1/i2" edge "1/foo" "1/mars" edge "1/foo" "1/mercury" edge "1/foo" "1/venus" edge "1/i0" "1/bar" edge "1/i1" "1/bar" edge "1/i2" "1/bar" edge "1/mars" "1/bar" edge "1/mercury" "1/bar" edge "1/venus" "1/bar" graph node "1/bar" "bar\n1" node "1/earth" "earth\n1" node "1/foo" "foo\n1" node "1/i0" "i0\n1" node "1/i1" "i1\n1" node "1/i2" "i2\n1" node "1/mars" "mars\n1" node "1/mercury" "mercury\n1" node "1/venus" "venus\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/0000775000175000017500000000000015202510242023532 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t2-0101-this.ref0000664000175000017500000000002615202510242026077 0ustar alastairalastair101 stuff this closed cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t1-0099-that.ref0000664000175000017500000000002315202510242026104 0ustar alastairalastair99 stuff that open cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t1-0099-this.ref0000664000175000017500000000002315202510242026113 0ustar alastairalastair99 stuff this open cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t2-0099-that.ref0000664000175000017500000000002515202510242026107 0ustar alastairalastair99 stuff that closed cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t2-0101-that.ref0000664000175000017500000000002615202510242026070 0ustar alastairalastair101 stuff that closed cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t1-0101-this.ref0000664000175000017500000000002415202510242026074 0ustar alastairalastair101 stuff this open cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t1-0101-that.ref0000664000175000017500000000002415202510242026065 0ustar alastairalastair101 stuff that open cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/t2-0099-this.ref0000664000175000017500000000002515202510242026116 0ustar alastairalastair99 stuff this closed cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/reference.log0000664000175000017500000000066615202510242026203 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1_num099_this -triggered off [] 1/t2_num099_this -triggered off ['1/t1_num099_this'] 1/t1_num099_that -triggered off [] 1/t2_num099_that -triggered off ['1/t1_num099_that'] 1/t1_num101_this -triggered off [] 1/t2_num101_this -triggered off ['1/t1_num101_this'] 1/t1_num101_that -triggered off [] 1/t2_num101_that -triggered off ['1/t1_num101_that'] 1/x_that -triggered off [] 1/x_this -triggered off [] cylc-flow-8.6.4/tests/functional/param_expand/03-env-tmpl/flow.cylc0000664000175000017500000000231415202510242025355 0ustar alastairalastair[task parameters] num = 99..101..2 stuff = this, that state = open, closed [scheduling] [[graph]] R1 = """ t1 => t2 x """ [runtime] [[T]] [[[environment]]] MYNUM = %(num)d MYSTUFF = stuff %(stuff)s MY_FILE = %(num)04d-%(stuff)s [[U]] [[[environment]]] STATUS = %(state)s [[t1]] inherit = T, U script = """ FILE="${CYLC_WORKFLOW_RUN_DIR}/t1-${MY_FILE}" echo "${MYNUM} ${MYSTUFF} ${STATUS}" >"${FILE}" diff ${FILE} ${FILE}.ref """ [[t2]] inherit = T, U script = """ FILE="${CYLC_WORKFLOW_RUN_DIR}/t2-${MY_FILE}" echo "${MYNUM} ${MYSTUFF} ${STATUS}" >"${FILE}" diff ${FILE} ${FILE}.ref """ # The following tests the example of GH #4248. We had wrongly assumed that # general comes before specific, for parameters in inherited environments. [[x]] inherit = U script = test $STATUS == closed [[x]] pre-script = "echo pre" cylc-flow-8.6.4/tests/functional/param_expand/02-param_val.t0000664000175000017500000001632615202510242024126 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Check tasks and graph generated by parameter expansion. . "$(dirname "$0")/test_header" set_test_number 23 #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo, boo]] script = true [[FAM]] [[bar]] inherit = "FAM" [[bar]] inherit = "FAM" # implicit values here (takes i=cat,j=3) __WORKFLOW__ TNAME=${TEST_NAME_BASE}-1 # validate run_ok "${TNAME}" cylc validate . # family graph graph_workflow . "${TNAME}-graph-fam" --group="" cmp_ok "${TNAME}-graph-fam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-fam-1.ref" # task graph graph_workflow . "${TNAME}-graph-exp" cmp_ok "${TNAME}-graph-exp" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-exp-1.ref" # inheritance graph graph_workflow . "${TNAME}-graph-nam" -n cmp_ok "${TNAME}-graph-nam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-nam-1.ref" #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo, boo]] script = true [[FAM]] [[bar]] inherit = "FAM" [[bar]] inherit = "FAM" # same with explicit values __WORKFLOW__ TNAME=${TEST_NAME_BASE}-2 # validate run_ok "${TNAME}" cylc validate . # family graph graph_workflow . "${TNAME}-graph-fam" "--group=" cmp_ok "${TNAME}-graph-fam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-fam-1.ref" # task graph graph_workflow . "${TNAME}-graph-exp" cmp_ok "${TNAME}-graph-exp" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-exp-1.ref" # inheritance graph graph_workflow . "${TNAME}-graph-nam" -n cmp_ok "${TNAME}-graph-nam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-nam-1.ref" #------------------------------------------------------------------------------ # Same, with white space in the parameter syntax. cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar< i ,j > bar< i = cat , j = 3 > => boo""" [runtime] [[foo, boo]] script = true [[FAM]] [[bar]] inherit = "FAM< i, j>" [[bar]] inherit = "FAM" __WORKFLOW__ TNAME=${TEST_NAME_BASE}-3 # validate run_ok "${TNAME}" cylc validate . # family graph graph_workflow . "${TNAME}-graph-fam" "--group=" cmp_ok "${TNAME}-graph-fam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-fam-1.ref" # task graph graph_workflow . "${TNAME}-graph-exp" cmp_ok "${TNAME}-graph-exp" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-exp-1.ref" # inheritance graph graph_workflow . "${TNAME}-graph-nam" -n cmp_ok "${TNAME}-graph-nam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-nam-1.ref" #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo, boo]] script = true [[FAM]] [[bar]] inherit = "FAM" [[bar]] inherit = "FAM" # different explicit values are legal __WORKFLOW__ TNAME=${TEST_NAME_BASE}-4 # validate run_ok "${TNAME}" cylc validate . # family graph graph_workflow . "${TNAME}-graph-fam" "--group=" cmp_ok "${TNAME}-graph-fam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-fam-b.ref" # task graph graph_workflow . "${TNAME}-graph-exp" cmp_ok "${TNAME}-graph-exp" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-exp-b.ref" # inheritance graph graph_workflow . "${TNAME}-graph-nam" -n cmp_ok "${TNAME}-graph-nam" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph-nam-b.ref" #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo]] script = true [[FAM]] [[bar]] inherit = "FAM" [[bar]] inherit = "FAM" [[boo]] inherit = "FAM" # OK (plain task can inherit from specific params) __WORKFLOW__ run_ok "${TEST_NAME_BASE}-5" cylc validate . #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar_baz""" [runtime] [[foo]] script = true [[BAR]] [[BAZ]] [[bar_baz]] # OK params separate inherit = BAR, BAZ __WORKFLOW__ run_ok "${TEST_NAME_BASE}-6" cylc validate . cylc graph --reference . 1>'06.graph' cmp_ok '06.graph' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/06.graph.ref" #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo, boo]] script = true [[FAM]] [[bar]] inherit = "FAM" # ERROR: no frog [[bar]] inherit = "FAM" __WORKFLOW__ TNAME=${TEST_NAME_BASE}-err-1 run_fail "${TNAME}" cylc validate . cmp_ok "${TNAME}.stderr" - << __ERR__ ParamExpandError: illegal value 'i=frog' in 'inherit = FAM' __ERR__ #------------------------------------------------------------------------------ cat >'flow.cylc' <<'__WORKFLOW__' [task parameters] i = cat, dog, fish j = 1..5 [scheduling] [[graph]] R1 = """foo => bar bar => boo""" [runtime] [[foo]] script = true [[FAM]] [[bar]] inherit = "FAM" [[bar]] inherit = "FAM" [[boo]] inherit = "FAM" # ERROR: i undefined here. __WORKFLOW__ TNAME="${TEST_NAME_BASE}-err-2" run_fail "${TNAME}" cylc validate . cmp_ok "${TNAME}.stderr" - << __ERR__ ParamExpandError: parameter 'i' undefined in 'inherit = FAM' __ERR__ cylc-flow-8.6.4/tests/functional/param_expand/test_header0000777000175000017500000000000015202510242030065 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/param_expand/02-param_val/0000775000175000017500000000000015202510242023731 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-fam-1.ref0000664000175000017500000000204215202510242026425 0ustar alastairalastairedge "1/FAM_cat_j3" "1/boo" edge "1/foo" "1/FAM_cat_j1" edge "1/foo" "1/FAM_cat_j2" edge "1/foo" "1/FAM_cat_j3" edge "1/foo" "1/FAM_cat_j4" edge "1/foo" "1/FAM_cat_j5" edge "1/foo" "1/FAM_dog_j1" edge "1/foo" "1/FAM_dog_j2" edge "1/foo" "1/FAM_dog_j3" edge "1/foo" "1/FAM_dog_j4" edge "1/foo" "1/FAM_dog_j5" edge "1/foo" "1/FAM_fish_j1" edge "1/foo" "1/FAM_fish_j2" edge "1/foo" "1/FAM_fish_j3" edge "1/foo" "1/FAM_fish_j4" edge "1/foo" "1/FAM_fish_j5" graph node "1/FAM_cat_j1" "FAM_cat_j1\n1" node "1/FAM_cat_j2" "FAM_cat_j2\n1" node "1/FAM_cat_j3" "FAM_cat_j3\n1" node "1/FAM_cat_j4" "FAM_cat_j4\n1" node "1/FAM_cat_j5" "FAM_cat_j5\n1" node "1/FAM_dog_j1" "FAM_dog_j1\n1" node "1/FAM_dog_j2" "FAM_dog_j2\n1" node "1/FAM_dog_j3" "FAM_dog_j3\n1" node "1/FAM_dog_j4" "FAM_dog_j4\n1" node "1/FAM_dog_j5" "FAM_dog_j5\n1" node "1/FAM_fish_j1" "FAM_fish_j1\n1" node "1/FAM_fish_j2" "FAM_fish_j2\n1" node "1/FAM_fish_j3" "FAM_fish_j3\n1" node "1/FAM_fish_j4" "FAM_fish_j4\n1" node "1/FAM_fish_j5" "FAM_fish_j5\n1" node "1/boo" "boo\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-nam-b.ref0000664000175000017500000000356115202510242026525 0ustar alastairalastairedge "FAM_cat_j1" "bar_cat_j1" edge "FAM_cat_j2" "bar_cat_j2" edge "FAM_cat_j4" "bar_cat_j4" edge "FAM_cat_j5" "bar_cat_j5" edge "FAM_dog_j1" "bar_cat_j3" edge "FAM_dog_j1" "bar_dog_j1" edge "FAM_dog_j2" "bar_dog_j2" edge "FAM_dog_j3" "bar_dog_j3" edge "FAM_dog_j4" "bar_dog_j4" edge "FAM_dog_j5" "bar_dog_j5" edge "FAM_fish_j1" "bar_fish_j1" edge "FAM_fish_j2" "bar_fish_j2" edge "FAM_fish_j3" "bar_fish_j3" edge "FAM_fish_j4" "bar_fish_j4" edge "FAM_fish_j5" "bar_fish_j5" edge "root" "FAM_cat_j1" edge "root" "FAM_cat_j2" edge "root" "FAM_cat_j3" edge "root" "FAM_cat_j4" edge "root" "FAM_cat_j5" edge "root" "FAM_dog_j1" edge "root" "FAM_dog_j2" edge "root" "FAM_dog_j3" edge "root" "FAM_dog_j4" edge "root" "FAM_dog_j5" edge "root" "FAM_fish_j1" edge "root" "FAM_fish_j2" edge "root" "FAM_fish_j3" edge "root" "FAM_fish_j4" edge "root" "FAM_fish_j5" edge "root" "boo" edge "root" "foo" graph node "FAM_cat_j1" "FAM_cat_j1" node "FAM_cat_j2" "FAM_cat_j2" node "FAM_cat_j3" "FAM_cat_j3" node "FAM_cat_j4" "FAM_cat_j4" node "FAM_cat_j5" "FAM_cat_j5" node "FAM_dog_j1" "FAM_dog_j1" node "FAM_dog_j2" "FAM_dog_j2" node "FAM_dog_j3" "FAM_dog_j3" node "FAM_dog_j4" "FAM_dog_j4" node "FAM_dog_j5" "FAM_dog_j5" node "FAM_fish_j1" "FAM_fish_j1" node "FAM_fish_j2" "FAM_fish_j2" node "FAM_fish_j3" "FAM_fish_j3" node "FAM_fish_j4" "FAM_fish_j4" node "FAM_fish_j5" "FAM_fish_j5" node "bar_cat_j1" "bar_cat_j1" node "bar_cat_j2" "bar_cat_j2" node "bar_cat_j3" "bar_cat_j3" node "bar_cat_j4" "bar_cat_j4" node "bar_cat_j5" "bar_cat_j5" node "bar_dog_j1" "bar_dog_j1" node "bar_dog_j2" "bar_dog_j2" node "bar_dog_j3" "bar_dog_j3" node "bar_dog_j4" "bar_dog_j4" node "bar_dog_j5" "bar_dog_j5" node "bar_fish_j1" "bar_fish_j1" node "bar_fish_j2" "bar_fish_j2" node "bar_fish_j3" "bar_fish_j3" node "bar_fish_j4" "bar_fish_j4" node "bar_fish_j5" "bar_fish_j5" node "boo" "boo" node "foo" "foo" node "root" "root" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-exp-b.ref0000664000175000017500000000204215202510242026537 0ustar alastairalastairedge "1/bar_cat_j3" "1/boo" edge "1/foo" "1/bar_cat_j1" edge "1/foo" "1/bar_cat_j2" edge "1/foo" "1/bar_cat_j3" edge "1/foo" "1/bar_cat_j4" edge "1/foo" "1/bar_cat_j5" edge "1/foo" "1/bar_dog_j1" edge "1/foo" "1/bar_dog_j2" edge "1/foo" "1/bar_dog_j3" edge "1/foo" "1/bar_dog_j4" edge "1/foo" "1/bar_dog_j5" edge "1/foo" "1/bar_fish_j1" edge "1/foo" "1/bar_fish_j2" edge "1/foo" "1/bar_fish_j3" edge "1/foo" "1/bar_fish_j4" edge "1/foo" "1/bar_fish_j5" graph node "1/bar_cat_j1" "bar_cat_j1\n1" node "1/bar_cat_j2" "bar_cat_j2\n1" node "1/bar_cat_j3" "bar_cat_j3\n1" node "1/bar_cat_j4" "bar_cat_j4\n1" node "1/bar_cat_j5" "bar_cat_j5\n1" node "1/bar_dog_j1" "bar_dog_j1\n1" node "1/bar_dog_j2" "bar_dog_j2\n1" node "1/bar_dog_j3" "bar_dog_j3\n1" node "1/bar_dog_j4" "bar_dog_j4\n1" node "1/bar_dog_j5" "bar_dog_j5\n1" node "1/bar_fish_j1" "bar_fish_j1\n1" node "1/bar_fish_j2" "bar_fish_j2\n1" node "1/bar_fish_j3" "bar_fish_j3\n1" node "1/bar_fish_j4" "bar_fish_j4\n1" node "1/bar_fish_j5" "bar_fish_j5\n1" node "1/boo" "boo\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/06.graph.ref0000664000175000017500000000224415202510242025756 0ustar alastairalastairedge "1/foo" "1/bar_cat_baz_j1" edge "1/foo" "1/bar_cat_baz_j2" edge "1/foo" "1/bar_cat_baz_j3" edge "1/foo" "1/bar_cat_baz_j4" edge "1/foo" "1/bar_cat_baz_j5" edge "1/foo" "1/bar_dog_baz_j1" edge "1/foo" "1/bar_dog_baz_j2" edge "1/foo" "1/bar_dog_baz_j3" edge "1/foo" "1/bar_dog_baz_j4" edge "1/foo" "1/bar_dog_baz_j5" edge "1/foo" "1/bar_fish_baz_j1" edge "1/foo" "1/bar_fish_baz_j2" edge "1/foo" "1/bar_fish_baz_j3" edge "1/foo" "1/bar_fish_baz_j4" edge "1/foo" "1/bar_fish_baz_j5" graph node "1/bar_cat_baz_j1" "bar_cat_baz_j1\n1" node "1/bar_cat_baz_j2" "bar_cat_baz_j2\n1" node "1/bar_cat_baz_j3" "bar_cat_baz_j3\n1" node "1/bar_cat_baz_j4" "bar_cat_baz_j4\n1" node "1/bar_cat_baz_j5" "bar_cat_baz_j5\n1" node "1/bar_dog_baz_j1" "bar_dog_baz_j1\n1" node "1/bar_dog_baz_j2" "bar_dog_baz_j2\n1" node "1/bar_dog_baz_j3" "bar_dog_baz_j3\n1" node "1/bar_dog_baz_j4" "bar_dog_baz_j4\n1" node "1/bar_dog_baz_j5" "bar_dog_baz_j5\n1" node "1/bar_fish_baz_j1" "bar_fish_baz_j1\n1" node "1/bar_fish_baz_j2" "bar_fish_baz_j2\n1" node "1/bar_fish_baz_j3" "bar_fish_baz_j3\n1" node "1/bar_fish_baz_j4" "bar_fish_baz_j4\n1" node "1/bar_fish_baz_j5" "bar_fish_baz_j5\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-fam-b.ref0000664000175000017500000000174215202510242026514 0ustar alastairalastairedge "1/FAM_dog_j1" "1/boo" edge "1/foo" "1/FAM_cat_j1" edge "1/foo" "1/FAM_cat_j2" edge "1/foo" "1/FAM_cat_j4" edge "1/foo" "1/FAM_cat_j5" edge "1/foo" "1/FAM_dog_j1" edge "1/foo" "1/FAM_dog_j2" edge "1/foo" "1/FAM_dog_j3" edge "1/foo" "1/FAM_dog_j4" edge "1/foo" "1/FAM_dog_j5" edge "1/foo" "1/FAM_fish_j1" edge "1/foo" "1/FAM_fish_j2" edge "1/foo" "1/FAM_fish_j3" edge "1/foo" "1/FAM_fish_j4" edge "1/foo" "1/FAM_fish_j5" graph node "1/FAM_cat_j1" "FAM_cat_j1\n1" node "1/FAM_cat_j2" "FAM_cat_j2\n1" node "1/FAM_cat_j4" "FAM_cat_j4\n1" node "1/FAM_cat_j5" "FAM_cat_j5\n1" node "1/FAM_dog_j1" "FAM_dog_j1\n1" node "1/FAM_dog_j2" "FAM_dog_j2\n1" node "1/FAM_dog_j3" "FAM_dog_j3\n1" node "1/FAM_dog_j4" "FAM_dog_j4\n1" node "1/FAM_dog_j5" "FAM_dog_j5\n1" node "1/FAM_fish_j1" "FAM_fish_j1\n1" node "1/FAM_fish_j2" "FAM_fish_j2\n1" node "1/FAM_fish_j3" "FAM_fish_j3\n1" node "1/FAM_fish_j4" "FAM_fish_j4\n1" node "1/FAM_fish_j5" "FAM_fish_j5\n1" node "1/boo" "boo\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-nam-1.ref0000664000175000017500000000356115202510242026444 0ustar alastairalastairedge "FAM_cat_j1" "bar_cat_j1" edge "FAM_cat_j2" "bar_cat_j2" edge "FAM_cat_j3" "bar_cat_j3" edge "FAM_cat_j4" "bar_cat_j4" edge "FAM_cat_j5" "bar_cat_j5" edge "FAM_dog_j1" "bar_dog_j1" edge "FAM_dog_j2" "bar_dog_j2" edge "FAM_dog_j3" "bar_dog_j3" edge "FAM_dog_j4" "bar_dog_j4" edge "FAM_dog_j5" "bar_dog_j5" edge "FAM_fish_j1" "bar_fish_j1" edge "FAM_fish_j2" "bar_fish_j2" edge "FAM_fish_j3" "bar_fish_j3" edge "FAM_fish_j4" "bar_fish_j4" edge "FAM_fish_j5" "bar_fish_j5" edge "root" "FAM_cat_j1" edge "root" "FAM_cat_j2" edge "root" "FAM_cat_j3" edge "root" "FAM_cat_j4" edge "root" "FAM_cat_j5" edge "root" "FAM_dog_j1" edge "root" "FAM_dog_j2" edge "root" "FAM_dog_j3" edge "root" "FAM_dog_j4" edge "root" "FAM_dog_j5" edge "root" "FAM_fish_j1" edge "root" "FAM_fish_j2" edge "root" "FAM_fish_j3" edge "root" "FAM_fish_j4" edge "root" "FAM_fish_j5" edge "root" "boo" edge "root" "foo" graph node "FAM_cat_j1" "FAM_cat_j1" node "FAM_cat_j2" "FAM_cat_j2" node "FAM_cat_j3" "FAM_cat_j3" node "FAM_cat_j4" "FAM_cat_j4" node "FAM_cat_j5" "FAM_cat_j5" node "FAM_dog_j1" "FAM_dog_j1" node "FAM_dog_j2" "FAM_dog_j2" node "FAM_dog_j3" "FAM_dog_j3" node "FAM_dog_j4" "FAM_dog_j4" node "FAM_dog_j5" "FAM_dog_j5" node "FAM_fish_j1" "FAM_fish_j1" node "FAM_fish_j2" "FAM_fish_j2" node "FAM_fish_j3" "FAM_fish_j3" node "FAM_fish_j4" "FAM_fish_j4" node "FAM_fish_j5" "FAM_fish_j5" node "bar_cat_j1" "bar_cat_j1" node "bar_cat_j2" "bar_cat_j2" node "bar_cat_j3" "bar_cat_j3" node "bar_cat_j4" "bar_cat_j4" node "bar_cat_j5" "bar_cat_j5" node "bar_dog_j1" "bar_dog_j1" node "bar_dog_j2" "bar_dog_j2" node "bar_dog_j3" "bar_dog_j3" node "bar_dog_j4" "bar_dog_j4" node "bar_dog_j5" "bar_dog_j5" node "bar_fish_j1" "bar_fish_j1" node "bar_fish_j2" "bar_fish_j2" node "bar_fish_j3" "bar_fish_j3" node "bar_fish_j4" "bar_fish_j4" node "bar_fish_j5" "bar_fish_j5" node "boo" "boo" node "foo" "foo" node "root" "root" stop cylc-flow-8.6.4/tests/functional/param_expand/02-param_val/graph-exp-1.ref0000664000175000017500000000204215202510242026456 0ustar alastairalastairedge "1/bar_cat_j3" "1/boo" edge "1/foo" "1/bar_cat_j1" edge "1/foo" "1/bar_cat_j2" edge "1/foo" "1/bar_cat_j3" edge "1/foo" "1/bar_cat_j4" edge "1/foo" "1/bar_cat_j5" edge "1/foo" "1/bar_dog_j1" edge "1/foo" "1/bar_dog_j2" edge "1/foo" "1/bar_dog_j3" edge "1/foo" "1/bar_dog_j4" edge "1/foo" "1/bar_dog_j5" edge "1/foo" "1/bar_fish_j1" edge "1/foo" "1/bar_fish_j2" edge "1/foo" "1/bar_fish_j3" edge "1/foo" "1/bar_fish_j4" edge "1/foo" "1/bar_fish_j5" graph node "1/bar_cat_j1" "bar_cat_j1\n1" node "1/bar_cat_j2" "bar_cat_j2\n1" node "1/bar_cat_j3" "bar_cat_j3\n1" node "1/bar_cat_j4" "bar_cat_j4\n1" node "1/bar_cat_j5" "bar_cat_j5\n1" node "1/bar_dog_j1" "bar_dog_j1\n1" node "1/bar_dog_j2" "bar_dog_j2\n1" node "1/bar_dog_j3" "bar_dog_j3\n1" node "1/bar_dog_j4" "bar_dog_j4\n1" node "1/bar_dog_j5" "bar_dog_j5\n1" node "1/bar_fish_j1" "bar_fish_j1\n1" node "1/bar_fish_j2" "bar_fish_j2\n1" node "1/bar_fish_j3" "bar_fish_j3\n1" node "1/bar_fish_j4" "bar_fish_j4\n1" node "1/bar_fish_j5" "bar_fish_j5\n1" node "1/boo" "boo\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/validate/0000775000175000017500000000000015202510242020702 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/58-icp-quoted-now.t0000775000175000017500000000216715202510242024205 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Quoted "now" for initial cycle point was failing. . "$(dirname "$0")/test_header" set_test_number 1 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] cycle point format = %Y%m%d [scheduling] initial cycle point = "now" [[graph]] P1D = t1 [runtime] [[t1]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/75-CYLC_TEMPLATE_VARS.t0000664000175000017500000000236115202510242024202 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test CYLC_TEMPLATE_VARS exported. . "$(dirname "$0")/test_header" set_test_number 2 cat > 'flow.cylc' <<__HEREDOC__ #!jinja2 [scheduling] initial cycle point = 2020 [[graph]] R1 = foo [runtime] [[foo]] __HEREDOC__ run_ok "${TEST_NAME_BASE}-validate" cylc validate . --debug grep_ok "CYLC_TEMPLATE_VARS={'CYLC_VERSION': '.*', 'CYLC_TEMPLATE_VARS': {...}" \ "${TEST_NAME_BASE}-validate.stderr" cylc-flow-8.6.4/tests/functional/validate/27-fail-constrained-final.t0000775000175000017500000000263315202510242025635 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating simple multi-inheritance workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/50-hyphen-fam.t0000775000175000017500000000272315202510242023354 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of task name with a XXX-FAM pattern. # See issue cylc/cylc-flow#1778 where validation of the following valid workflow failed. . "$(dirname "$0")/test_header" set_test_number 2 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = "baz-foo => bar" [runtime] [[foo]] [[bar, baz-foo]] inherit = foo __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = "foo-baz => bar" [runtime] [[foo]] [[bar, foo-baz]] inherit = foo __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/56-succeed-sub.t0000775000175000017500000000231215202510242023522 0ustar alastairalastair#!/usr/bin/env bash # 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 . # In graph lines where we have multiple triggers of the same task, ensure # :succeed does not get substituted to symbols that already have a trigger. . "$(dirname "$0")/test_header" set_test_number 1 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo:fail? | (foo? & bar:fail) => something [runtime] [[root]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/22-fail-year-bounds/0000775000175000017500000000000015202510242024264 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/22-fail-year-bounds/flow.cylc0000664000175000017500000000020315202510242026102 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = +10000-01-01T00 [[graph]] T00 = foo cylc-flow-8.6.4/tests/functional/validate/66-fail-consec-spaces.t0000775000175000017500000000255115202510242024765 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test consecutive spaces in a __MANY__ name fails validation (GitHub #2417). . "$(dirname "$0")/test_header" set_test_number 2 TEST_NAME="${TEST_NAME_BASE}-val" cat > flow.cylc <<__END__ [scheduling] [[graph]] R1 = task1 [runtime] [[HPC]] [[[directives]]] -l select=1:ncpus=1:mem=5GB [[task1]] inherit = HPC [[[directives]]] -l select=1:ncpus=24:mem=20GB # ERROR! __END__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<__END__ IllegalItemError: [runtime][task1][directives]-l select - (consecutive spaces) __END__ cylc-flow-8.6.4/tests/functional/validate/24-fail-initial-greater-final/0000775000175000017500000000000015202510242026205 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/24-fail-initial-greater-final/flow.cylc0000664000175000017500000000066315202510242030035 0ustar alastairalastair[meta] title = "Test workflow for initial cycle point less than final." description = """Task are not important here but the cycl points are. Initial is set to greater than the final and the workflow should report the error.""" [scheduler] UTC mode = True [scheduling] initial cycle point = 20141208T0000Z final cycle point = 20141207T0000Z [[graph]] T00 = A => B [runtime] [[A]] [[B]] cylc-flow-8.6.4/tests/functional/validate/22-fail-year-bounds.t0000775000175000017500000000302315202510242024452 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and an async graph. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "incompatible with \[cylc\]cycle point num expanded year digits = 0" \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/9999_rollover/0000775000175000017500000000000015202510242023251 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/9999_rollover/flow.cylc0000664000175000017500000000037515202510242025101 0ustar alastairalastair# A workflow that tries to run beyond year 9999 without using extended year digits. [scheduler] UTC mode = True [scheduling] initial cycle point = 99991231T2200 [[graph]] R3//PT1H = "foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/validate/21-fail-no-graph-2.t0000775000175000017500000000320115202510242024071 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation fails if no graph is defined. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-empty-graph cat > flow.cylc <<__END__ [scheduling] [[graph]] R1 = "" __END__ run_fail "${TEST_NAME}" cylc validate -v . grep_ok "No workflow dependency graph defined." "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-no-graph cat > flow.cylc <<__END__ [scheduling] initial cycle point = 2015 [[graph]] __END__ run_fail "${TEST_NAME}" cylc validate -v . grep_ok "No workflow dependency graph defined." "${TEST_NAME}.stderr" cylc-flow-8.6.4/tests/functional/validate/18-fail-no-scheduling/0000775000175000017500000000000015202510242024600 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/18-fail-no-scheduling/flow.cylc0000664000175000017500000000000015202510242026411 0ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/19-fail-no-dependencies.t0000775000175000017500000000276115202510242025300 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and an async graph. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "No workflow dependency graph defined\." "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/16-fail-old-syntax-5.t0000775000175000017500000000301415202510242024471 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and start-up tasks. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stderr" <<__END__ IllegalItemError: [scheduling][special tasks]start-up __END__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/40-jinja2-template-syntax-error-main.t0000775000175000017500000000323415202510242027700 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation for a bad Jinja2 TemplateSyntaxError workflow. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate . cmp_ok_re "${TEST_NAME}.stderr" <<'__ERROR__' Jinja2Error: Encountered unknown tag 'end'. Jinja was looking for the following tags: 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'. File.* \[\[graph\]\] {% if true %} R1 = foo {% end if % <-- TemplateSyntaxError __ERROR__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/67-relative-icp.t0000775000175000017500000000251515202510242023713 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Checking that syntax of relative initial cycle point is validated. # Note: remember to update this test after 01/01/2117 . "$(dirname "$0")/test_header" set_test_number 3 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] UTC mode = true allow implicit tasks = True [scheduling] initial cycle point = previous(-17T1200Z; -18T1200Z) - P1D [[graph]] P1D = t1 __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-val" cylc validate . TEST_NAME="${TEST_NAME_BASE}-graph" run_ok "$TEST_NAME" cylc graph --reference . grep_ok "20171231T1200Z/t1" "${TEST_NAME}.stdout" exit cylc-flow-8.6.4/tests/functional/validate/23-fail-old-syntax-7.t0000775000175000017500000000302315202510242024471 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a prev-style cycle syntax and post-style retry syntax . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stderr" <<__END__ IllegalItemError: [scheduling]initial cycle time __END__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/57-offset-no-offset.t0000775000175000017500000000237515202510242024516 0ustar alastairalastair#!/usr/bin/env bash # 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 . # GitHub PR #2002 - validation of "foo | foo[-P1D] => bar" was failing because # the explicit ':succeed' trigger was being substituted before the offset # instead of after, creating an invalid trigger expression. . "$(dirname "$0")/test_header" set_test_number 1 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 2010 [[graph]] P1D = foo | foo[-P1D] => bar [runtime] [[foo, bar]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/24-fail-initial-greater-final.t0000775000175000017500000000305115202510242026374 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation fails for initial cycle point greater than the final. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "The initial cycle point:20141208T0000Z is after the final cycle \ point:20141207T0000Z." "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/02-scripting-quotes.t0000775000175000017500000000263215202510242024634 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that validating: script = "foo"bar"baz" fails . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/71-task-proxy-sequence-bounds-err.t0000775000175000017500000000346515202510242027334 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test for handling task proxy sequence bounds error. #2735 . "$(dirname "$0")/test_header" set_test_number 4 cat > flow.cylc <<__END__ [scheduler] UTC mode = True [scheduling] initial cycle point = 2000 [[graph]] R1//1999 = t1 [runtime] [[t1]] script = true __END__ TEST_NAME="${TEST_NAME_BASE}-single" run_ok "$TEST_NAME" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WARNING - R1/P0Y/19990101T0000Z: sequence out of bounds for initial cycle point 20000101T0000Z __ERR__ cat > flow.cylc <<__END__ [scheduler] UTC mode = True [scheduling] initial cycle point = 2000 [[graph]] R1//1996, R1//1997, R1//1998, R1//1999 = t1 [runtime] [[t1]] script = true __END__ TEST_NAME="${TEST_NAME_BASE}-multiple" run_ok "$TEST_NAME" cylc validate . contains_ok "${TEST_NAME}.stderr" <<__ERR__ WARNING - multiple sequences out of bounds for initial cycle point 20000101T0000Z: ${LOG_INDENT}R1/P0Y/19960101T0000Z, R1/P0Y/19970101T0000Z, R1/P0Y/19980101T0000Z, ${LOG_INDENT}R1/P0Y/19990101T0000Z __ERR__ exit cylc-flow-8.6.4/tests/functional/validate/65-bad-task-event-handler-tmpl.t0000775000175000017500000000346115202510242026520 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation fails on bad task event handler templates. . "$(dirname "$0")/test_header" set_test_number 4 TEST_NAME="${TEST_NAME_BASE}-bad-key" cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=true [[[events]]] failed handlers = echo %(id)s, echo %(rubbish)s __FLOW_CONFIG__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: bad task event handler template t1: echo %(rubbish)s: KeyError('rubbish') __ERR__ TEST_NAME="${TEST_NAME_BASE}-bad-value" cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=true [[[events]]] failed handlers = echo %(ids __FLOW_CONFIG__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: bad task event handler template t1: echo %(ids: ValueError('incomplete format key') __ERR__ exit cylc-flow-8.6.4/tests/functional/validate/38-degenerate-point-format.t0000775000175000017500000000313215202510242026041 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation fails when the cycle point format is less precise than a # graph recurrence interval . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "SequenceDegenerateError: R/2015-08/P1D, point format %Y-%m: equal adjacent points: 2015-08 => 2015-08." \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/02-scripting-quotes/0000775000175000017500000000000015202510242024441 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/02-scripting-quotes/flow.cylc0000664000175000017500000000014315202510242026262 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = "foo"bar"baz" cylc-flow-8.6.4/tests/functional/validate/74-templatevar-types.t0000664000175000017500000000303415202510242025005 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating xtrigger names in workflow. . "$(dirname "$0")/test_header" set_test_number 5 TEST_NAME="${TEST_NAME_BASE}-val" # test a valid xtrigger cat >'flow.cylc' <<'__FLOW_CONFIG__' #!Jinja2 [scheduler] allow implicit tasks = True [scheduling] initial cycle point = {{ ICP - 1 }} cycling mode = integer [[graph]] R1 = foo __FLOW_CONFIG__ run_fail "${TEST_NAME}-valid" cylc validate . run_fail "${TEST_NAME}-valid" cylc validate . -s 'ICP="2000"' run_ok "${TEST_NAME}-valid" cylc validate . -s 'ICP=2000' cat >'template' <<'__TEMPLATE__' ICP=2000 __TEMPLATE__ run_fail "${TEST_NAME}-valid" cylc validate . run_ok "${TEST_NAME}-valid" cylc validate . --set-file=template exit cylc-flow-8.6.4/tests/functional/validate/25-fail-constrained-initial.t0000775000175000017500000000263315202510242026173 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating simple multi-inheritance workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/13-fail-old-syntax-2.t0000775000175000017500000000277015202510242024473 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and a prev-style offset. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok 'Illegal graph node: foo\[T\-24\]:succeed' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/07-null-parentage.t0000775000175000017500000000310515202510242024233 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of workflow for tasks with inherit = None . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] inherit = None __FLOW__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" grep_ok 'WorkflowConfigError: null parentage for foo' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/70-no-clock-int-cycle.t0000664000175000017500000000251115202510242024704 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test that clock xtriggers are not allowed with integer cycling. . "$(dirname "$0")/test_header" set_test_number 2 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 [[xtriggers]] c1 = wall_clock(offset=P0Y) [[graph]] R/^/P1 = "@c1 & foo[-P1] => foo" __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-val" cylc validate '.' contains_ok "${TEST_NAME_BASE}-val.stderr" <<'__END__' WorkflowConfigError: Clock xtriggers require datetime cycling: c1 = wall_clock(offset=P0Y) __END__ cylc-flow-8.6.4/tests/functional/validate/61-include-missing-quote.t0000775000175000017500000000215515202510242025546 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Error message for missing quote in include statement . "$(dirname "$0")/test_header" set_test_number 2 cat >'flow.cylc' <<'__FLOW_CONFIG__' %include 'foo.cylc __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}" cylc validate . cmp_ok "${TEST_NAME_BASE}.stderr" <<__ERR__ FileParseError: mismatched quotes (in $PWD/flow.cylc): %include 'foo.cylc __ERR__ exit cylc-flow-8.6.4/tests/functional/validate/55-hyphen-finish.t0000775000175000017500000000212315202510242024070 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test hyphen in task name + ":finish". See cylc/cylc-flow#1949. . "$(dirname "$0")/test_header" set_test_number 1 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo-bar:finish => baz [runtime] [[foo-bar,baz]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/37-special-implicit-task.t0000775000175000017500000000265515202510242025521 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation fails for special implicit tasks . "$(dirname "$0")/test_header" set_test_number 2 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 20200101 [[special tasks]] clock-trigger = foo(PT0M) [[graph]] T00 = bar [runtime] [[bar]] script = true __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}" cylc validate "${PWD}" cmp_ok "${TEST_NAME_BASE}.stderr" << '__ERR__' WorkflowConfigError: implicit tasks detected (no entry under [runtime]): * foo To allow implicit tasks, use 'flow.cylc[scheduler]allow implicit tasks' __ERR__ exit cylc-flow-8.6.4/tests/functional/validate/73-xtrigger-names.t0000664000175000017500000000312615202510242024254 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating xtrigger names in workflow. . "$(dirname "$0")/test_header" set_test_number 3 TEST_NAME="${TEST_NAME_BASE}-val" # test a valid xtrigger cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 2000 [[xtriggers]] foo = wall_clock():PT1S [[graph]] R1 = @foo => bar [runtime] [[bar]] __FLOW_CONFIG__ run_ok "${TEST_NAME}-valid" cylc validate . # test an invalid xtrigger cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 2000 [[xtriggers]] foo-1 = wall_clock():PT1S [[graph]] R1 = @foo-1 => bar [runtime] [[bar]] __FLOW_CONFIG__ run_fail "${TEST_NAME}-invalid" cylc validate . grep_ok 'Invalid xtrigger name' "${TEST_NAME}-invalid.stderr" exit cylc-flow-8.6.4/tests/functional/validate/18-fail-no-scheduling.t0000775000175000017500000000275115202510242024775 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and an async graph. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "missing \[scheduling\] section" "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/45-jinja2-type-error.t0000775000175000017500000000337715202510242024615 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation for some StandardError while doing Jinja2 processing. . "$(dirname "$0")/test_header" set_test_number 4 TEST_NAME="${TEST_NAME_BASE}-type-error" cat >'flow.cylc' <<'__FLOW_CONFIG__' #!jinja2 [scheduling] [[graph]] R1 = foo {{ 1 / 'foo' }} __FLOW_CONFIG__ run_fail "${TEST_NAME}" cylc validate . cmp_ok_re "${TEST_NAME}.stderr" <<'__ERROR__' Jinja2Error: unsupported operand type\(s\) .* 'int' and 'str' File.* \[scheduling\] \[\[graph\]\] R1 = foo {{ 1 / 'foo' }} <-- TypeError __ERROR__ TEST_NAME="${TEST_NAME}-value-error" cat >'flow.cylc' <<'__FLOW_CONFIG__' #!Jinja2 {% set foo = [1, 2] %} {% set a, b, c = foo %} __FLOW_CONFIG__ run_fail "${TEST_NAME}" cylc validate . cmp_ok_re "${TEST_NAME}.stderr" <<'__ERROR__' Jinja2Error: not enough values to unpack \(expected 3, got 2\) File.* #!Jinja2 {% set foo = \[1, 2\] %} {% set a, b, c = foo %} <-- ValueError __ERROR__ exit cylc-flow-8.6.4/tests/functional/validate/20-fail-no-graph/0000775000175000017500000000000015202510242023545 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/20-fail-no-graph/flow.cylc0000664000175000017500000000003315202510242025364 0ustar alastairalastair[scheduling] [[graph]] cylc-flow-8.6.4/tests/functional/validate/23-fail-old-syntax-7/0000775000175000017500000000000015202510242024303 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/23-fail-old-syntax-7/flow.cylc0000664000175000017500000000042215202510242026124 0ustar alastairalastair[scheduler] [[events]] timeout = 4320 [scheduling] initial cycle time = 2014030300 final cycle time = 2014030306 [[graph]] 0,6,12,18 = A [runtime] [[A]] script = "sleep 10" execution retry delays = 2*PT30M, PT60M cylc-flow-8.6.4/tests/functional/validate/49-jinja2-undefined-error.t0000775000175000017500000000306015202510242025566 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation for a Jinja2 type error, with no line number info. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate . cmp_ok_re "${TEST_NAME}.stderr" <<'__ERROR__' Jinja2Error: 'UNDEFINED_WHATEVER' is undefined File.* \[scheduling\] \[\[graph\]\] R1 = foo \[\[\[{{ UNDEFINED_WHATEVER }}\]\]\] <-- UndefinedError __ERROR__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/25-fail-constrained-initial/0000775000175000017500000000000015202510242025777 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/25-fail-constrained-initial/flow.cylc0000664000175000017500000000043115202510242027620 0ustar alastairalastair[meta] title = "Test validation of initial cycle point against constraints" [scheduler] [scheduling] initial cycle point = 20100101T03 initial cycle point constraints = T00, T06, T12, T18 [[graph]] T00, T06, T12, T18 = foo [runtime] [[FOO]] [[BAR]] [[foo]] inherit = FOO, BAR cylc-flow-8.6.4/tests/functional/validate/16-fail-old-syntax-5/0000775000175000017500000000000015202510242024303 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/16-fail-old-syntax-5/flow.cylc0000664000175000017500000000027715202510242026134 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20100101T00 [[special tasks]] start-up = cold_foo [[graph]] T12 = "cold_foo => foo" cylc-flow-8.6.4/tests/functional/validate/52-null-timeout.t0000775000175000017500000000213015202510242023750 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test explicit unset timeout intervals validate (GitHub #1865). . "$(dirname "$0")/test_header" set_test_number 1 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo [runtime] [[foo]] [[[events]]] execution timeout = __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/38-degenerate-point-format/0000775000175000017500000000000015202510242025652 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/38-degenerate-point-format/flow.cylc0000664000175000017500000000023215202510242027472 0ustar alastairalastair[scheduler] cycle point format = %Y-%m allow implicit tasks = True [scheduling] initial cycle point = 2015-08 [[graph]] P1D = foo cylc-flow-8.6.4/tests/functional/validate/13-fail-old-syntax-2/0000775000175000017500000000000015202510242024275 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/13-fail-old-syntax-2/flow.cylc0000664000175000017500000000021615202510242026117 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20100101T00 [[graph]] T00 = "foo[T-24] => foo" cylc-flow-8.6.4/tests/functional/validate/43-jinja2-template-error-main.t0000775000175000017500000000267615202510242026370 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation for a filter Jinja2 error with no line number. . "$(dirname "$0")/test_header" set_test_number 2 cat >'flow.cylc' <<'__FLOW_CONFIG__' #!jinja2 {% set foo = {} %} [scheduling] [[graph]] R1 = {{ foo|dictsort(by='by') }} [runtime] [[foo]] script = sleep 1 __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}" cylc validate . cmp_ok_re "${TEST_NAME_BASE}.stderr" <<'__ERROR__' Jinja2Error: You can only sort by either "key" or "value" File.* {% set foo = {} %} \[scheduling\] \[\[graph\]\] R1 = {{ foo\|dictsort\(by='by'\) }} <-- FilterArgumentError __ERROR__ exit cylc-flow-8.6.4/tests/functional/validate/54-self-suicide.t0000775000175000017500000000401315202510242023672 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation, graph for workflow with self-suiciding task . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """ foo? => bar => baz foo:fail? => qux => baz foo:fail? & baz => !foo """ __FLOW__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-graph-check run_ok "${TEST_NAME}" cylc graph --reference "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" <<'__OUT__' edge "1/bar" "1/baz" edge "1/foo" "1/bar" edge "1/foo" "1/qux" edge "1/qux" "1/baz" graph node "1/bar" "bar\n1" node "1/baz" "baz\n1" node "1/foo" "foo\n1" node "1/qux" "qux\n1" stop __OUT__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/15-fail-old-syntax-4.t0000775000175000017500000000314115202510242024470 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a prev-style cycle point and a new-style cycling section . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "Incompatible value for : 2010010100: Invalid ISO 8601 date representation: 2010010100" \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/08-whitespace.t0000775000175000017500000000300315202510242023447 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a lot of whitespace added - trailing, around # section headings, around list item delimiters, in include-files, and # added by jinja2. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_ok "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/00-multi/0000775000175000017500000000000015202510242022251 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/00-multi/reference.log0000664000175000017500000000007015202510242024707 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/validate/00-multi/flow.cylc0000664000175000017500000000033715202510242024077 0ustar alastairalastair[meta] title = "Test validation of simple multiple inheritance" description = """Bug identified at 5.1.1-314-g4960684.""" [scheduling] [[graph]] R1 = """foo""" [runtime] [[FOO]] [[BAR]] [[foo]] inherit = FOO, BAR cylc-flow-8.6.4/tests/functional/validate/30-pass-constrained-final.t0000775000175000017500000000263115202510242025660 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating simple multi-inheritance workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/14-fail-old-syntax-3.t0000775000175000017500000000314215202510242024467 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and a prev-style limit. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stderr" <<__END__ IllegalValueError: (type=ISO 8601 interval) [runtime][root][events]execution timeout = 3 - (Invalid ISO 8601 duration representation: 3) __END__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/30-pass-constrained-final/0000775000175000017500000000000015202510242025466 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/30-pass-constrained-final/flow.cylc0000664000175000017500000000047315202510242027315 0ustar alastairalastair[meta] title = "Test validation of final cycle point against constraints" [scheduler] [scheduling] initial cycle point = 20100101T06 final cycle point = 20100101T18 final cycle point constraints = T00, T06, T12, T18 [[graph]] T00, T06, T12, T18 = """foo""" [runtime] [[FOO]] [[BAR]] [[foo]] inherit = FOO, BAR cylc-flow-8.6.4/tests/functional/validate/08-whitespace/0000775000175000017500000000000015202510242023263 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/08-whitespace/inc.cylc0000664000175000017500000000004715202510242024711 0ustar alastairalastair [ runtime ] # nothing ... cylc-flow-8.6.4/tests/functional/validate/08-whitespace/flow.cylc0000664000175000017500000000056515202510242025114 0ustar alastairalastair#!jinja2 {% set HELLO=False %} [ scheduling ] initial cycle point = 20140101T00 [[ special tasks ]] sequential = foo , bar , baz [[ graph ]] T00, T12 =""" a => b foo => bar & baz """ [ runtime ] [[baz ]] [[ foo,bar , a, b ]] %include "inc.cylc" cylc-flow-8.6.4/tests/functional/validate/68-trailing_whitespace.t0000664000175000017500000000335215202510242025352 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of a workflow with self-edges fails. . "$(dirname "$0")/test_header" set_test_number 3 # Test example with trailing whitespace cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20000101T06 final cycle point = 20010101T18 [[graph]] T00 = """ # NOTE: don't let editor strip trailing space on next line foo | bar \ => baz & qux pub """ T12 = """ qux baz """ __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-simple-fail" cylc validate . cmp_ok "${TEST_NAME_BASE}-simple-fail.stderr" <<'__ERR__' FileParseError: Syntax error line 9: Whitespace after the line continuation character (\). __ERR__ # Test example with correct syntax sed -i 's/\\ /\\/' 'flow.cylc' run_ok "${TEST_NAME_BASE}-simple-pass" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/01-periodical.t0000775000175000017500000000263315202510242023427 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating Daily, Monthly and Yearly type tasks. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/17-fail-old-syntax-6.t0000775000175000017500000000313215202510242024474 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with a new-style cycle point and an async graph. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 20100101T00 [[graph]] R1 = "cold_foo" 12 = "cold_foo => foo" __FLOW_CONFIG__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}" run_fail "${TEST_NAME}" cylc validate -v . grep_ok 'WorkflowConfigError: Cannot process recurrence 12' "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- exit cylc-flow-8.6.4/tests/functional/validate/test_header0000777000175000017500000000000015202510242027217 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/09-include-missing.t0000775000175000017500000000252515202510242024416 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation missing include-file. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 echo '%include foo.cylc' >flow.cylc echo '%include bar.cylc' >foo.cylc run_fail "${TEST_NAME_BASE}" cylc validate . cmp_ok "${TEST_NAME_BASE}.stderr" <<__ERR__ IncludeFileNotFoundError: bar.cylc via foo.cylc from $PWD/flow.cylc __ERR__ #------------------------------------------------------------------------------- rm flow.cylc rm foo.cylc cylc-flow-8.6.4/tests/functional/validate/27-fail-constrained-final/0000775000175000017500000000000015202510242025441 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/27-fail-constrained-final/flow.cylc0000664000175000017500000000047315202510242027270 0ustar alastairalastair[meta] title = "Test validation of final cycle point against constraints" [scheduler] [scheduling] initial cycle point = 20100101T03 final cycle point = 20100102T17 final cycle point constraints = T00, T06, T12, T18 [[graph]] T00, T06, T12, T18 = """foo""" [runtime] [[FOO]] [[BAR]] [[foo]] inherit = FOO, BAR cylc-flow-8.6.4/tests/functional/validate/06-implicit-missing.t0000775000175000017500000000301315202510242024573 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of workflow for tasks without runtime entries # (accidental implicit tasks) . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = "foo => bar" [runtime] [[foo]] __FLOW__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/76-section-section-transpose.t0000664000175000017500000000415215202510242026445 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test handling of mixed up sections vs settings . "$(dirname "$0")/test_header" set_test_number 6 # 1. section as setting (normal) TEST_NAME='section-as-setting-normal' cat > 'flow.cylc' <<__HEREDOC__ [runtime] [[foo]] environment = 42 __HEREDOC__ run_fail "${TEST_NAME}-validate" cylc validate . grep_ok \ 'IllegalItemError: \[runtime\]\[foo\]environment - ("environment" should be a \[section\] not a setting)' \ "${TEST_NAME}-validate.stderr" # 2. section as setting (via upgrader) # NOTE: if this test fails it is likely because the upgrader for "scheduling" # has been removed, convert this to use a new deprecated section TEST_NAME='section-as-setting-upgrader' cat > 'flow.cylc' <<__HEREDOC__ scheduling = 22 __HEREDOC__ run_fail "${TEST_NAME}-validate" cylc validate . grep_ok \ 'UpgradeError: \[scheduling\] ("scheduling" should be a \[section\] not a setting' \ "${TEST_NAME}-validate.stderr" # 3. setting as section TEST_NAME='setting-as-section' cat > 'flow.cylc' <<__HEREDOC__ [scheduling] [[initial cycle point]] __HEREDOC__ run_fail "${TEST_NAME}-validate" cylc validate . grep_ok \ 'IllegalItemError: \[scheduling\]initial cycle point - ("initial cycle point" should be a setting not a \[section\])' \ "${TEST_NAME}-validate.stderr" cylc-flow-8.6.4/tests/functional/validate/64-circular.t0000775000175000017500000000736315202510242023136 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of a workflow with self-edges fails. . "$(dirname "$0")/test_header" set_test_number 13 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = a => a __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-simple-1" cylc validate . contains_ok "${TEST_NAME_BASE}-simple-1.stderr" <<'__ERR__' WorkflowConfigError: self-edge detected: a:succeeded => a __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = a => b => c => d => a => z __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-simple-2" cylc validate . contains_ok "${TEST_NAME_BASE}-simple-2.stderr" <<'__ERR__' WorkflowConfigError: circular edges detected: 1/d => 1/a 1/a => 1/b 1/b => 1/c 1/c => 1/d __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = FAM:succeed-all => f & g => z [runtime] [[FAM]] [[f,g,h]] inherit = FAM __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-simple-fam" cylc validate . contains_ok "${TEST_NAME_BASE}-simple-fam.stderr" <<'__ERR__' WorkflowConfigError: self-edge detected: f:succeeded => f __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True cycle point format = %Y [scheduling] initial cycle point = 2001 final cycle point = 2010 [[graph]] P1Y = ''' a[-P1Y] => a a[+P1Y] => a ''' __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-intercycle-1" cylc validate . contains_ok "${TEST_NAME_BASE}-intercycle-1.stderr" <<'__ERR__' WorkflowConfigError: circular edges detected: 2002/a => 2001/a 2001/a => 2002/a 2003/a => 2002/a 2002/a => 2003/a __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] 2/P3 = foo => bar => baz 8/P1 = baz => foo __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-intercycle-2" cylc validate . contains_ok "${TEST_NAME_BASE}-intercycle-2.stderr" <<'__ERR__' WorkflowConfigError: circular edges detected: 8/foo => 8/bar 8/bar => 8/baz 8/baz => 8/foo __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [task parameters] foo = 1..5 [scheduling] [[graph]] R1 = """ fool => fool fool => fool """ __FLOW_CONFIG__ run_fail "${TEST_NAME_BASE}-param-1" cylc validate . contains_ok "${TEST_NAME_BASE}-param-1.stderr" <<'__ERR__' WorkflowConfigError: circular edges detected: 1/fool_foo2 => 1/fool_foo1 1/fool_foo1 => 1/fool_foo2 __ERR__ cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] 1/P3 = foo => bar 2/P3 = bar => foo __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-param-2" cylc validate . exit cylc-flow-8.6.4/tests/functional/validate/05-implicit-typo.t0000775000175000017500000000303015202510242024113 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of workflow with case mismatches in task names # (accidental implicit tasks) . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = "Foo => bar" [runtime] [[foo]] [[bar]] __FLOW__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/49-jinja2-undefined-error/0000775000175000017500000000000015202510242025377 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/49-jinja2-undefined-error/flow.cylc0000664000175000017500000000020415202510242027216 0ustar alastairalastair#!jinja2 [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo [[[{{ UNDEFINED_WHATEVER }}]]] cylc-flow-8.6.4/tests/functional/validate/29-pass-constrained-initial/0000775000175000017500000000000015202510242026036 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/29-pass-constrained-initial/flow.cylc0000664000175000017500000000043715202510242027665 0ustar alastairalastair[meta] title = "Test validation of initial cycle point against constraints" [scheduler] [scheduling] initial cycle point = 20100101T06 initial cycle point constraints = T00, T06, T12, T18 [[graph]] T00, T06, T12, T18 = """foo""" [runtime] [[FOO]] [[BAR]] [[foo]] inherit = FOO, BAR cylc-flow-8.6.4/tests/functional/validate/10-bad-recurrence.t0000775000175000017500000000743415202510242024201 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation for a bad recurrences . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 10 TEST_NAME="${TEST_NAME_BASE}-interval" cat >'flow.cylc' <<'__WORKFLOW__' [scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20140101T00 final cycle point = 20140201T00 [[graph]] # PT5D is invalid - should be P5D R/T00/PT5D = "foo" [runtime] [[foo]] script = true __WORKFLOW__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: Cannot process recurrence R/T00/PT5D (initial cycle point=20140101T0000+01) (final cycle point=20140201T0000+01) __ERR__ TEST_NAME="${TEST_NAME_BASE}-old-icp" cat >'flow.cylc' <<'__WORKFLOW__' [scheduler] UTC mode = True [scheduling] initial cycle point = 20140101 [[graph]] R1/P0D = "foo => final_foo" [runtime] [[root]] script = true __WORKFLOW__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: Cannot process recurrence R1/P0D (initial cycle point=20140101T0000Z) (final cycle point=None) This workflow requires a final cycle point. __ERR__ TEST_NAME="${TEST_NAME_BASE}-2-digit-century" cat >'flow.cylc' <<'__WORKFLOW__' [scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20140101T00 final cycle point = 20140201T00 [[graph]] # Users may easily write 00 where they mean T00 or '0' in old syntax. # Technically 00 means the year 0000, but we won't allow it in Cylc. R/00/P5D = "foo" [runtime] [[foo]] script = true __WORKFLOW__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: Cannot process recurrence R/00/P5D (initial cycle point=20140101T0000+01) (final cycle point=20140201T0000+01) '00': 2 digit centuries not allowed. Did you mean T-digit-digit e.g. 'T00'? __ERR__ TEST_NAME="${TEST_NAME_BASE}-old-recurrences" cat >'flow.cylc' <<'__WORKFLOW__' [scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 [[graph]] 0,6,12 = "foo" __WORKFLOW__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: Cannot process recurrence 0 (initial cycle point=20100101T0000+01) (final cycle point=None) '0': not a valid cylc-shorthand or full ISO 8601 date representation __ERR__ TEST_NAME="${TEST_NAME_BASE}-old-cycle-point-format" cat >'flow.cylc' <<'__WORKFLOW__' [scheduler] cycle point format = %Y%m%d%H [scheduling] initial cycle point = 2010010101 [[graph]] R1 = foo __WORKFLOW__ run_fail "${TEST_NAME}" cylc validate . cmp_ok "${TEST_NAME}.stderr" <<'__ERR__' WorkflowConfigError: Cannot process recurrence R1 (initial cycle point=2010010101) (final cycle point=None) '2010010101': not a valid cylc-shorthand or full ISO 8601 date representation __ERR__ exit cylc-flow-8.6.4/tests/functional/validate/29-pass-constrained-initial.t0000775000175000017500000000263115202510242026230 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating simple multi-inheritance workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/03-incomplete-quotes/0000775000175000017500000000000015202510242024577 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/03-incomplete-quotes/flow.cylc0000664000175000017500000000013215202510242026416 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = "foo cylc-flow-8.6.4/tests/functional/validate/14-fail-old-syntax-3/0000775000175000017500000000000015202510242024277 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/14-fail-old-syntax-3/flow.cylc0000664000175000017500000000032715202510242026124 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20100101T00 [[graph]] T00,T06,T12 = "foo" [runtime] [[root]] [[[events]]] execution timeout = 3 cylc-flow-8.6.4/tests/functional/validate/00-multi.t0000775000175000017500000000263115202510242022443 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating simple multi-inheritance workflows. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/28-9999_rollover.t0000775000175000017500000000247215202510242023675 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 9999_rollover #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_fail "${TEST_NAME}" cylc validate --debug "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/validate/01-periodical/0000775000175000017500000000000015202510242023233 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/01-periodical/reference.log0000664000175000017500000000030115202510242025666 0ustar alastairalastairInitial point: 2010010100 Final point: 2010010200 2010010100/monthly -triggered off [] 2010010100/yearly -triggered off [] 2010010100/daily -triggered off [] 2010010200/daily -triggered off [] cylc-flow-8.6.4/tests/functional/validate/01-periodical/flow.cylc0000664000175000017500000000040515202510242025055 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100102T00 [[graph]] P1D = "daily" P1M = "monthly" P1Y = "yearly" [runtime] [[root]] script = "true" cylc-flow-8.6.4/tests/functional/validate/15-fail-old-syntax-4/0000775000175000017500000000000015202510242024301 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/15-fail-old-syntax-4/flow.cylc0000664000175000017500000000020015202510242026114 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 2010010100 [[graph]] T12 = "foo" cylc-flow-8.6.4/tests/functional/validate/03-incomplete-quotes.t0000775000175000017500000000261515202510242024773 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validating: script = "foo fails . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/validate/20-fail-no-graph.t0000775000175000017500000000272315202510242023741 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation of an empty graph. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_fail "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" grep_ok "No workflow dependency graph defined\." "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/validate/40-jinja2-template-syntax-error-main/0000775000175000017500000000000015202510242027506 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/40-jinja2-template-syntax-error-main/flow.cylc0000664000175000017500000000027215202510242031332 0ustar alastairalastair#!jinja2 [scheduler] allow implicit tasks = True [scheduling] [[graph]] {% if true %} R1 = foo {% end if % [runtime] [[foo]] script = sleep 1 cylc-flow-8.6.4/tests/functional/validate/31-fail-not-integer.t0000775000175000017500000000321315202510242024456 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test validation with initial and final cycle points in scheduling but no R1. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduling] initial cycle point = 2015-01-01 final cycle point = 2015-01-01 [[graph]] 1 = foo [runtime] [[foo]] script = sleep 10 __FLOW_CONFIG__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}" run_fail "${TEST_NAME}" cylc validate -v . grep_ok "WorkflowConfigError: Cannot process recurrence 1" "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- exit cylc-flow-8.6.4/tests/functional/validate/19-fail-no-dependencies/0000775000175000017500000000000015202510242025102 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/validate/19-fail-no-dependencies/flow.cylc0000664000175000017500000000003315202510242026721 0ustar alastairalastair[scheduling] [[graph]] cylc-flow-8.6.4/tests/functional/recurrence-min/0000775000175000017500000000000015202510242022027 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/01-offset-initial.t0000775000175000017500000000170015202510242025350 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with R/min(+PTNH,...) syntax . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/recurrence-min/00-basic/0000775000175000017500000000000015202510242023325 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/00-basic/reference.log0000664000175000017500000000012515202510242025764 0ustar alastairalastairInitial point: 20100101T0300Z Final point: None 20100101T0600Z/foo -triggered off [] cylc-flow-8.6.4/tests/functional/recurrence-min/00-basic/flow.cylc0000664000175000017500000000021215202510242025143 0ustar alastairalastair[scheduler] UTC mode = true [scheduling] initial cycle point = 20100101T03 [[graph]] R1/min(T00, T06, T12 , T18) = foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/recurrence-min/03-neg-offset-truncated/0000775000175000017500000000000015202510242026273 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/03-neg-offset-truncated/reference.log0000664000175000017500000000012515202510242030732 0ustar alastairalastairInitial point: 20100101T0300Z Final point: None 20100101T0400Z/foo -triggered off [] cylc-flow-8.6.4/tests/functional/recurrence-min/03-neg-offset-truncated/flow.cylc0000664000175000017500000000021415202510242030113 0ustar alastairalastair[scheduler] UTC mode = true [scheduling] initial cycle point = 20100101T03 [[graph]] R1/min(T00-PT20H,T06,T12,T18) = foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/recurrence-min/03-neg-offset-truncated.t0000775000175000017500000000170015202510242026461 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with R/min(+PTNH,...) syntax . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/recurrence-min/02-offset-truncated/0000775000175000017500000000000015202510242025523 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/02-offset-truncated/reference.log0000664000175000017500000000012515202510242030162 0ustar alastairalastairInitial point: 20100101T0300Z Final point: None 20100101T0700Z/foo -triggered off [] cylc-flow-8.6.4/tests/functional/recurrence-min/02-offset-truncated/flow.cylc0000664000175000017500000000021315202510242027342 0ustar alastairalastair[scheduler] UTC mode = true [scheduling] initial cycle point = 20100101T03 [[graph]] R1/min(T00,T06+PT1H,T12,T18) = foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/recurrence-min/test_header0000777000175000017500000000000015202510242030344 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/01-offset-initial/0000775000175000017500000000000015202510242025162 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/recurrence-min/01-offset-initial/reference.log0000664000175000017500000000012515202510242027621 0ustar alastairalastairInitial point: 20100101T0300Z Final point: None 20100101T0400Z/foo -triggered off [] cylc-flow-8.6.4/tests/functional/recurrence-min/01-offset-initial/flow.cylc0000664000175000017500000000021415202510242027002 0ustar alastairalastair[scheduler] UTC mode = true [scheduling] initial cycle point = 20100101T03 [[graph]] R1/min(T00,+PT1H,T06,T12,T18) = foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/recurrence-min/02-offset-truncated.t0000775000175000017500000000170015202510242025711 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with R/min(+PTNH,...) syntax . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/recurrence-min/00-basic.t0000775000175000017500000000167615202510242023527 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running with R/min(T00,...) syntax . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/empy/0000775000175000017500000000000015202510242020063 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/empy/test_header0000777000175000017500000000000015202510242026400 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/empy/00-simple/0000775000175000017500000000000015202510242021571 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/empy/00-simple/flow.cylc-expanded0000664000175000017500000000105415202510242025202 0ustar alastairalastair[scheduling] [[graph]] R1 = "a => FAM" [runtime] [[a]] script = echo this is a set variable [[FAM]] [[[environment]]] TITLE="member" [[member_0]] inherit = FAM script = echo I am $TITLE 0 [[member_1]] inherit = FAM script = echo I am $TITLE 1 [[member_2]] inherit = FAM script = echo I am $TITLE 2 [[member_3]] inherit = FAM script = echo I am $TITLE 3 [[member_4]] inherit = FAM script = echo I am $TITLE 4 cylc-flow-8.6.4/tests/functional/xtriggers/0000775000175000017500000000000015202510242021127 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/xtriggers/02-persistence.t0000664000175000017500000000520015202510242024054 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test persistence of xtrigger results across restart. A cycling task depends # on a non cycle-point dependent custom xtrigger called "faker". In the first # cycle point the xtrigger succeeds and returns a result, then a task shuts # the workflow down. Then we replace the custom xtrigger function with one that # will fail if called again - which should not happen because the original # result should be remembered (as this xtrigger is not cycle point dependent). # Also test the correct result is broadcast to the dependent task before and # after workflow restart. . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the succeeding xtrigger function. cd "${WORKFLOW_RUN_DIR}" || exit 1 mkdir -p 'lib/python' cp "${WORKFLOW_RUN_DIR}/faker_succ.py" 'lib/python/faker.py' # Validate the test workflow. run_ok "${TEST_NAME_BASE}-val" cylc validate --debug "${WORKFLOW_NAME}" # Run the first cycle, till auto shutdown by task. TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" # Check the broadcast result of xtrigger. cylc cat-log "${WORKFLOW_NAME}//2010/foo" >'2010.foo.out' grep_ok 'NAME is bob' '2010.foo.out' # Replace the xtrigger function with one that will fail if called again. cp "${WORKFLOW_RUN_DIR}/faker_fail.py" 'lib/python/faker.py' # Validate again (with the new xtrigger function). run_ok "${TEST_NAME_BASE}-val2" cylc validate --debug "${WORKFLOW_NAME}" # Restart the workflow, to run the final cycle point. TEST_NAME="${TEST_NAME_BASE}-restart" workflow_run_ok "${TEST_NAME}" cylc play --no-detach "${WORKFLOW_NAME}" # Check the broadcast result has persisted from first run. cylc cat-log "${WORKFLOW_NAME}//2011/foo" >'2011.foo.out' grep_ok 'NAME is bob' '2011.foo.out' purge cylc-flow-8.6.4/tests/functional/xtriggers/04-suite_state.t0000664000175000017500000000313315202510242024066 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test that deprecation warnings are printed appropriately for the suite_state # xtrigger. . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "$TEST_NAME_BASE" << __FLOW_CONFIG__ [scheduling] initial cycle point = 2000 [[dependencies]] [[[R1]]] graph = @upstream => foo [[xtriggers]] upstream = suite_state(suite=thorin/oin/gloin, task=mithril, point=1) [runtime] [[foo]] __FLOW_CONFIG__ msg='WARNING - The suite_state xtrigger is deprecated' TEST_NAME="${TEST_NAME_BASE}-val" run_ok "$TEST_NAME" cylc validate "$WORKFLOW_NAME" grep_ok "$msg" "${TEST_NAME}.stderr" # Rename flow.cylc to suite.rc: mv "${WORKFLOW_RUN_DIR}/flow.cylc" "${WORKFLOW_RUN_DIR}/suite.rc" TEST_NAME="${TEST_NAME_BASE}-val-2" run_ok "$TEST_NAME" cylc validate "$WORKFLOW_NAME" grep_fail "$msg" "${TEST_NAME}.stderr" cylc-flow-8.6.4/tests/functional/xtriggers/04-respect-cylc-pythonpath/0000775000175000017500000000000015202510242026141 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/xtriggers/04-respect-cylc-pythonpath/dir/0000775000175000017500000000000015202510242026717 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/xtriggers/04-respect-cylc-pythonpath/dir/echo.py0000664000175000017500000000154415202510242030213 0ustar alastairalastair#!/usr/bin/env python3 # 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 . def echo(*args, **kwargs): print(f"echo overridden, args={args}") return (True, {}) cylc-flow-8.6.4/tests/functional/xtriggers/04-respect-cylc-pythonpath/flow.cylc0000664000175000017500000000021715202510242027764 0ustar alastairalastair[scheduling] [[xtriggers]] x1 = echo("the_args") [[graph]] R1 = @x1 => foo [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/xtriggers/04-sequential.t0000664000175000017500000000601115202510242023705 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test xtrigger sequential spawning - # . "$(dirname "$0")/test_header" set_test_number 7 # Test workflow uses built-in 'echo' xtrigger. init_workflow "${TEST_NAME_BASE}" << '__FLOW_CONFIG__' [scheduler] cycle point format = %Y allow implicit tasks = True [scheduling] initial cycle point = 3000 runahead limit = P5 sequential xtriggers = True [[xtriggers]] clock_1 = wall_clock(offset=P2Y, sequential=False) clock_2 = wall_clock() up_1 = workflow_state(\ workflow_task_id=%(workflow)s//%(point)s/b:succeeded, offset=-P1Y, sequential=False \ ):PT1S [[graph]] R1 = """ @clock_1 => a b """ +P1Y/P1Y = """ @clock_2 => a @clock_2 => b @up_1 => c """ __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" # Run workflow; it will stall waiting on the never-satisfied xtriggers. cylc play "${WORKFLOW_NAME}" poll_grep_workflow_log -E '3001/c/.* => succeeded' cylc stop --max-polls=10 --interval=2 "${WORKFLOW_NAME}" cylc play "${WORKFLOW_NAME}" cylc show "${WORKFLOW_NAME}//3001/a" | grep -E 'state: ' > 3001.a.log cylc show "${WORKFLOW_NAME}//3002/a" 2>&1 >/dev/null | grep -E 'No matching' > 3002.a.log # 3001/a should be spawned at both 3000/3001. cmp_ok 3001.a.log - <<__END__ state: waiting __END__ # 3002/a should not exist. cmp_ok 3002.a.log - <<__END__ No matching active tasks found: 3002/a __END__ cylc reload "${WORKFLOW_NAME}" cylc remove "${WORKFLOW_NAME}//3001/b" poll_grep_workflow_log 'Command "remove_tasks" actioned.' cylc show "${WORKFLOW_NAME}//3002/b" | grep -E 'state: ' > 3002.b.log cylc show "${WORKFLOW_NAME}//3003/b" 2>&1 >/dev/null | grep -E 'No matching' > 3003.b.log # 3002/b should be only at 3002. cmp_ok 3002.b.log - <<__END__ state: waiting __END__ cmp_ok 3003.b.log - <<__END__ No matching active tasks found: 3003/b __END__ cylc show "${WORKFLOW_NAME}//3002/c" | grep -E 'state: ' > 3002.c.log cylc show "${WORKFLOW_NAME}//3005/c" | grep -E 'state: ' > 3005.c.log # c should be from 3002-3005. cmp_ok 3002.c.log - <<__END__ state: waiting __END__ cmp_ok 3005.c.log - <<__END__ state: waiting __END__ cylc stop --now --max-polls=10 --interval=2 "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/xtriggers/03-sequence.t0000664000175000017500000000365215202510242023352 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test xtrigger cycle-point specificity - # https://github.com/cylc/cylc-flow/issues/3283 . "$(dirname "$0")/test_header" set_test_number 2 # Test workflow uses built-in 'echo' xtrigger. init_workflow "${TEST_NAME_BASE}" << '__FLOW_CONFIG__' [scheduler] cycle point format = %Y [scheduling] initial cycle point = 2025 final cycle point = +P1Y [[xtriggers]] e1 = echo(name='bob', succeed=True) e2 = echo(name='alice', succeed=False) [[dependencies]] [[[R1]]] graph = "start" [[[R/^/P2Y]]] graph = "@e1 => foo" [[[R/^+P1Y/P2Y]]] graph = "@e2 => foo" [runtime] [[start]] [[foo]] __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" # Run workflow; it will stall waiting on the never-satisfied xtriggers. cylc play "${WORKFLOW_NAME}" poll_grep_workflow_log -E '2025/start.* => succeeded' cylc show "${WORKFLOW_NAME}//2026/foo" | grep -E '^ ⨯ xtrigger' > 2026.foo.log # 2026/foo should get only xtrigger e2. cmp_ok 2026.foo.log - <<__END__ ⨯ xtrigger "e2 = echo(name=alice, succeed=False)" __END__ cylc stop --now --max-polls=10 --interval=2 "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/xtriggers/04-respect-cylc-pythonpath.t0000664000175000017500000000277615202510242026342 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # An xtrigger added to $CYLC_PYTHONPATH should take precedence # over a `cylc.xtriggers` entry point of the same name . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the succeeding xtrigger function. export CYLC_PYTHONPATH=${WORKFLOW_RUN_DIR}/dir:${CYLC_PYTHONPATH:-} # Validate the test workflow. run_ok "${TEST_NAME_BASE}-val" cylc validate --debug "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" # Check the result of xtrigger. grep_workflow_log_ok "${TEST_NAME_BASE}-grep" "echo overridden, args=('the_args',)" purge cylc-flow-8.6.4/tests/functional/xtriggers/test_header0000777000175000017500000000000015202510242027444 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/xtriggers/02-persistence/0000775000175000017500000000000015202510242023672 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/xtriggers/02-persistence/faker_fail.py0000664000175000017500000000155115202510242026331 0ustar alastairalastair#!/usr/bin/env python3 # 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 . def faker(name, debug=False): print("%s: failing" % name) return (False, {"name": name}) cylc-flow-8.6.4/tests/functional/xtriggers/02-persistence/faker_succ.py0000664000175000017500000000155315202510242026355 0ustar alastairalastair#!/usr/bin/env python3 # 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 . def faker(name, debug=False): print("%s: succeeding" % name) return (True, {"name": name}) cylc-flow-8.6.4/tests/functional/xtriggers/02-persistence/flow.cylc0000664000175000017500000000061415202510242025516 0ustar alastairalastair[scheduler] cycle point format = %Y [scheduling] initial cycle point = 2010 final cycle point = 2011 runahead limit = P0 [[xtriggers]] x1 = faker(name="bob") [[graph]] R1 = "@x1 => foo => shutdown" P1Y = "@x1 => foo" [runtime] [[foo]] script = "echo NAME is $x1_name" [[shutdown]] script = "cylc shutdown $CYLC_WORKFLOW_ID" cylc-flow-8.6.4/tests/functional/env-filter/0000775000175000017500000000000015202510242021164 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/env-filter/00-filter.t0000664000175000017500000000600515202510242023054 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test environment filtering . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 13 #------------------------------------------------------------------------------- # a test workflow that uses environment filtering: init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = "foo & bar & baz & qux" [runtime] [[root]] [[[environment]]] FOO = foo BAR = bar BAZ = baz [[foo]] [[[environment filter]]] include = FOO, BAR [[[environment]]] QUX = qux [[bar]] [[[environment filter]]] include = FOO, BAR exclude = FOO [[baz]] [[[environment filter]]] exclude = FOO, BAR [[[environment]]] QUX = qux [[qux]] [[[environment]]] QUX = qux __FLOW_CONFIG__ #------------------------------------------------------------------------------- # check validation TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # check that config retrieves only the filtered environment TEST_NAME=${TEST_NAME_BASE}-config run_ok "${TEST_NAME}" cylc config --item='[runtime][foo]environment' "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME}.stdout" - <<__OUT__ FOO = foo BAR = bar __OUT__ cmp_ok "${TEST_NAME}.stderr" - b [runtime] [[a]] script = sleep 20 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//*/a" cylc-flow-8.6.4/tests/functional/cylc-poll/08-slurm.t0000777000175000017500000000000015202510242025464 206-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/10-basic-compat.t0000775000175000017500000000167615202510242023771 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cat-check against workflow database . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-poll/02-task-submit-failed.t0000775000175000017500000000221715202510242025105 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that polling a submit-failed task sets the task state correctly export REQUIRE_PLATFORM='runner:at comms:tcp' . "$(dirname "$0")/test_header" set_test_number 2 create_test_global_config "" " [platforms] [[$CYLC_TEST_PLATFORM]] job runner command template = at noon tomorrow " reftest purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/13-comm-method/0000775000175000017500000000000015202510242023441 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/13-comm-method/reference.log0000664000175000017500000000011615202510242026100 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off [] cylc-flow-8.6.4/tests/functional/cylc-poll/13-comm-method/flow.cylc0000664000175000017500000000100415202510242025257 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = t1 & t2 [runtime] [[root]] script = """ wait # sleep for twice the polling interval to make sure # the started message gets picked up before the succeeded # message is issued sleep 12 """ platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[t1]] [[t2]] [[[job]]] submission polling intervals = 10*PT6S execution polling intervals = 10*PT6S cylc-flow-8.6.4/tests/functional/cylc-poll/09-lsf.t0000777000175000017500000000000015202510242025107 206-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/11-event-time.t0000775000175000017500000000277715202510242023510 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test delayed poll gives the correct event time . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" RUND="$RUN_DIR/${WORKFLOW_NAME}" sed -n 's/CYLC_JOB_EXIT_TIME=//p' "${RUND}/log/job/1/w1/NN/job.status" >'st-time.txt' sqlite3 "${RUND}/log/db" " SELECT time_run_exit FROM task_jobs WHERE cycle=='1' AND name=='w1' AND submit_num=='1'" >'db-time.txt' run_ok "${TEST_NAME_BASE}-time-run-exit" diff -u 'st-time.txt' 'db-time.txt' purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/14-intervals/0000775000175000017500000000000015202510242023240 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/14-intervals/reference.log0000664000175000017500000000011615202510242025677 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off [] cylc-flow-8.6.4/tests/functional/cylc-poll/14-intervals/flow.cylc0000664000175000017500000000051415202510242025063 0ustar alastairalastair[scheduling] [[graph]] R1 = t1 & t2 [runtime] [[t1]] env-script = """ # Simulate disappearing after job submission export CYLC_TASK_COMMS_METHOD=poll sleep 10 """ script = true [[t2]] script = """ # Simulate disappearing after job running export CYLC_TASK_COMMS_METHOD=poll sleep 10 """ cylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/0000775000175000017500000000000015202510242024372 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/lib/0000775000175000017500000000000015202510242025140 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/lib/python/0000775000175000017500000000000015202510242026461 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/lib/python/badqstat0000775000175000017500000000007115202510242030210 0ustar alastairalastair#!/usr/bin/env bash echo 'Connection refused' >&2 exit 1 cylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/lib/python/my_pbs.py0000664000175000017500000000220615202510242030324 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import os from cylc.flow.job_runner_handlers.pbs import PBSHandler class MyPBSHandler(PBSHandler): """For testing poll command connection refused.""" @staticmethod def get_poll_many_cmd(_): """Always print PBSHandler.POLL_CANT_CONNECT_ERR to STDERR.""" return os.path.join(os.path.dirname(__file__), 'badqstat') JOB_RUNNER_HANDLER = MyPBSHandler() cylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/reference.log0000664000175000017500000000006715202510242027036 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect/flow.cylc0000664000175000017500000000040515202510242026214 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = sleep 60 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[job]]] execution time limit = PT2M execution polling intervals = PT20S cylc-flow-8.6.4/tests/functional/cylc-poll/15-job-st-file-no-batch.t0000775000175000017500000000271015202510242025226 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test polling with "job.status" missing job runner information. . "$(dirname "${0}")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" run_ok "${TEST_NAME_BASE}-log-1" \ grep -F '[jobs-poll err] 1/t1/01/job.status: incomplete job runner info' "${LOG}" run_ok "${TEST_NAME_BASE}-log-2" \ grep -E '1/t1/01:running.*\(polled\)failed' "${LOG}" purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/11-event-time/0000775000175000017500000000000015202510242023303 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/11-event-time/reference.log0000664000175000017500000000012415202510242025741 0ustar alastairalastairInitial point: 1 Final point: 1 1/w1 -triggered off [] 1/w2 -triggered off ['1/w1'] cylc-flow-8.6.4/tests/functional/cylc-poll/11-event-time/flow.cylc0000664000175000017500000000116415202510242025130 0ustar alastairalastair[scheduling] [[graph]] R1="w1:started => w2" [runtime] [[w1]] init-script=cylc__job__disable_fail_signals ERR EXIT script=""" cylc__job__wait_cylc_message_started # Append to job.status cat >>"${CYLC_TASK_LOG_ROOT}.status" <<__STATUS__ CYLC_JOB_EXIT=SUCCEEDED CYLC_JOB_EXIT_TIME=$(date -u +'%FT%H:%M:%SZ') __STATUS__ # Exit without trap exit 1 """ [[w2]] script = """ cylc__job__wait_cylc_message_started cylc poll "${CYLC_WORKFLOW_ID}//1/w1" """ cylc-flow-8.6.4/tests/functional/cylc-poll/07-pbs/0000775000175000017500000000000015202510242022017 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/07-pbs/reference.log0000664000175000017500000000012115202510242024452 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/07-pbs/flow.cylc0000664000175000017500000000034115202510242023640 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = a:start => b [runtime] [[a]] script = sleep 20 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//*/a" cylc-flow-8.6.4/tests/functional/cylc-poll/13-comm-method.t0000775000175000017500000000374015202510242023635 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test poll intervals is used from both global.cylc and flow.cylc . "$(dirname "${0}")/test_header" #------------------------------------------------------------------------------- set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" create_test_global_config " [platforms] [[$CYLC_TEST_PLATFORM]] communication method = poll execution polling intervals = 10*PT6S submission polling intervals = 10*PT6S " workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- LOG_FILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" PRE_MSG='health:' POST_MSG='.*, polling intervals=10\*PT6S...' for INDEX in 1 2; do for STAGE in 'submission' 'execution'; do grep_ok "1/t${INDEX}.* ${PRE_MSG} ${STAGE}${POST_MSG}" "${LOG_FILE}" -E done done #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/08-slurm/0000775000175000017500000000000015202510242022376 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/08-slurm/reference.log0000664000175000017500000000012115202510242025031 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/08-slurm/flow.cylc0000664000175000017500000000034115202510242024217 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = a:start => b [runtime] [[a]] script = sleep 20 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//*/a" cylc-flow-8.6.4/tests/functional/cylc-poll/00-basic/0000775000175000017500000000000015202510242022305 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/00-basic/reference.log0000664000175000017500000000012115202510242024740 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/00-basic/flow.cylc0000664000175000017500000000041215202510242024125 0ustar alastairalastair[scheduling] [[graph]] R1 = a:start => b [runtime] [[a]] script = """ while ! grep -qF 'CYLC_JOB_EXIT' "${CYLC_WORKFLOW_RUN_DIR}/log/job/1/b/NN/job.status" do sleep 1 done """ [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//1/a" cylc-flow-8.6.4/tests/functional/cylc-poll/01-task-failed/0000775000175000017500000000000015202510242023411 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/01-task-failed/reference.log0000664000175000017500000000016215202510242026051 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/handled -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/01-task-failed/flow.cylc0000664000175000017500000000150315202510242025233 0ustar alastairalastair[meta] title = "Test workflow for task state change on poll result." description = """ Task A fails silently - it will be stuck in 'running' unless polled. Task B then polls A to find it has failed, allowing A to suicide via a :fail trigger, and the workflow to shut down successfully. """ [scheduler] [[events]] expected task failures = 1/a [scheduling] [[graph]] R1 = """ a:start => b a:fail => handled """ [runtime] [[a]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started exit 1 """ [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//*/a" [[handled]] # (allows a:fail to be removed as handled) script = true cylc-flow-8.6.4/tests/functional/cylc-poll/10-basic-compat/0000775000175000017500000000000015202510242023567 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/10-basic-compat/reference.log0000664000175000017500000000012115202510242026222 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/10-basic-compat/flow.cylc0000664000175000017500000000035615202510242025416 0ustar alastairalastair[scheduling] [[graph]] R1 = a:start => b [runtime] [[a]] script = sleep 20 [[b]] script = """ cylc__job__wait_cylc_message_started cylc poll "$CYLC_WORKFLOW_ID//1/a" """ cylc-flow-8.6.4/tests/functional/cylc-poll/06-loadleveler.t0000775000175000017500000000225615202510242023725 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc poll" for loadleveler, slurm, or pbs jobs. # TODO Check this test on a dockerized system or VM. JOB_RUNNER="${0##*\/??-}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER%%.t} comms:tcp" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/05-poll-multi-messages/0000775000175000017500000000000015202510242025134 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/05-poll-multi-messages/reference.log0000664000175000017500000000032415202510242027574 0ustar alastairalastairInitial point: 1 Final point: 1 1/speaker1 -triggered off [] 1/speaker2 -triggered off [] 1/poller -triggered off ['1/speaker1', '1/speaker2'] 1/finisher -triggered off ['1/speaker1', '1/speaker1', '1/speaker2'] cylc-flow-8.6.4/tests/functional/cylc-poll/05-poll-multi-messages/flow.cylc0000664000175000017500000000316615202510242026765 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] [[graph]] R1 = """ speaker1:start & speaker2:start => poller speaker1:hello1 & speaker1:hello2 & speaker2:greet => finisher """ [runtime] [[speaker1]] script = """ # Wait for "cylc message started" command cylc__job__wait_cylc_message_started # Simulate "cylc message", messages written to status file but failed to # get sent back to the workflow { echo "CYLC_MESSAGE=$(date +%FT%H:%M:%SZ)|INFO|hello1" echo "CYLC_MESSAGE=$(date +%FT%H:%M:%SZ)|INFO|hello2" } >>"${CYLC_TASK_LOG_ROOT}.status" cylc__job__poll_grep_workflow_log -E '1/speaker1/01:running.* \(polled\)hello1' cylc__job__poll_grep_workflow_log -E '1/speaker1/01:running.* \(polled\)hello2' """ [[[outputs]]] hello1 = "hello1" hello2 = "hello2" [[speaker2]] script=""" # Wait for "cylc message started" command cylc__job__wait_cylc_message_started # Simulate "cylc message", messages written to status file but failed to # get sent back to the workflow echo "CYLC_MESSAGE=$(date +%FT%H:%M:%SZ)|INFO|greet" \ >>"${CYLC_TASK_LOG_ROOT}.status" cylc__job__poll_grep_workflow_log -E '1/speaker2/01:running.* \(polled\)greet' """ [[[outputs]]] greet = "greet" [[finisher]] script=true [[poller]] script=cylc poll "${CYLC_WORKFLOW_ID}//*/speaker[12]" cylc-flow-8.6.4/tests/functional/cylc-poll/05-poll-multi-messages.t0000775000175000017500000000166115202510242025330 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test poll multiple messages . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-poll/01-task-failed.t0000775000175000017500000000172315202510242023604 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that polling a failed task sets the task state correctly . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-poll/test_header0000777000175000017500000000000015202510242027324 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/06-loadleveler/0000775000175000017500000000000015202510242023530 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/06-loadleveler/reference.log0000664000175000017500000000012115202510242026163 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] cylc-flow-8.6.4/tests/functional/cylc-poll/06-loadleveler/flow.cylc0000664000175000017500000000066415202510242025361 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = a:start => b [runtime] [[a]] script = sleep 20 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[directives]]] class=serial job_type=serial notification=never resources=ConsumableCpus(1) ConsumableMemory(64mb) wall_clock_limit=180,120 [[b]] script = cylc poll "$CYLC_WORKFLOW_ID//*/a" cylc-flow-8.6.4/tests/functional/cylc-poll/02-task-submit-failed/0000775000175000017500000000000015202510242024713 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/02-task-submit-failed/reference.log0000664000175000017500000000026315202510242027355 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/kill_foo_submit -triggered off ['1/foo'] 1/poll_foo -triggered off ['1/kill_foo_submit'] 1/stop -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/cylc-poll/02-task-submit-failed/flow.cylc0000664000175000017500000000135615202510242026543 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/foo [scheduling] [[graph]] R1 = """ foo:submit? => kill_foo_submit => poll_foo foo:submit-fail? => stop """ [runtime] [[foo]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[poll_foo]] script = """ cylc__job__wait_cylc_message_started cylc poll "$CYLC_WORKFLOW_ID//*/foo" """ [[stop]] script = cylc stop $CYLC_WORKFLOW_ID [[kill_foo_submit]] script = """ cylc__job__wait_cylc_message_started ID=$(sed -n "s/^CYLC_JOB_ID=//p" \ $CYLC_WORKFLOW_RUN_DIR/log/job/1/foo/01/job.status) atrm $ID """ cylc-flow-8.6.4/tests/functional/cylc-poll/17-pbs-cant-connect.t0000775000175000017500000000412415202510242024563 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test poll PBS connection refused export REQUIRE_PLATFORM="runner:pbs" . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] job runner = my_pbs hosts = ${CYLC_TEST_BATCH_TASK_HOST} " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" if [[ "${CYLC_TEST_HOST}" != 'localhost' ]]; then # shellcheck disable=SC2029 ssh -n "${CYLC_TEST_HOST}" "mkdir -p 'cylc-run/${WORKFLOW_NAME}/'" rsync -a 'lib' "${CYLC_TEST_HOST}:cylc-run/${WORKFLOW_NAME}/" fi run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" # ssh security warnings may appear between outputs => check separately too. sed -n 's/^.*\(\[jobs-poll err\]\) \(Connection refused\).*$/\1\n\2/p; s/^.*\(\[jobs-poll err\]\).*$/\1/p; s/^.*\(Connection refused\).*$/\1/p; s/^.*\(INFO - \[1/t1\] status=running: (polled)started\).*$/\1/p' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" >'sed-log.out' contains_ok 'sed-log.out' <<'__LOG__' [jobs-poll err] Connection refused __LOG__ contains_ok 'sed-log.out' <<'__LOG__' INFO - [1/t1] status=running: (polled)started __LOG__ purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/04-poll-multi-hosts/0000775000175000017500000000000015202510242024464 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/04-poll-multi-hosts/reference.log0000664000175000017500000000060315202510242027124 0ustar alastairalastairInitial point: 1 Final point: 1 1/remote-success-1 -triggered off [] 1/remote-fail-1 -triggered off [] 1/local-fail-2 -triggered off [] 1/remote-success-2 -triggered off [] 1/local-success-1 -triggered off [] 1/local-fail-1 -triggered off [] 1/poller -triggered off ['1/local-fail-1', '1/local-fail-2', '1/local-success-1', '1/remote-fail-1', '1/remote-success-1', '1/remote-success-2'] cylc-flow-8.6.4/tests/functional/cylc-poll/04-poll-multi-hosts/flow.cylc0000664000175000017500000000242115202510242026306 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [[events]] expected task failures = 1/local-fail-1, 1/local-fail-2, 1/remote-fail-1 [scheduling] [[graph]] R1 = """ POLLABLE:start-all => poller POLLABLE:succeed-any # (make member success optional) """ [runtime] [[POLLABLE]] init-script = cylc__job__disable_fail_signals ERR EXIT [[FAIL]] inherit = POLLABLE script = """ echo 'I am failing...' >&2 exit 1 """ [[local-fail-1, local-fail-2]] inherit = FAIL [[remote-fail-1]] inherit = FAIL platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[SUCCESS]] inherit = POLLABLE script = """ echo 'I am OK.' { echo 'CYLC_JOB_EXIT=SUCCEEDED' echo "CYLC_JOB_EXIT_TIME=$(date +%FT%H:%M:%SZ)" } >>"${CYLC_TASK_LOG_ROOT}.status" exit 1 """ [[local-success-1]] inherit = SUCCESS [[remote-success-1, remote-success-2]] inherit = SUCCESS platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[poller]] script = """ cylc poll "${CYLC_WORKFLOW_ID}//*/POLLABLE" cylc stop "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/cylc-poll/07-pbs.t0000777000175000017500000000000015202510242025105 206-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/04-poll-multi-hosts.t0000775000175000017500000000336515202510242024663 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test poll multiple jobs on localhost and a remote host export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" RUN_DIR="$RUN_DIR/${WORKFLOW_NAME}" LOG="${RUN_DIR}/log/scheduler/log" sed -n 's/^.*\(cylc jobs-poll\)/\1/p' "${LOG}" | sort -u >'edited-workflow-log' sort >'edited-workflow-log-ref' <<__LOG__ cylc jobs-poll --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/remote-fail-1/01 1/remote-success-1/01 1/remote-success-2/01 cylc jobs-poll --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/local-fail-1/01 1/local-fail-2/01 1/local-success-1/01 __LOG__ cmp_ok 'edited-workflow-log' 'edited-workflow-log-ref' purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/14-intervals.t0000775000175000017500000000406215202510242023432 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test the correct intervals are used . "$(dirname "${0}")/test_header" #------------------------------------------------------------------------------- set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" create_test_global_config ' [platforms] [[localhost]] submission polling intervals = PT2S,6*PT10S execution polling intervals = 2*PT1S,10*PT6S' workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- LOG_FILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" PRE_MSG='health:' for INDEX in 1 2; do for STAGE in 'submission' 'execution'; do POLL_INT='PT2S,6\*PT10S,' if [[ "${STAGE}" == 'execution' ]]; then POLL_INT='2\*PT1S,10\*PT6S,' fi POST_MSG=".*, polling intervals=${POLL_INT}..." grep_ok "1/t${INDEX}.*${PRE_MSG} ${STAGE}${POST_MSG}" "${LOG_FILE}" -E done done #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/cylc-poll/15-job-st-file-no-batch/0000775000175000017500000000000015202510242025036 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-poll/15-job-st-file-no-batch/reference.log0000664000175000017500000000006715202510242027502 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/cylc-poll/15-job-st-file-no-batch/flow.cylc0000664000175000017500000000111515202510242026657 0ustar alastairalastair[scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started ST_FNAME="${CYLC_TASK_LOG_ROOT}.status" sed -i '/\(CYLC_JOB_RUNNER_NAME\|CYLC_JOB_ID\)/d' "${ST_FNAME}" #echo 'CYLC_JOB_EXIT=SUCCEEDED' >>"${ST_FNAME}" #echo "CYLC_JOB_EXIT_TIME=$(date -u +%FT%H:%M:%SZ)" >>"${ST_FNAME}" exit 1 """ [[[events]]] execution timeout = PT15S cylc-flow-8.6.4/tests/functional/cylc-poll/00-basic.t0000775000175000017500000000167615202510242022507 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cat-check against workflow database . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/intercycle/0000775000175000017500000000000015202510242021252 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/05-datetime-abs-3/0000775000175000017500000000000015202510242024173 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/05-datetime-abs-3/reference.log0000664000175000017500000000032415202510242026633 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100102T0000Z 20100101T0600Z/fixed_cycle -triggered off [] 20100101T0000Z/init_cycle -triggered off [] 20100101T1200Z/foo -triggered off ['20100101T0600Z/fixed_cycle'] cylc-flow-8.6.4/tests/functional/intercycle/05-datetime-abs-3/flow.cylc0000664000175000017500000000061115202510242026014 0ustar alastairalastair[scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 20100101T0000Z final cycle point = +P1D [[graph]] R1 = init_cycle R1/2010-01-01T06:00+00:00 = fixed_cycle T12 = fixed_cycle[2010-01-01T06:00+00:00] => foo [runtime] [[foo, fixed_cycle, init_cycle]] script = true cylc-flow-8.6.4/tests/functional/intercycle/04-datetime-abs-2/0000775000175000017500000000000015202510242024171 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/04-datetime-abs-2/reference.log0000664000175000017500000000051315202510242026631 0ustar alastairalastairInitial point: 20000101T0000Z Final point: 20000103T0000Z 20000101T0000Z/start -triggered off [] 20000101T0000Z/foo -triggered off ['20000101T0000Z/start'] 20000102T0000Z/foo -triggered off ['20000101T0000Z/start'] 20000103T0000Z/foo -triggered off ['20000101T0000Z/start'] 20000102T1200Z/bar -triggered off ['20000102T0000Z/foo'] cylc-flow-8.6.4/tests/functional/intercycle/04-datetime-abs-2/flow.cylc0000664000175000017500000000054515202510242026020 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 20000101T00Z [[graph]] R1 = start T00 = start[^] => foo R1/20000102T12Z = foo[20000102T00Z] => bar [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/intercycle/04-datetime-abs-2.t0000664000175000017500000000235215202510242024360 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test dependency on absolute datetime point # https://github.com/cylc/cylc-flow/issues/1951 . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" \ cylc play --reference-test --debug --no-detach --fcp=20000103 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/intercycle/03-datetime-abs.t0000664000175000017500000000176115202510242024223 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test dependency on absolute datetime point # https://github.com/cylc/cylc-flow/issues/1951 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/intercycle/02-integer-abs.t0000664000175000017500000000234215202510242024057 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test dependency on absolute integer point # https://github.com/cylc/cylc-flow/issues/1394 . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" \ cylc play --reference-test --debug --no-detach --fcp=5 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/intercycle/01-future.t0000664000175000017500000000166615202510242023200 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test future cycle dependencies. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/intercycle/00-past/0000775000175000017500000000000015202510242022436 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/00-past/reference.log0000664000175000017500000000035215202510242025077 0ustar alastairalastairInitial point: 20120101T0000Z Final point: 20120101T1800Z 20120101T0000Z/A -triggered off [] 20120101T1200Z/A -triggered off [] 20120101T0600Z/B -triggered off ['20120101T0000Z/A'] 20120101T1800Z/B -triggered off ['20120101T1200Z/A'] cylc-flow-8.6.4/tests/functional/intercycle/00-past/flow.cylc0000664000175000017500000000062415202510242024263 0ustar alastairalastair[meta] title = "reference test workflow: zig-zag intercycle dependencies" description = """ Task A should only run at 0, 12 hours; Task B at 6, 18""" [scheduler] UTC mode = True [scheduling] initial cycle point = 20120101T00 final cycle point = 20120101T18 [[graph]] T00,T12 = "A" T06,T18 = "A[-PT6H] => B" [runtime] [[A, B]] script = "true" # fast cylc-flow-8.6.4/tests/functional/intercycle/test_header0000777000175000017500000000000015202510242027567 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/02-integer-abs/0000775000175000017500000000000015202510242023671 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/02-integer-abs/reference.log0000664000175000017500000000033715202510242026335 0ustar alastairalastairInitial point: 1 Final point: 5 2/start -triggered off [] 1/foo -triggered off ['2/start'] 2/foo -triggered off ['2/start'] 3/foo -triggered off ['2/start'] 4/foo -triggered off ['2/start'] 5/foo -triggered off ['2/start'] cylc-flow-8.6.4/tests/functional/intercycle/02-integer-abs/flow.cylc0000664000175000017500000000043215202510242025513 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 1 cycling mode = integer [[graph]] R1/+P1 = start R//P1 = start[2] => foo [runtime] [[start, foo]] script = true cylc-flow-8.6.4/tests/functional/intercycle/01-future/0000775000175000017500000000000015202510242023002 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/01-future/reference.log0000664000175000017500000000044115202510242025442 0ustar alastairalastairInitial point: 20120101T00 Final point: 20120101T18 20120101T06/a -triggered off [] 20120101T12/a -triggered off [] 20120101T00/b -triggered off ['20120101T06/a'] 20120101T06/b -triggered off ['20120101T12/a'] 20120101T18/a -triggered off [] 20120101T12/b -triggered off ['20120101T18/a'] cylc-flow-8.6.4/tests/functional/intercycle/01-future/flow.cylc0000664000175000017500000000040015202510242024617 0ustar alastairalastair[scheduler] cycle point format = %Y%m%dT%H [scheduling] initial cycle point = 20120101T00 final cycle point = 20120101T18 [[graph]] PT6H = a[+PT6H] => b R/+PT6H/PT6H = a [runtime] [[a,b]] script = "true" # fast cylc-flow-8.6.4/tests/functional/intercycle/03-datetime-abs/0000775000175000017500000000000015202510242024031 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intercycle/03-datetime-abs/reference.log0000664000175000017500000000032415202510242026471 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100102T0000Z 20100101T0600Z/fixed_cycle -triggered off [] 20100101T0000Z/init_cycle -triggered off [] 20100101T1200Z/foo -triggered off ['20100101T0600Z/fixed_cycle'] cylc-flow-8.6.4/tests/functional/intercycle/03-datetime-abs/flow.cylc0000664000175000017500000000057115202510242025657 0ustar alastairalastair[scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 20100101T0000Z final cycle point = +P1D [[graph]] R1 = init_cycle R1/20100101T0600Z = fixed_cycle T12 = fixed_cycle[20100101T0600Z] => foo [runtime] [[foo, fixed_cycle, init_cycle]] script = true cylc-flow-8.6.4/tests/functional/intercycle/05-datetime-abs-3.t0000664000175000017500000000176115202510242024365 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test dependency on absolute datetime point # https://github.com/cylc/cylc-flow/issues/1951 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/intercycle/00-past.t0000664000175000017500000000166415202510242022632 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/graphql/0000775000175000017500000000000015202510242020547 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphql/01-workflow/0000775000175000017500000000000015202510242022637 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphql/01-workflow/flow.cylc0000664000175000017500000000041215202510242024457 0ustar alastairalastair[meta] title = foo description = bar [scheduler] UTC mode = True [scheduling] initial cycle point = 20210101T0000Z final cycle point = 20210101T0000Z [[graph]] P1Y = foo[-P1Y] => foo [runtime] [[foo]] script = sleep 60 cylc-flow-8.6.4/tests/functional/graphql/01-workflow.t0000775000175000017500000000737415202510242023042 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow graphql interface . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # run workflow run_ok "${TEST_NAME_BASE}-run" cylc play --pause "${WORKFLOW_NAME}" # query workflow TEST_NAME="${TEST_NAME_BASE}-workflows" read -r -d '' workflowQuery <<_args_ { "request_string": " query { workflows { name status statusMsg host port owner cylcVersion meta { title description } newestActiveCyclePoint oldestActiveCyclePoint reloaded runMode nEdgeDistance stateTotals workflowLogDir timeZoneInfo { hours minutes } nsDefOrder states latestStateTasks (states: [\"waiting\"]) } }", "variables": null } _args_ run_graphql_ok "${TEST_NAME}" "${WORKFLOW_NAME}" "${workflowQuery}" # scrape workflow info from contact file TEST_NAME="${TEST_NAME_BASE}-contact" run_ok "${TEST_NAME_BASE}-contact" cylc get-contact "${WORKFLOW_NAME}" HOST=$(sed -n 's/CYLC_WORKFLOW_HOST=\(.*\)/\1/p' "${TEST_NAME}.stdout") PORT=$(sed -n 's/CYLC_WORKFLOW_PORT=\(.*\)/\1/p' "${TEST_NAME}.stdout") WORKFLOW_LOG_DIR="$( cylc cat-log -m p "${WORKFLOW_NAME}" \ | xargs dirname )" # stop workflow cylc stop --max-polls=10 --interval=2 --kill "${WORKFLOW_NAME}" # Compare to expectation # Note: One active cycle point on starting paused # (runahead tasks are now in the main scheduler task pool) cmp_json "${TEST_NAME}-out" "${TEST_NAME_BASE}-workflows.stdout" << __HERE__ { "workflows": [ { "name": "${WORKFLOW_NAME}", "status": "paused", "statusMsg": "paused", "host": "${HOST}", "port": ${PORT}, "owner": "${USER}", "cylcVersion": "$(cylc version)", "meta": { "title": "foo", "description": "bar" }, "newestActiveCyclePoint": "20210101T0000Z", "oldestActiveCyclePoint": "20210101T0000Z", "reloaded": false, "runMode": "live", "nEdgeDistance": 1, "stateTotals": { "waiting": 1, "expired": 0, "preparing": 0, "submit-failed": 0, "submitted": 0, "running": 0, "failed": 0, "succeeded": 0 }, "workflowLogDir": "${WORKFLOW_LOG_DIR}", "timeZoneInfo": { "hours": 0, "minutes": 0 }, "nsDefOrder": [ "foo", "root" ], "states": ["waiting"], "latestStateTasks": { "waiting": ["20210101T0000Z/foo"] } } ] } __HERE__ purge exit cylc-flow-8.6.4/tests/functional/graphql/03-is-held-arg.t0000775000175000017500000000600315202510242023252 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow graphql interface # TODO: convert to integration test . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}" << __FLOW__ [meta] title = foo description = bar [scheduling] [[graph]] R1 = foo [runtime] [[BAZ]] [[foo]] inherit = BAZ script = sleep 20 __FLOW__ # run workflow run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" cylc hold --after=0 "${WORKFLOW_NAME}" sleep 1 # query workflow TEST_NAME="${TEST_NAME_BASE}-is-held-arg" read -r -d '' isHeld <<_args_ { "request_string": " query { workflows { name isHeldTotal taskProxies(isHeld: true, graphDepth: 1) { id jobs { submittedTime startedTime } } familyProxies(exids: [\"*/root\"], isHeld: true, graphDepth: 1) { id } } }", "variables": null } _args_ run_graphql_ok "${TEST_NAME}" "${WORKFLOW_NAME}" "${isHeld}" # scrape workflow info from contact file TEST_NAME="${TEST_NAME_BASE}-contact" run_ok "${TEST_NAME_BASE}-contact" cylc get-contact "${WORKFLOW_NAME}" # stop workflow cylc stop --max-polls=10 --interval=2 --kill "${WORKFLOW_NAME}" RESPONSE="${TEST_NAME_BASE}-is-held-arg.stdout" perl -pi -e 's/("submittedTime":).*$/${1} "blargh",/' "${RESPONSE}" perl -pi -e 's/("startedTime":).*$/${1} "blargh"/' "${RESPONSE}" # compare to expectation cmp_json "${TEST_NAME}-out" "$RESPONSE" << __HERE__ { "workflows": [ { "name": "${WORKFLOW_NAME}", "isHeldTotal": 1, "taskProxies": [ { "id": "~${USER}/${WORKFLOW_NAME}//1/foo", "jobs": [ { "submittedTime": "blargh", "startedTime": "blargh" } ] } ], "familyProxies": [ { "id": "~${USER}/${WORKFLOW_NAME}//1/BAZ" } ] } ] } __HERE__ purge cylc-flow-8.6.4/tests/functional/graphql/test_header0000777000175000017500000000000015202510242027064 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-expire/0000775000175000017500000000000015202510242021476 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-expire/00-basic/0000775000175000017500000000000015202510242022774 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-expire/00-basic/flow.cylc0000664000175000017500000000236115202510242024621 0ustar alastairalastair#!Jinja2 [meta] title = task expire example workflow description = """ Skip a daily post-processing workflow if the 'copy' task has expired.""" [scheduler] cycle point format = %Y-%m-%dT%H allow implicit tasks = True cycle point time zone = {{CPTZ}} [[events]] abort on stall timeout = True stall timeout = PT1M [scheduling] initial cycle point = now final cycle point = +P3D [[special tasks]] clock-expire = copy(-P1DT1H) # NOTE this would normally be copy(P1D) i.e. expire if more than 1 day # behind the wall clock, but here we have to start from 'now' in order # to stay near the wall clock, so expire the task if more than 1 day # behind "now + 1 day". This makes the first two 'copy' tasks expire. [[graph]] P1D = """ model[-P1D] => model => copy? => proc copy:expired? => !proc """ [runtime] [[root]] script = true [[copy]] script = """ # Abort if I run in either of the first two cycle points. test "${CYLC_TASK_CYCLE_POINT}" != "${CYLC_WORKFLOW_INITIAL_CYCLE_POINT}" P2D="$(cylc cyclepoint --offset='P1D' "${CYLC_WORKFLOW_INITIAL_CYCLE_POINT}")" test "${CYLC_TASK_CYCLE_POINT}" != "${P2D}" """ cylc-flow-8.6.4/tests/functional/clock-expire/test_header0000777000175000017500000000000015202510242030013 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-expire/00-basic.t0000664000175000017500000000237215202510242023165 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the clock-expire test workflow . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" CPTZ=$(date '+%z') run_ok "${TEST_NAME_BASE}-validate" cylc validate -s "CPTZ='${CPTZ}'" "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --abort-if-any-task-fails -s "CPTZ='${CPTZ}'" "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/workflow-state/0000775000175000017500000000000015202510242022101 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/upstream/0000775000175000017500000000000015202510242023741 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/upstream/flow.cylc0000664000175000017500000000111015202510242025555 0ustar alastairalastair[meta] title = "One task takes 20 sec to succeed, another to fail, another to send a message." [scheduling] [[graph]] R1 = """ good-stuff & bad? bad:fail? => handler messenger:x => done """ [runtime] [[done]] [[good-stuff]] script = "sleep 20" [[bad]] script = "sleep 20; false" [[messenger]] script = """ sleep 20 cylc message 'the quick brown fox' """ [[[outputs]]] x = "the quick brown fox" [[handler]] script = true cylc-flow-8.6.4/tests/functional/workflow-state/01-polling.t0000664000175000017500000000724715202510242024162 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow-state/polling test workflow # The test workflow is in polling/; it depends on another workflow in upstream/ . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'polling' #------------------------------------------------------------------------------- # copy the upstream workflow to the test directory and install it cp -r "${TEST_SOURCE_DIR}/upstream" "${TEST_DIR}/" # this version uses a simple Rose-style workflow name [\w-] UPSTREAM="${WORKFLOW_NAME}-upstream" cylc install "${TEST_DIR}/upstream" --workflow-name="${UPSTREAM}" --no-run-name #------------------------------------------------------------------------------- # validate both workflows as tests TEST_NAME="${TEST_NAME_BASE}-validate-upstream" run_ok "${TEST_NAME}" cylc validate --debug "${UPSTREAM}" TEST_NAME="${TEST_NAME_BASE}-validate-polling" run_ok "${TEST_NAME}" \ cylc validate --debug --set="UPSTREAM='${UPSTREAM}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # check auto-generated task script for lbad cylc config -d \ --set="UPSTREAM='${UPSTREAM}'" \ -i '[runtime][lbad]script' "${WORKFLOW_NAME}" >'lbad.script' cmp_ok 'lbad.script' << __END__ echo cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/bad:failed --interval=2 --max-polls=20 cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/bad:failed --interval=2 --max-polls=20 __END__ # check auto-generated task script for l-good cylc config -d \ --set="UPSTREAM='${UPSTREAM}'" \ -i '[runtime][l-good]script' "${WORKFLOW_NAME}" >'l-good.script' cmp_ok 'l-good.script' << __END__ echo cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/good-stuff:succeeded --interval=2 --max-polls=20 cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/good-stuff:succeeded --interval=2 --max-polls=20 __END__ #------------------------------------------------------------------------------- # run the upstream workflow and detach (not a test) cylc play "${UPSTREAM}" 1>'upstream.out' 2>&1 #------------------------------------------------------------------------------- # run the workflow-state polling test workflow TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach --set="UPSTREAM='${UPSTREAM}'" \ "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge #------------------------------------------------------------------------------- # clean up the upstream workflow # just in case (expect error message here, but exit 0): cylc stop --now "${UPSTREAM}" --max-polls=20 --interval=2 >'/dev/null' 2>&1 purge "${UPSTREAM}" exit cylc-flow-8.6.4/tests/functional/workflow-state/06a-noformat.t0000775000175000017500000000304315202510242024502 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test "cylc workflow-state" cycle point format conversion, when the target workflow # sets no explicit cycle point format, and the CLI does (the reverse of 06.t). . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True # (Use default cycle point format) [scheduling] initial cycle point = 20100101T0000Z [[graph]] R1 = foo [runtime] [[foo]] script = true __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME=${TEST_NAME_BASE}-cli-poll run_ok "${TEST_NAME}" cylc workflow-state "${WORKFLOW_NAME}//2010-01-01T00+00" contains_ok "${TEST_NAME}.stdout" <<__OUT__ 20100101T0000Z/foo:succeeded __OUT__ purge cylc-flow-8.6.4/tests/functional/workflow-state/00-polling.t0000664000175000017500000001004215202510242024144 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow-state/polling test workflow # The test workflow is in polling/; it depends on another workflow in upstream/ . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 7 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'polling' #------------------------------------------------------------------------------- # copy the upstream workflow to the test directory and install it cp -r "${TEST_SOURCE_DIR}/upstream" "${TEST_DIR}/" # use full range of characters in the workflow-to-be-polled name: UPSTREAM="${WORKFLOW_NAME}-up_stre.am" cylc install "${TEST_DIR}/upstream" --workflow-name="${UPSTREAM}" --no-run-name #------------------------------------------------------------------------------- # validate both workflows as tests TEST_NAME="${TEST_NAME_BASE}-validate-upstream" run_ok "${TEST_NAME}" cylc validate --debug "${UPSTREAM}" TEST_NAME=${TEST_NAME_BASE}-validate-polling-y run_fail "${TEST_NAME}" \ cylc validate --set="UPSTREAM='${UPSTREAM}'" --set="OUTPUT=':y'" "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowConfigError: Polling task "l-mess" must configure a target status or output message in \ the graph (:y) or task definition (message = "the quick brown fox") but not both. __ERR__ TEST_NAME=${TEST_NAME_BASE}-validate-polling run_ok "${TEST_NAME}" \ cylc validate --debug --set="UPSTREAM='${UPSTREAM}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # run the upstream workflow and detach (not a test) cylc play "${UPSTREAM}" #------------------------------------------------------------------------------- # check auto-generated task script for lbad cylc config -d \ --set="UPSTREAM='${UPSTREAM}'" -i '[runtime][lbad]script' "${WORKFLOW_NAME}" \ >'lbad.script' cmp_ok 'lbad.script' << __END__ echo cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/bad:failed --interval=2 --max-polls=20 cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/bad:failed --interval=2 --max-polls=20 __END__ # check auto-generated task script for l-good cylc config -d \ --set="UPSTREAM='${UPSTREAM}'" -i '[runtime][l-good]script' "${WORKFLOW_NAME}" \ >'l-good.script' cmp_ok 'l-good.script' << __END__ echo cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/good-stuff:succeeded --interval=2 --max-polls=20 cylc workflow-state ${UPSTREAM}//\$CYLC_TASK_CYCLE_POINT/good-stuff:succeeded --interval=2 --max-polls=20 __END__ #------------------------------------------------------------------------------- # run the workflow-state polling test workflow TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach \ --set="UPSTREAM='${UPSTREAM}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge #------------------------------------------------------------------------------- # clean up the upstream workflow # just in case (expect error message here, but exit 0): cylc stop --now "${UPSTREAM}" --max-polls=20 --interval=2 >'/dev/null' 2>&1 purge "${UPSTREAM}" exit cylc-flow-8.6.4/tests/functional/workflow-state/02-validate-blank-command-scripting.t0000775000175000017500000000251615202510242031006 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate blank script in automatic workflow polling task. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit 0 cylc-flow-8.6.4/tests/functional/workflow-state/integer/0000775000175000017500000000000015202510242023536 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/integer/flow.cylc0000664000175000017500000000043015202510242025356 0ustar alastairalastair[scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 [[graph]] P1 = """ foo:x => bar """ [runtime] [[foo]] script = cylc message "hello" [[[outputs]]] x = "hello" [[bar]] cylc-flow-8.6.4/tests/functional/workflow-state/10-backcompat.t0000775000175000017500000000334615202510242024621 0ustar alastairalastair#!/usr/bin/env bash # 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 . . "$(dirname "$0")/test_header" set_test_number 8 install_workflow "${TEST_NAME_BASE}" backcompat # create Cylc 7 DB run_ok "create-db" sqlite3 "${WORKFLOW_RUN_DIR}/log/db" < schema-1.sql TEST_NAME="${TEST_NAME_BASE}_compat_1" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:succeeded 2051/bar:succeeded __END__ # recreate Cylc 7 DB with one NULL status rm "${WORKFLOW_RUN_DIR}/log/db" run_ok "create-db" sqlite3 "${WORKFLOW_RUN_DIR}/log/db" < schema-2.sql TEST_NAME="${TEST_NAME_BASE}_compat_2" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:succeeded __END__ # Cylc 7 DB only contains custom outputs TEST_NAME="${TEST_NAME_BASE}_outputs" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 --messages "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:{'x': 'the quick brown fox'} __END__ purge cylc-flow-8.6.4/tests/functional/workflow-state/07-message2/0000775000175000017500000000000015202510242024033 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/07-message2/flow.cylc0000775000175000017500000000050415202510242025660 0ustar alastairalastair[scheduler] cycle point format = %Y [scheduling] initial cycle point = 2010 final cycle point = 2012 [[graph]] P1Y = "foo:x => bar" [runtime] [[foo]] script = cylc message "the quick brown fox" [[[outputs]]] x = "the quick brown fox" [[bar]] script = true cylc-flow-8.6.4/tests/functional/workflow-state/03-options.t0000775000175000017500000000255715202510242024215 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test running of cylc workflow-state with various CLI options . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 install_workflow "${TEST_NAME_BASE}" options #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE} workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge exit 0 cylc-flow-8.6.4/tests/functional/workflow-state/output/0000775000175000017500000000000015202510242023441 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/output/reference.log0000664000175000017500000000021315202510242026076 0ustar alastairalastairInitial point: 20100101T0000Z Final point: None 20100101T0000Z/t1 -triggered off [] 20100101T0000Z/t2 -triggered off ['20100101T0000Z/t1'] cylc-flow-8.6.4/tests/functional/workflow-state/output/flow.cylc0000664000175000017500000000041615202510242025265 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T0000Z [[graph]] R1 = t1:out1 => t2 [runtime] [[t1]] script = cylc message "hello" [[[outputs]]] out1 = "hello" [[t2]] script = true cylc-flow-8.6.4/tests/functional/workflow-state/09-datetime.t0000775000175000017500000000756615202510242024331 0ustar alastairalastair#!/usr/bin/env bash # 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 . . "$(dirname "$0")/test_header" set_test_number 24 install_workflow "${TEST_NAME_BASE}" datetime # run one cycle TEST_NAME="${TEST_NAME_BASE}_run_1" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach --stopcp=2051 "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}_check_1_status" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2052/foo:waiting 2051/foo:succeeded 2051/bar:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_check_1_status_old_fmt" run_ok "${TEST_NAME}" cylc workflow-state --old-format --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ foo, 2052, waiting foo, 2051, succeeded bar, 2051, succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_check_1_outputs" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 --triggers "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:{'submitted': 'submitted', 'started': 'started', 'succeeded': 'succeeded', 'x': 'hello'} 2052/foo:{} 2051/bar:{'submitted': 'submitted', 'started': 'started', 'succeeded': 'succeeded'} __END__ TEST_NAME="${TEST_NAME_BASE}_poll_fail" run_fail "${TEST_NAME}" cylc workflow-state --max-polls=2 --interval=1 "${WORKFLOW_NAME}//2052/foo:succeeded" contains_ok "${TEST_NAME}.stderr" <<__END__ ERROR - failed after 2 polls __END__ # finish the run TEST_NAME="${TEST_NAME_BASE}_run_2" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}_check_1_status_2" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:succeeded 2052/foo:succeeded 2051/bar:succeeded 2052/bar:succeeded 2052/foo:succeeded(flows=2) 2052/bar:succeeded(flows=2) __END__ TEST_NAME="${TEST_NAME_BASE}_check_1_status_3" run_ok "${TEST_NAME}" cylc workflow-state --flow=2 --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2052/foo:succeeded(flows=2) 2052/bar:succeeded(flows=2) __END__ TEST_NAME="${TEST_NAME_BASE}_check_1_wildcard" run_ok "${TEST_NAME}" cylc workflow-state --flow=1 --max-polls=1 "${WORKFLOW_NAME}//*/foo" contains_ok "${TEST_NAME}.stdout" <<__END__ 2051/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_poll_succeed" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//2052/foo:succeeded" contains_ok "${TEST_NAME}.stdout" <<__END__ 2052/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_datetime_offset" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//2051/foo:succeeded" --offset=P1Y contains_ok "${TEST_NAME}.stdout" <<__END__ 2052/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_datetime_format" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//20510101T0000Z/foo:succeeded" --offset=P1Y contains_ok "${TEST_NAME}.stdout" <<__END__ 2052/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_bad_point" run_fail "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//205/foo:succeeded" contains_ok "${TEST_NAME}.stderr" <<__END__ InputError: Cycle point "205" is not compatible with DB point format "CCYY" __END__ purge cylc-flow-8.6.4/tests/functional/workflow-state/05-output.t0000775000175000017500000000231215202510242024051 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test cylc workflow-state for outputs (as opposed to statuses) . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" output TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME=${TEST_NAME_BASE}-cli-check run_ok "${TEST_NAME}" cylc workflow-state "${WORKFLOW_NAME}//20100101T0000Z/t1:out1" --triggers --max-polls=1 purge cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/0000775000175000017500000000000015202510242023452 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/11-multi/c8a.sql0000664000175000017500000001055015202510242024647 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE absolute_outputs(cycle TEXT, name TEXT, output TEXT); CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('1','foo','"poll_timer"','["tuple", [[1, "running"]]]','[900.0]',1,'900.0','1717563116.69952'); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "submission-retry"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "execution-retry"]','null','[]',0,NULL,NULL); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:36:56+12:00',1,'submitted',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:36:56+12:00',1,'started',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:36:56+12:00',1,'x','the quick brown'); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:36:57+12:00',1,'succeeded',''); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, flow_nums TEXT, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, platform_name TEXT, job_runner_name TEXT, job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); INSERT INTO task_jobs VALUES('1','foo',1,'[1]',0,1,'2024-06-05T16:36:55+12:00','2024-06-05T16:36:56+12:00',0,'2024-06-05T16:36:56+12:00','2024-06-05T16:36:56+12:00',NULL,0,'localhost','background','21511'); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, flow_nums TEXT, outputs TEXT, PRIMARY KEY(cycle, name, flow_nums)); INSERT INTO task_outputs VALUES('1','foo','[1]','["submitted", "started", "succeeded", "the quick brown"]'); CREATE TABLE task_pool(cycle TEXT, name TEXT, flow_nums TEXT, status TEXT, is_held INTEGER, PRIMARY KEY(cycle, name, flow_nums)); CREATE TABLE task_prerequisites(cycle TEXT, name TEXT, flow_nums TEXT, prereq_name TEXT, prereq_cycle TEXT, prereq_output TEXT, satisfied TEXT, PRIMARY KEY(cycle, name, flow_nums, prereq_name, prereq_cycle, prereq_output)); CREATE TABLE task_states(name TEXT, cycle TEXT, flow_nums TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, flow_wait INTEGER, is_manual_submit INTEGER, PRIMARY KEY(name, cycle, flow_nums)); INSERT INTO task_states VALUES('foo','1','[1]','2024-06-05T16:36:55+12:00','2024-06-05T16:36:57+12:00',1,'succeeded',0,0); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE tasks_to_hold(name TEXT, cycle TEXT); CREATE TABLE workflow_flows(flow_num INTEGER, start_time TEXT, description TEXT, PRIMARY KEY(flow_num)); INSERT INTO workflow_flows VALUES(1,'2024-06-05T16:36:55','original flow from 1'); CREATE TABLE workflow_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO workflow_params VALUES('uuid_str','cabb2bd8-bb36-4c7a-9c51-d2b1d456bc4e'); INSERT INTO workflow_params VALUES('cylc_version','8.3.0.dev'); INSERT INTO workflow_params VALUES('UTC_mode','0'); INSERT INTO workflow_params VALUES('n_restart','0'); INSERT INTO workflow_params VALUES('cycle_point_format',NULL); INSERT INTO workflow_params VALUES('is_paused','0'); INSERT INTO workflow_params VALUES('stop_clock_time',NULL); INSERT INTO workflow_params VALUES('stop_task',NULL); INSERT INTO workflow_params VALUES('icp',NULL); INSERT INTO workflow_params VALUES('fcp',NULL); INSERT INTO workflow_params VALUES('startcp',NULL); INSERT INTO workflow_params VALUES('stopcp',NULL); INSERT INTO workflow_params VALUES('run_mode',NULL); INSERT INTO workflow_params VALUES('cycle_point_tz','+1200'); CREATE TABLE workflow_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); COMMIT; cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/upstream/0000775000175000017500000000000015202510242025312 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/11-multi/upstream/suite.rc0000664000175000017500000000070115202510242026767 0ustar alastairalastair# Run this with Cylc 7, 8 (pre-8.3.0), and 8 (8.3.0+) # to generate DBs for workflow state checks. # (The task_outputs table is different in each case). [scheduling] cycling mode = integer initial cycle point = 1 [[dependencies]] [[[R1]]] graph = """ foo """ [runtime] [[foo]] script = "cylc message - 'the quick brown'" [[[outputs]]] x = "the quick brown" cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/c7.sql0000664000175000017500000000721215202510242024506 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO suite_params VALUES('uuid_str','814ef90e-31a2-45e7-904b-fb3c6dcb87a9'); INSERT INTO suite_params VALUES('run_mode','live'); INSERT INTO suite_params VALUES('cylc_version','7.9.9'); INSERT INTO suite_params VALUES('UTC_mode','0'); INSERT INTO suite_params VALUES('cycle_point_tz','+1200'); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, user_at_host TEXT, batch_sys_name TEXT, batch_sys_job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); INSERT INTO task_jobs VALUES('1','foo',1,0,1,'2024-06-05T16:31:01+12:00','2024-06-05T16:31:02+12:00',0,'2024-06-05T16:31:02+12:00','2024-06-05T16:31:02+12:00',NULL,0,'NIWA-1022450.niwa.local','background','19328'); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE broadcast_states_checkpoints(id INTEGER, point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(id, point, namespace, key)); CREATE TABLE checkpoint_id(id INTEGER, time TEXT, event TEXT, PRIMARY KEY(id)); INSERT INTO checkpoint_id VALUES(0,'2024-06-05T16:31:02+12:00','latest'); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); CREATE TABLE suite_params_checkpoints(id INTEGER, key TEXT, value TEXT, PRIMARY KEY(id, key)); CREATE TABLE task_pool_checkpoints(id INTEGER, cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(id, cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, outputs TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_outputs VALUES('1','foo','{"x": "the quick brown"}'); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE task_states(name TEXT, cycle TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, PRIMARY KEY(name, cycle)); INSERT INTO task_states VALUES('foo','1','2024-06-05T16:31:01+12:00','2024-06-05T16:31:02+12:00',1,'succeeded'); CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:31:02+12:00',1,'submitted',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:31:02+12:00',1,'started',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:31:02+12:00',1,'x','the quick brown'); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:31:02+12:00',1,'succeeded',''); CREATE TABLE suite_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE task_pool(cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_pool VALUES('1','foo',1,'succeeded',NULL); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); COMMIT; cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/c8b.sql0000664000175000017500000001062215202510242024650 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE absolute_outputs(cycle TEXT, name TEXT, output TEXT); CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('1','foo','"poll_timer"','["tuple", [[1, "running"]]]','[900.0]',1,'900.0','1717562943.77014'); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "submission-retry"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('1','foo','["try_timers", "execution-retry"]','null','[]',0,NULL,NULL); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:34:03+12:00',1,'submitted',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:34:03+12:00',1,'started',''); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:34:03+12:00',1,'x','the quick brown'); INSERT INTO task_events VALUES('foo','1','2024-06-05T16:34:04+12:00',1,'succeeded',''); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, flow_nums TEXT, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, platform_name TEXT, job_runner_name TEXT, job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); INSERT INTO task_jobs VALUES('1','foo',1,'[1]',0,1,'2024-06-05T16:34:02+12:00','2024-06-05T16:34:03+12:00',0,'2024-06-05T16:34:03+12:00','2024-06-05T16:34:03+12:00',NULL,0,'localhost','background','20985'); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, flow_nums TEXT, outputs TEXT, PRIMARY KEY(cycle, name, flow_nums)); INSERT INTO task_outputs VALUES('1','foo','[1]','{"submitted": "submitted", "started": "started", "succeeded": "succeeded", "x": "the quick brown"}'); CREATE TABLE task_pool(cycle TEXT, name TEXT, flow_nums TEXT, status TEXT, is_held INTEGER, PRIMARY KEY(cycle, name, flow_nums)); CREATE TABLE task_prerequisites(cycle TEXT, name TEXT, flow_nums TEXT, prereq_name TEXT, prereq_cycle TEXT, prereq_output TEXT, satisfied TEXT, PRIMARY KEY(cycle, name, flow_nums, prereq_name, prereq_cycle, prereq_output)); CREATE TABLE task_states(name TEXT, cycle TEXT, flow_nums TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, flow_wait INTEGER, is_manual_submit INTEGER, PRIMARY KEY(name, cycle, flow_nums)); INSERT INTO task_states VALUES('foo','1','[1]','2024-06-05T16:34:02+12:00','2024-06-05T16:34:04+12:00',1,'succeeded',0,0); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE tasks_to_hold(name TEXT, cycle TEXT); CREATE TABLE workflow_flows(flow_num INTEGER, start_time TEXT, description TEXT, PRIMARY KEY(flow_num)); INSERT INTO workflow_flows VALUES(1,'2024-06-05T16:34:02','original flow from 1'); CREATE TABLE workflow_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO workflow_params VALUES('uuid_str','4185a45a-8faa-491f-ad35-2d221e780efa'); INSERT INTO workflow_params VALUES('cylc_version','8.3.0.dev'); INSERT INTO workflow_params VALUES('UTC_mode','0'); INSERT INTO workflow_params VALUES('n_restart','0'); INSERT INTO workflow_params VALUES('cycle_point_format',NULL); INSERT INTO workflow_params VALUES('is_paused','0'); INSERT INTO workflow_params VALUES('stop_clock_time',NULL); INSERT INTO workflow_params VALUES('stop_task',NULL); INSERT INTO workflow_params VALUES('icp',NULL); INSERT INTO workflow_params VALUES('fcp',NULL); INSERT INTO workflow_params VALUES('startcp',NULL); INSERT INTO workflow_params VALUES('stopcp',NULL); INSERT INTO workflow_params VALUES('run_mode',NULL); INSERT INTO workflow_params VALUES('cycle_point_tz','+1200'); CREATE TABLE workflow_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); COMMIT; cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/reference.log0000664000175000017500000000113115202510242026107 0ustar alastairalastair1/bar1 -triggered off [] in flow 1 1/qux2 -triggered off [] in flow 1 1/bar2 -triggered off [] in flow 1 1/baz2 -triggered off [] in flow 1 1/f4 -triggered off [] in flow 1 1/f1 -triggered off [] in flow 1 1/f2 -triggered off [] in flow 1 1/f3 -triggered off [] in flow 1 1/f5 -triggered off [] in flow 1 1/x1 -triggered off [] in flow 1 1/f6 -triggered off [] in flow 1 1/f7 -triggered off [] in flow 1 1/x2 -triggered off [] in flow 1 1/g4 -triggered off ['1/baz2'] in flow 1 1/g2 -triggered off ['1/bar2'] in flow 1 1/g7 -triggered off ['1/qux2'] in flow 1 1/g1 -triggered off ['1/bar1'] in flow 1 cylc-flow-8.6.4/tests/functional/workflow-state/11-multi/flow.cylc0000664000175000017500000000442015202510242025275 0ustar alastairalastair#!Jinja2 {# alt-cylc-run-dir default for easy validation #} {% set ALT = ALT | default("alt") %} [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 [[xtriggers]] # Cylc 7 back compat z1 = suite_state(c7, foo, 1, offset=P0, cylc_run_dir={{ALT}}):PT1S # status=succeeded z2 = suite_state(c7, foo, 1, offset=P0, message="the quick brown", cylc_run_dir={{ALT}}):PT1S # Cylc 7 xtrigger, Cylc 8 DB a1 = suite_state(c8b, foo, 1, offset=P0, cylc_run_dir={{ALT}}):PT1S # status=succeeded a2 = suite_state(c8b, foo, 1, offset=P0, message="the quick brown", cylc_run_dir={{ALT}}):PT1S # Cylc 8 back compat (pre-8.3.0) b1 = workflow_state(c8a, foo, 1, offset=P0, status=succeeded, cylc_run_dir={{ALT}}):PT1S b2 = workflow_state(c8a, foo, 1, offset=P0, message="the quick brown", cylc_run_dir={{ALT}}):PT1S # Cylc 8 new (from 8.3.0) c1 = workflow_state(c8b//1/foo, offset=P0, alt_cylc_run_dir={{ALT}}):PT1S c2 = workflow_state(c8b//1/foo:succeeded, offset=P0, alt_cylc_run_dir={{ALT}}):PT1S c3 = workflow_state(c8b//1/foo:x, offset=P0, alt_cylc_run_dir={{ALT}}, is_trigger=True):PT1S c4 = workflow_state(c8b//1/foo:"the quick brown", offset=P0, is_message=True, alt_cylc_run_dir={{ALT}}):PT1S [[graph]] R1 = """ # Deprecated workflow-state polling tasks. # (does not support %(suite_name)s templates or offsets # or output triggers - just messages) # status bar1 => g1 bar2 => g2 # output baz2 => g4 # message given in task definition qux2 => g7 # message given in task definition @z1 => x1 @z2 => x2 @a1 => f1 @a2 => f2 @b1 => f3 @b2 => f4 @c1 => f5 @c2 => f6 @c3 => f7 """ [runtime] [[bar1, bar2]] [[[workflow state polling]]] alt-cylc-run-dir = {{ALT}} [[qux2, baz2]] [[[workflow state polling]]] message = "the quick brown" alt-cylc-run-dir = {{ALT}} [[x1, x2]] [[f1, f2, f3, f4, f5, f6, f7]] [[g1, g2, g4, g7]] cylc-flow-8.6.4/tests/functional/workflow-state/07-message2.t0000775000175000017500000000254715202510242024233 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Originally (Cylc 7): test workflow-state query on a waiting task - GitHub #2440. # Now (Cylc 8): test result of a failed workflow-state query. . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME=${TEST_NAME_BASE}-query run_fail "${TEST_NAME}" cylc workflow-state "${WORKFLOW_NAME}//2013/foo:x" --triggers --max-polls=1 grep_ok "failed after 1 polls" "${TEST_NAME}.stderr" purge cylc-flow-8.6.4/tests/functional/workflow-state/test_header0000777000175000017500000000000015202510242030416 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/02-validate-blank-command-scripting/0000775000175000017500000000000015202510242030612 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/02-validate-blank-command-scripting/flow.cylc0000664000175000017500000000050515202510242032435 0ustar alastairalastair#!jinja2 [meta] title=Test validation of blank script in automatic workflow polling task [scheduling] [[graph]] R1=eat=>full [runtime] [[root]] script=true [[eat]] script= [[[workflow state polling]]] interval=PT2S max-polls=20 [[full]] cylc-flow-8.6.4/tests/functional/workflow-state/datetime/0000775000175000017500000000000015202510242023675 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/datetime/flow.cylc0000664000175000017500000000101615202510242025516 0ustar alastairalastair[scheduler] cycle point format = CCYY [scheduling] initial cycle point = 2051 final cycle point = 2052 [[graph]] P1Y = """ foo:x => bar """ [runtime] [[foo]] script = cylc message "hello" [[[outputs]]] x = "hello" [[bar]] script = """ if (( CYLC_TASK_CYCLE_POINT == 2052 )) && (( CYLC_TASK_SUBMIT_NUMBER == 1 )) then cylc trigger --flow=new $CYLC_WORKFLOW_ID//2052/foo fi """ cylc-flow-8.6.4/tests/functional/workflow-state/polling/0000775000175000017500000000000015202510242023545 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/polling/reference.log0000664000175000017500000000022215202510242026202 0ustar alastairalastairInitial point: 1 Final point: 1 1/l-good -triggered off [] 1/lbad -triggered off [] 1/l-mess -triggered off [] 1/done -triggered off ['1/l-mess'] cylc-flow-8.6.4/tests/functional/workflow-state/polling/flow.cylc0000664000175000017500000000123415202510242025370 0ustar alastairalastair#!jinja2 {# e.g. set OUTPUT = ":x" #} {% set OUTPUT = OUTPUT | default("") %} [meta] title = "polls for success and failure tasks in another workflow" [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """ l-good<{{UPSTREAM}}::good-stuff> & lbad<{{UPSTREAM}}::bad:fail> l-mess<{{UPSTREAM}}::messenger{{OUTPUT}}> => done """ [runtime] [[l-good,lbad]] [[[workflow state polling]]] interval = PT2S max-polls = 20 [[l-mess]] [[[workflow state polling]]] interval = PT2S max-polls = 20 message = "the quick brown fox" cylc-flow-8.6.4/tests/functional/workflow-state/06-format.t0000775000175000017500000000337315202510242024012 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test "cylc workflow-state" cycle point format conversion, when the target workflow # sets an explicit cycle point format, and the CLI does not. . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True cycle point format = CCYY-MM-DD [scheduling] initial cycle point = 20100101 [[graph]] R1 = foo [runtime] [[foo]] script = true __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME=${TEST_NAME_BASE}-cli-poll run_ok "${TEST_NAME}" cylc workflow-state "${WORKFLOW_NAME}//20100101T0000Z/foo:succeeded" --max-polls=1 contains_ok "${TEST_NAME}.stdout" <<__OUT__ 2010-01-01/foo:succeeded __OUT__ TEST_NAME=${TEST_NAME_BASE}-cli-dump run_ok "${TEST_NAME}" cylc workflow-state --old-format "${WORKFLOW_NAME}//20100101T0000Z" --max-polls=1 contains_ok "${TEST_NAME}.stdout" <<__OUT__ foo, 2010-01-01, succeeded __OUT__ purge cylc-flow-8.6.4/tests/functional/workflow-state/11-multi.t0000664000175000017500000001105715202510242023643 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test all kinds of workflow-state DB checking. # shellcheck disable=SC2086 . "$(dirname "$0")/test_header" set_test_number 42 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Create Cylc 7, 8 (pre-8.3.0), and 8(8.3.0+) DBs for workflow-state checking. DBDIR="${WORKFLOW_RUN_DIR}/dbs" for x in c7 c8a c8b; do mkdir -p "${DBDIR}/${x}/log" sqlite3 "${DBDIR}/${x}/log/db" < "${x}.sql" done run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" --set="ALT=\"${DBDIR}\"" grep_ok \ "WARNING - (8.3.0) Deprecated function signature used for workflow_state xtrigger was automatically upgraded" \ "${TEST_NAME_BASE}-validate.stderr" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play "${WORKFLOW_NAME}" --set="ALT=\"${DBDIR}\"" \ --reference-test --debug --no-detach # Single poll. CMD="cylc workflow-state --run-dir=$DBDIR --max-polls=1" # Content of the c8b DB: # "select * from task_outputs" # 1|foo|[1]|{"submitted": "submitted", "started": "started", "succeeded": "succeeded", "x": "the quick brown"} # "select * from task_states" # foo|1|[1]|2024-06-05T16:34:02+12:00|2024-06-05T16:34:04+12:00|1|succeeded|0|0 #--------------- # Test the new-format command line (8.3.0+). T=${TEST_NAME_BASE}-cli-c8b run_ok "${T}-1" $CMD c8b run_ok "${T}-2" $CMD c8b//1 run_ok "${T}-3" $CMD c8b//1/foo run_fail "${T}-4" $CMD c8b//1/foo:waiting run_ok "${T}-4" $CMD c8b//1/foo:succeeded run_ok "${T}-5" $CMD "c8b//1/foo:the quick brown" --messages run_ok "${T}-6" $CMD "c8b//1/foo:x" --triggers run_ok "${T}-8" $CMD c8b//1 run_ok "${T}-9" $CMD c8b//1:succeeded run_fail "${T}-3" $CMD c8b//1/foo:failed run_fail "${T}-5" $CMD "c8b//1/foo:the quick brown" --triggers run_fail "${T}-5" $CMD "c8b//1/foo:x" --messages run_fail "${T}-1" $CMD c8b//1:failed run_fail "${T}-1" $CMD c8b//2 run_fail "${T}-1" $CMD c8b//2:failed #--------------- T=${TEST_NAME_BASE}-cli-c8a run_ok "${T}-1" $CMD "c8a//1/foo:the quick brown" --messages run_ok "${T}-2" $CMD "c8a//1/foo:the quick brown" --triggers # OK for 8.0 <= 8.3 run_fail "${T}-3" $CMD "c8a//1/foo:x" --triggers # not possible for 8.0 <= 8.3 #--------------- T=${TEST_NAME_BASE}-cli-c7 run_ok "${T}-1" $CMD "c7//1/foo:the quick brown" --messages run_fail "${T}-2" $CMD "c7//1/foo:the quick brown" --triggers run_ok "${T}-3" $CMD "c7//1/foo:x" --triggers #--------------- # Test the old-format command line (pre-8.3.0). T=${TEST_NAME_BASE}-cli-8b-compat run_ok "${T}-1" $CMD c8b run_ok "${T}-2" $CMD c8b --point=1 run_ok "${T}-3" $CMD c8b --point=1 --task=foo run_ok "${T}-4" $CMD c8b --point=1 --task=foo --status=succeeded run_ok "${T}-5" $CMD c8b --point=1 --task=foo --message="the quick brown" run_ok "${T}-6" $CMD c8b --point=1 --task=foo --output="the quick brown" run_fail "${T}-7" $CMD c8b --point=1 --task=foo --status=failed run_fail "${T}-8" $CMD c8b --point=1 --task=foo --message="x" run_fail "${T}-9" $CMD c8b --point=1 --task=foo --output="x" run_fail "${T}-10" $CMD c8b --point=2 run_fail "${T}-11" $CMD c8b --point=2 --task=foo --status="succeeded" #--------------- T=${TEST_NAME_BASE}-bad-cli TEST_NAME="${T}-1" run_fail "$TEST_NAME" $CMD c8b --status=succeeded --message="the quick brown" cmp_ok "${TEST_NAME}.stderr" <<__ERR__ InputError: set --status or --message, not both. __ERR__ TEST_NAME="${T}-2" run_fail "$TEST_NAME" $CMD c8b --task-point --point=1 cmp_ok "${TEST_NAME}.stderr" <<__ERR__ InputError: set --task-point or --point=CYCLE, not both. __ERR__ TEST_NAME="${T}-3" run_fail "$TEST_NAME" $CMD c8b --task-point cmp_ok "${TEST_NAME}.stderr" << "__ERR__" InputError: --task-point: $CYLC_TASK_CYCLE_POINT is not defined __ERR__ export CYLC_TASK_CYCLE_POINT=1 TEST_NAME="${T}-3" run_ok "$TEST_NAME" $CMD c8b --task-point purge cylc-flow-8.6.4/tests/functional/workflow-state/backcompat/0000775000175000017500000000000015202510242024205 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/backcompat/schema-1.sql0000664000175000017500000001123615202510242026327 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO suite_params VALUES('uuid_str','0d0bf7e8-4543-4aeb-8bc6-397e3a03ee19'); INSERT INTO suite_params VALUES('run_mode','live'); INSERT INTO suite_params VALUES('cylc_version','7.9.9'); INSERT INTO suite_params VALUES('UTC_mode','0'); INSERT INTO suite_params VALUES('cycle_point_format','CCYY'); INSERT INTO suite_params VALUES('cycle_point_tz','+1200'); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, user_at_host TEXT, batch_sys_name TEXT, batch_sys_job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); INSERT INTO task_jobs VALUES('2051','foo',1,0,1,'2024-05-30T14:11:40+12:00','2024-05-30T14:11:40+12:00',0,'2024-05-30T14:11:40+12:00','2024-05-30T14:11:40+12:00',NULL,0,'NIWA-1022450.niwa.local','background','12272'); INSERT INTO task_jobs VALUES('2051','bar',1,0,1,'2024-05-30T14:11:42+12:00','2024-05-30T14:11:42+12:00',0,'2024-05-30T14:11:42+12:00','2024-05-30T14:11:42+12:00',NULL,0,'NIWA-1022450.niwa.local','background','12327'); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE broadcast_states_checkpoints(id INTEGER, point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(id, point, namespace, key)); CREATE TABLE checkpoint_id(id INTEGER, time TEXT, event TEXT, PRIMARY KEY(id)); INSERT INTO checkpoint_id VALUES(0,'2024-05-30T14:11:43+12:00','latest'); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); INSERT INTO inheritance VALUES('bar','["bar", "root"]'); CREATE TABLE suite_params_checkpoints(id INTEGER, key TEXT, value TEXT, PRIMARY KEY(id, key)); CREATE TABLE task_pool_checkpoints(id INTEGER, cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(id, cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, outputs TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_outputs VALUES('2051','foo','{"x": "the quick brown fox"}'); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE task_states(name TEXT, cycle TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, PRIMARY KEY(name, cycle)); INSERT INTO task_states VALUES('foo','2051','2024-05-30T14:11:40+12:00','2024-05-30T14:11:41+12:00',1,'succeeded'); INSERT INTO task_states VALUES('bar','2051','2024-05-30T14:11:40+12:00','2024-05-30T14:11:43+12:00',1,'succeeded'); CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'submitted',''); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'started',''); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'x','the quick brown fox'); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'succeeded',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'submitted',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'started',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'succeeded',''); CREATE TABLE suite_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE task_pool(cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_pool VALUES('2051','foo',1,'succeeded',NULL); INSERT INTO task_pool VALUES('2051','bar',1,'succeeded',NULL); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('2051','foo','["try_timers", "retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','foo','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','bar','["try_timers", "retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','bar','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); COMMIT; cylc-flow-8.6.4/tests/functional/workflow-state/backcompat/schema-2.sql0000664000175000017500000001122715202510242026330 0ustar alastairalastairPRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key)); INSERT INTO suite_params VALUES('uuid_str','0d0bf7e8-4543-4aeb-8bc6-397e3a03ee19'); INSERT INTO suite_params VALUES('run_mode','live'); INSERT INTO suite_params VALUES('cylc_version','7.9.9'); INSERT INTO suite_params VALUES('UTC_mode','0'); INSERT INTO suite_params VALUES('cycle_point_format','CCYY'); INSERT INTO suite_params VALUES('cycle_point_tz','+1200'); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, user_at_host TEXT, batch_sys_name TEXT, batch_sys_job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); INSERT INTO task_jobs VALUES('2051','foo',1,0,1,'2024-05-30T14:11:40+12:00','2024-05-30T14:11:40+12:00',0,'2024-05-30T14:11:40+12:00','2024-05-30T14:11:40+12:00',NULL,0,'NIWA-1022450.niwa.local','background','12272'); INSERT INTO task_jobs VALUES('2051','bar',1,0,1,'2024-05-30T14:11:42+12:00','2024-05-30T14:11:42+12:00',0,'2024-05-30T14:11:42+12:00','2024-05-30T14:11:42+12:00',NULL,0,'NIWA-1022450.niwa.local','background','12327'); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE broadcast_states_checkpoints(id INTEGER, point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(id, point, namespace, key)); CREATE TABLE checkpoint_id(id INTEGER, time TEXT, event TEXT, PRIMARY KEY(id)); INSERT INTO checkpoint_id VALUES(0,'2024-05-30T14:11:43+12:00','latest'); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); INSERT INTO inheritance VALUES('root','["root"]'); INSERT INTO inheritance VALUES('foo','["foo", "root"]'); INSERT INTO inheritance VALUES('bar','["bar", "root"]'); CREATE TABLE suite_params_checkpoints(id INTEGER, key TEXT, value TEXT, PRIMARY KEY(id, key)); CREATE TABLE task_pool_checkpoints(id INTEGER, cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(id, cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, outputs TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_outputs VALUES('2051','foo','{"x": "the quick brown fox"}'); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE task_states(name TEXT, cycle TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, PRIMARY KEY(name, cycle)); INSERT INTO task_states VALUES('foo','2051','2024-05-30T14:11:40+12:00','2024-05-30T14:11:41+12:00',1,'succeeded'); INSERT INTO task_states VALUES('bar','2051','2024-05-30T14:11:40+12:00','2024-05-30T14:11:43+12:00',1,NULL); CREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'submitted',''); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'started',''); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'x','the quick brown fox'); INSERT INTO task_events VALUES('foo','2051','2024-05-30T14:11:41+12:00',1,'succeeded',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'submitted',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'started',''); INSERT INTO task_events VALUES('bar','2051','2024-05-30T14:11:43+12:00',1,'succeeded',''); CREATE TABLE suite_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE task_pool(cycle TEXT, name TEXT, spawned INTEGER, status TEXT, hold_swap TEXT, PRIMARY KEY(cycle, name)); INSERT INTO task_pool VALUES('2051','foo',1,'succeeded',NULL); INSERT INTO task_pool VALUES('2051','bar',1,'succeeded',NULL); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); INSERT INTO task_action_timers VALUES('2051','foo','["try_timers", "retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','foo','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','bar','["try_timers", "retrying"]','null','[]',0,NULL,NULL); INSERT INTO task_action_timers VALUES('2051','bar','["try_timers", "submit-retrying"]','null','[]',0,NULL,NULL); COMMIT; cylc-flow-8.6.4/tests/functional/workflow-state/backcompat/suite.rc0000664000175000017500000000047115202510242025666 0ustar alastairalastair[cylc] cycle point format = CCYY [scheduling] initial cycle point = 2051 [[dependencies]] [[[R1]]] graph = """ foo:x => bar """ [runtime] [[foo]] script = "cylc message 'the quick brown fox'" [[[outputs]]] x = "the quick brown fox" [[bar]] cylc-flow-8.6.4/tests/functional/workflow-state/options/0000775000175000017500000000000015202510242023574 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-state/options/reference.log0000664000175000017500000000065715202510242026245 0ustar alastairalastairInitial point: 20100101T0000Z Final point: 20100103T0000Z 20100101T0000Z/foo -triggered off ['20091231T0000Z/foo'] 20100102T0000Z/foo -triggered off ['20100101T0000Z/foo'] 20100103T0000Z/foo -triggered off ['20100102T0000Z/foo'] 20100102T0000Z/env_polling -triggered off ['20100102T0000Z/foo'] 20100102T0000Z/offset_polling -triggered off ['20100102T0000Z/foo'] 20100102T0000Z/offset_polling2 -triggered off ['20100103T0000Z/foo'] cylc-flow-8.6.4/tests/functional/workflow-state/options/flow.cylc0000664000175000017500000000131015202510242025412 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True [scheduling] initial cycle point = 20100101T00Z final cycle point = 20100103T00Z [[graph]] T00 = "foo[-P1D] => foo" R1/20100102T00Z = """ foo => env_polling foo => offset_polling foo[+P1D] => offset_polling2 """ [runtime] [[foo]] script = true [[env_polling]] script = cylc workflow-state ${CYLC_WORKFLOW_ID}//$CYLC_TASK_CYCLE_POINT/foo:succeeded [[offset_polling]] script = cylc workflow-state ${CYLC_WORKFLOW_ID}//20100102T0000Z/foo --offset=P1D [[offset_polling2]] script = cylc workflow-state ${CYLC_WORKFLOW_ID}//20100102T0000Z/foo --offset=-P1D cylc-flow-8.6.4/tests/functional/workflow-state/08-integer.t0000775000175000017500000000540415202510242024156 0ustar alastairalastair#!/usr/bin/env bash # 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 . . "$(dirname "$0")/test_header" set_test_number 15 install_workflow "${TEST_NAME_BASE}" integer # run one cycle TEST_NAME="${TEST_NAME_BASE}_run_1" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach --stopcp=1 "${WORKFLOW_NAME}" # too many args TEST_NAME="${TEST_NAME_BASE}_cl_error" run_fail "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}-a" "${WORKFLOW_NAME}-b" TEST_NAME="${TEST_NAME_BASE}_check_1_status" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 2/foo:waiting 1/foo:succeeded 1/bar:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_check_1_outputs" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 --triggers "${WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__END__ 1/foo:{'submitted': 'submitted', 'started': 'started', 'succeeded': 'succeeded', 'x': 'hello'} 2/foo:{} 1/bar:{'submitted': 'submitted', 'started': 'started', 'succeeded': 'succeeded'} __END__ TEST_NAME="${TEST_NAME_BASE}_poll_fail" run_fail "${TEST_NAME}" cylc workflow-state --max-polls=2 --interval=1 "${WORKFLOW_NAME}//2/foo:succeeded" contains_ok "${TEST_NAME}.stderr" <<__END__ ERROR - failed after 2 polls __END__ # finish the run TEST_NAME="${TEST_NAME_BASE}_run_2" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}_poll_succeed" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//2/foo:succeeded" contains_ok "${TEST_NAME}.stdout" <<__END__ 2/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_int_offset" run_ok "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//1/foo:succeeded" --offset=P1 contains_ok "${TEST_NAME}.stdout" <<__END__ 2/foo:succeeded __END__ TEST_NAME="${TEST_NAME_BASE}_wildcard_offset" run_fail "${TEST_NAME}" cylc workflow-state --max-polls=1 "${WORKFLOW_NAME}//*/foo:succeeded" --offset=P1 contains_ok "${TEST_NAME}.stderr" <<__END__ InputError: Cycle point "*" is not compatible with an offset. __END__ purge cylc-flow-8.6.4/tests/functional/cyclepoint/0000775000175000017500000000000015202510242021262 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cyclepoint/00-time.t0000664000175000017500000001150715202510242022626 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # basic jinja2 expansion test . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 45 #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-use-env-var export CYLC_TASK_CYCLE_POINT=20100102T0300 run_ok "${TEST_NAME}.check-env" cylc cycle-point run_ok "${TEST_NAME}.year-only" cylc cycle-point --print-year cmp_ok "${TEST_NAME}.year-only.stdout" - << __OUT__ 2010 __OUT__ run_ok "${TEST_NAME}.month-only" cylc cycle-point --print-month cmp_ok "${TEST_NAME}.month-only.stdout" - << __OUT__ 01 __OUT__ run_ok "${TEST_NAME}.day-only" cylc cycle-point --print-day cmp_ok "${TEST_NAME}.day-only.stdout" - << __OUT__ 02 __OUT__ run_ok "${TEST_NAME}.hour-only" cylc cycle-point --print-hour cmp_ok "${TEST_NAME}.hour-only.stdout" - << __OUT__ 03 __OUT__ #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-offset-env-var run_ok "${TEST_NAME}.year" cylc cycle-point --offset-years=10 cmp_ok "${TEST_NAME}.year.stdout" - << __OUT__ 20200102T0300 __OUT__ run_ok "${TEST_NAME}.year-neg" cylc cycle-point --offset-years=-11 cmp_ok "${TEST_NAME}.year-neg.stdout" - << __OUT__ 19990102T0300 __OUT__ run_ok "${TEST_NAME}.month" cylc cycle-point --offset-months=2 cmp_ok "${TEST_NAME}.month.stdout" - << __OUT__ 20100302T0300 __OUT__ run_ok "${TEST_NAME}.month-neg" cylc cycle-point --offset-months=-1 cmp_ok "${TEST_NAME}.month-neg.stdout" - << __OUT__ 20091202T0300 __OUT__ run_ok "${TEST_NAME}.day" cylc cycle-point --offset-days=10 cmp_ok "${TEST_NAME}.day.stdout" - << __OUT__ 20100112T0300 __OUT__ run_ok "${TEST_NAME}.day-neg" cylc cycle-point --offset-days=-2 cmp_ok "${TEST_NAME}.day-neg.stdout" - << __OUT__ 20091231T0300 __OUT__ run_ok "${TEST_NAME}.hour" cylc cycle-point --offset-hours=10 cmp_ok "${TEST_NAME}.hour.stdout" - << __OUT__ 20100102T1300 __OUT__ run_ok "${TEST_NAME}.hour-neg" cylc cycle-point --offset-hours=-3 cmp_ok "${TEST_NAME}.hour-neg.stdout" - << __OUT__ 20100102T0000 __OUT__ #------------------------------------------------------------------------------- #Test with a supplied cycle time # N.B. this also checks environment variable being by CLI options TEST_NAME="${TEST_NAME_BASE}-print-supplied-ctime" run_ok "${TEST_NAME}.full" cylc cycle-point '2011-01-01' cmp_ok "${TEST_NAME}.full.stdout" - <<<'2011-01-01' run_ok "${TEST_NAME}-offset-week" cylc cycle-point --offset=P1W '20160301T06Z' cmp_ok "${TEST_NAME}-offset-week.stdout" - <<<'20160308T06Z' #------------------------------------------------------------------------------- unset CYLC_TASK_CYCLE_POINT # Test --equal option TEST_NAME="${TEST_NAME_BASE}-equal" run_ok "${TEST_NAME}-true" cylc cycle-point 2000 --equal 2000 run_fail "${TEST_NAME}-true" cylc cycle-point 2000 --equal 2001 run_fail "${TEST_NAME}-invalid" cylc cycle-point 2000 --equal x # Test --template option TEST_NAME="${TEST_NAME_BASE}-template" run_ok "${TEST_NAME}-pass" cylc cycle-point 2010-08 \ --offset-years=2 --template=foo-CCYY-MM.nc cmp_ok "${TEST_NAME}-pass.stdout" <<< 'foo-2012-08.nc' # invalid arg combo run_fail "${TEST_NAME}-fail" cylc cycle-point 2000 --template=x --print-year #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-fail" # no cycle point run_fail "${TEST_NAME}-1" cylc cycle-point # invalid cycle point run_fail "${TEST_NAME}-2" cylc cycle-point x # too many cycle points run_fail "${TEST_NAME}-3" cylc cycle-point 2000 2000 # invalid offsets run_ok "${TEST_NAME}-5" cylc cycle-point 2000 --offset-hours=1 # VALID run_fail "${TEST_NAME}-6" cylc cycle-point 2000 --offset-hours=x # INVALID run_fail "${TEST_NAME}-7" cylc cycle-point 2000 --offset-days=x run_fail "${TEST_NAME}-8" cylc cycle-point 2000 --offset-months=x run_fail "${TEST_NAME}-9" cylc cycle-point 2000 --offset-years=x # invalid ISO offset run_ok "${TEST_NAME}-10" cylc cycle-point 2000 --offset=P1Y # VALID run_fail "${TEST_NAME}-11" cylc cycle-point 2000 --offset=PT1Y # INVALID cylc-flow-8.6.4/tests/functional/cyclepoint/02-template.t0000775000175000017500000000447315202510242023514 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # basic cylc cyclepoint --template option tests . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 12 #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-extract run_ok "${TEST_NAME}.strf" cylc cyclepoint --template foo-%Y-%m-%d-%H.nc 20140809T12 cmp_ok "${TEST_NAME}.strf.stdout" - << __OUT__ foo-2014-08-09-12.nc __OUT__ #------------------------------------------------------------------------------- run_ok "${TEST_NAME}.yr" cylc cyclepoint --template CCYY 20140808T1200 cmp_ok "${TEST_NAME}.yr.stdout" - << __OUT__ 2014 __OUT__ #------------------------------------------------------------------------------- run_ok "${TEST_NAME}.month" cylc cyclepoint --template CC 20140809T1200 cmp_ok "${TEST_NAME}.month.stdout" - << __OUT__ 20 __OUT__ #------------------------------------------------------------------------------- run_ok "${TEST_NAME}.month" cylc cyclepoint --template MM 20140809T1200 cmp_ok "${TEST_NAME}.month.stdout" - << __OUT__ 08 __OUT__ #------------------------------------------------------------------------------- run_ok "${TEST_NAME}.day" cylc cyclepoint --template DD 20140809T1200 cmp_ok "${TEST_NAME}.day.stdout" - << __OUT__ 09 __OUT__ #------------------------------------------------------------------------------- run_ok "${TEST_NAME}.hour" cylc cyclepoint --template hh 20140809T1200 cmp_ok "${TEST_NAME}.hour.stdout" - << __OUT__ 12 __OUT__ cylc-flow-8.6.4/tests/functional/cyclepoint/test_header0000777000175000017500000000000015202510242027577 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/n-window/0000775000175000017500000000000015202510242020653 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/n-window/01-past-present-future/0000775000175000017500000000000015202510242025026 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/n-window/01-past-present-future/flow.cylc0000664000175000017500000000166515202510242026661 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ a => b => c => d => e """ [runtime] [[a]] script = """ set +e read -r -d '' gqlDoc <<_DOC_ {"request_string": " mutation { setGraphWindowExtent ( workflows: [\"${CYLC_WORKFLOW_ID}\"], nEdgeDistance: 2) { result } }", "variables": null} _DOC_ echo "${gqlDoc}" cylc client "$CYLC_WORKFLOW_ID" graphql < <(echo ${gqlDoc}) 2>/dev/null set -e """ [[c]] script = """ cylc show "$CYLC_WORKFLOW_ID//1/a" >> $CYLC_WORKFLOW_RUN_DIR/show-a.txt cylc show "$CYLC_WORKFLOW_ID//1/b" >> $CYLC_WORKFLOW_RUN_DIR/show-b.txt cylc show "$CYLC_WORKFLOW_ID//1/c" >> $CYLC_WORKFLOW_RUN_DIR/show-c.txt cylc show "$CYLC_WORKFLOW_ID//1/d" >> $CYLC_WORKFLOW_RUN_DIR/show-d.txt cylc show "$CYLC_WORKFLOW_ID//1/e" >> $CYLC_WORKFLOW_RUN_DIR/show-e.txt """ cylc-flow-8.6.4/tests/functional/n-window/02-big-window/0000775000175000017500000000000015202510242023140 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/n-window/02-big-window/flow.cylc0000664000175000017500000000176415202510242024773 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ a => b => c => d => e => f => g => h b => i => j => f """ [runtime] [[a]] script = """ set +e read -r -d '' gqlDoc <<_DOC_ {"request_string": " mutation { setGraphWindowExtent ( workflows: [\"${CYLC_WORKFLOW_ID}\"], nEdgeDistance: 5) { result } }", "variables": null} _DOC_ echo "${gqlDoc}" cylc client "$CYLC_WORKFLOW_ID" graphql < <(echo ${gqlDoc}) 2>/dev/null set -e """ [[c]] script = """ cylc show "$CYLC_WORKFLOW_ID//1/a" >> $CYLC_WORKFLOW_RUN_DIR/show-a.txt cylc show "$CYLC_WORKFLOW_ID//1/j" >> $CYLC_WORKFLOW_RUN_DIR/show-j.txt cylc show "$CYLC_WORKFLOW_ID//1/h" >> $CYLC_WORKFLOW_RUN_DIR/show-h.txt """ [[i]] script = """ # Slow 2nd branch down sleep 5 """ [[f]] script = """ # test re-trigger of old point cylc trigger "$CYLC_WORKFLOW_ID//1/b" """ cylc-flow-8.6.4/tests/functional/n-window/02-big-window.t0000664000175000017500000000361115202510242023326 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test large window size using graphql and find tasks in window. # This is helpful with coverage by using most the no-rewalk mechanics. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" # 'a => b => . . . f => g => h', 'a' sets window size to 5, # 'b => i => j => f', 'c' finds 'a', 'j', 'h' workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-show-a.past" contains_ok "$WORKFLOW_RUN_DIR/show-a.txt" <<__END__ state: succeeded prerequisites: (None) __END__ TEST_NAME="${TEST_NAME_BASE}-show-j.parallel" contains_ok "${WORKFLOW_RUN_DIR}/show-j.txt" <<__END__ state: waiting prerequisites: ('⨯': not satisfied) ⨯ 1/i succeeded __END__ TEST_NAME="${TEST_NAME_BASE}-show-h.future" contains_ok "${WORKFLOW_RUN_DIR}/show-h.txt" <<__END__ state: waiting prerequisites: ('⨯': not satisfied) ⨯ 1/g succeeded __END__ purge cylc-flow-8.6.4/tests/functional/n-window/01-past-present-future.t0000664000175000017500000000415015202510242025213 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test window size using graphql and cylc-show for all tasks. . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" # 'a => b => c => d => e', 'a' sets window size to 2, 'c' uses cylc show on all. workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-show-a.past" contains_ok "$WORKFLOW_RUN_DIR/show-a.txt" <<__END__ state: succeeded prerequisites: (None) __END__ TEST_NAME="${TEST_NAME_BASE}-show-b.past" contains_ok "$WORKFLOW_RUN_DIR/show-b.txt" <<__END__ state: succeeded prerequisites: (n/a for past tasks) __END__ TEST_NAME="${TEST_NAME_BASE}-show-c.present" contains_ok "${WORKFLOW_RUN_DIR}/show-c.txt" <<__END__ prerequisites: ('⨯': not satisfied) ✓ 1/b succeeded __END__ TEST_NAME="${TEST_NAME_BASE}-show-d.future" contains_ok "${WORKFLOW_RUN_DIR}/show-d.txt" <<__END__ state: waiting prerequisites: ('⨯': not satisfied) ⨯ 1/c succeeded __END__ TEST_NAME="${TEST_NAME_BASE}-show-e.future" contains_ok "${WORKFLOW_RUN_DIR}/show-e.txt" <<__END__ state: waiting prerequisites: ('⨯': not satisfied) ⨯ 1/d succeeded __END__ purge cylc-flow-8.6.4/tests/functional/n-window/test_header0000777000175000017500000000000015202510242027170 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-resources/0000775000175000017500000000000015202510242022630 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-resources/test_header0000777000175000017500000000000015202510242031145 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-resources/00-error.t0000775000175000017500000000253415202510242024372 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Cylc get-resources doesn't overwrite a file with a dir, or vice-versa: . "$(dirname "$0")/test_header" set_test_number 4 TEST="${TEST_NAME_BASE}-overwrite-dir" mkdir cylc run_fail "${TEST}" cylc get-resources cylc grep_ok "Cannot extract file ${PWD}/cylc as there is an existing directory with the same name" "${TEST}.stderr" rm -r cylc touch syntax run_fail "${TEST}" cylc get-resources syntax grep_ok "Cannot extract directory ${PWD}/syntax as there is an existing file with the same name" "${TEST}.stderr" exit cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/0000775000175000017500000000000015202510242024030 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/06-vip-named-run0000777000175000017500000000000015202510242027611 200-vipustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/08-vr-against-src.t0000664000175000017500000000574115202510242027311 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall restart) # Tests that VR doesn't modify the source directory for Cylc play. # See https://github.com/cylc/cylc-flow/issues/6209 . "$(dirname "$0")/test_header" set_test_number 9 # Setup (Run VIP, check that the play step fails in the correct way): WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow_fail_on_play/flow.cylc" . # Run Cylc VIP run_fail "setup (vip)" \ cylc vip --debug \ --workflow-name "${WORKFLOW_NAME}" \ --no-run-name validate1="$(grep -Po "WARNING - vip:(.*)" "setup (vip).stderr" | awk -F ':' '{print $2}')" play1="$(grep -Po "WARNING - play:(.*)" "setup (vip).stderr" | awk -F ':' '{print $2}')" # Change the workflow to make the reinstall happen: echo "" >> flow.cylc # Run Cylc VR: TEST_NAME="${TEST_NAME_BASE}" run_fail "${TEST_NAME}" cylc vr "${WORKFLOW_NAME}" validate2="$(grep -Po "WARNING - vr:(.*)" "${TEST_NAME_BASE}.stderr" | awk -F ':' '{print $2}')" play2="$(grep -Po "WARNING - play:(.*)" "${TEST_NAME_BASE}.stderr" | awk -F ':' '{print $2}')" # Test that the correct source directory is openened at different # stages of Cylc VIP & VR TEST_NAME="outputs-created" if [[ -n $validate1 && -n $validate2 && -n $play1 && -n $play2 ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi TEST_NAME="vip validate and play operate on different folders" if [[ $validate1 != "${play1}" ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi TEST_NAME="vr & vip validate operate on the same folder" if [[ $validate1 == "${validate2}" ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi TEST_NAME="vr validate and play operate on different folders" if [[ $validate2 != "${play2}" ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi TEST_NAME="vip play loads from a cylc-run subdir" if [[ "${play2}" =~ cylc-run ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi TEST_NAME="vr play loads from a cylc-run subdir" if [[ "${play2}" =~ cylc-run ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi # Clean Up: run_ok "${TEST_NAME_BASE}-stop cylc stop ${WORKFLOW_NAME} --now --now" purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow_stop/0000775000175000017500000000000015202510242027456 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow_stop/flow.cylc0000664000175000017500000000030615202510242031300 0ustar alastairalastair[scheduler] allow implicit tasks = true [[events]] startup handlers = cylc stop --now --now %(workflow)s [scheduling] initial cycle point = 1500 [[graph]] P1Y = foo cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/05-vr-fail-is-running.t0000664000175000017500000000400415202510242030064 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall) # In this case the target workflow is in an abiguous state: We cannot tell # Whether it's running, paused or stopped. Cylc VR should validate before # reinstall: . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config "" """ [scheduler] [[main loop]] plugins = reset bad hosts """ # Setup WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow/flow.cylc" . run_ok "setup (vip)" \ cylc vip --debug \ --workflow-name "${WORKFLOW_NAME}" \ --no-run-name # Get the workflow into an unreachable state CONTACTFILE="${RUN_DIR}/${WORKFLOW_NAME}/.service/contact" cp "$CONTACTFILE" "${CONTACTFILE}.old" poll test -e "${CONTACTFILE}" sed -i 's@CYLC_WORKFLOW_HOST=.*@CYLC_WORKFLOW_HOST=elephantshrew@' "${CONTACTFILE}" # It can't figure out whether the workflow is running: # Change source workflow and run vr: run_fail "${TEST_NAME_BASE}-runs" cylc vr "${WORKFLOW_NAME}" grep_ok "elephantshrew." "${TEST_NAME_BASE}-runs.stderr" # Clean Up: mv "${CONTACTFILE}.old" "$CONTACTFILE" run_ok "${TEST_NAME_BASE}-stop" cylc stop "${WORKFLOW_NAME}" --now --now purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/00-vip/0000775000175000017500000000000015202510242025043 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/00-vip/reference.log0000664000175000017500000000005715202510242027506 0ustar alastairalastair13000101T0000Z/foo -triggered off [] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/00-vip/flow.cylc0000664000175000017500000000017015202510242026664 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1500 [[graph]] R1 = foo cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/00-vip.t0000664000175000017500000000323715202510242025235 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vip` (Validate Install Play) . "$(dirname "$0")/test_header" set_test_number 5 WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp -r "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/flow.cylc" . cp -r "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/reference.log" . run_ok "${TEST_NAME_BASE}-from-path" \ cylc vip --no-detach --debug \ --workflow-name "${WORKFLOW_NAME}" \ --initial-cycle-point=1300 \ --no-run-name \ --reference-test \ grep_ok "13000101T0000Z" "${TEST_NAME_BASE}-from-path.stdout" grep "\$" "${TEST_NAME_BASE}-from-path.stdout" > VIPOUT.txt named_grep_ok "${TEST_NAME_BASE}-it-validated" "$ cylc validate" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc install" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-played" "$ cylc play" "VIPOUT.txt" purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow_fail_on_play/0000775000175000017500000000000015202510242031125 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow_fail_on_play/flow.cylc0000664000175000017500000000056315202510242032754 0ustar alastairalastair#!jinja2 {% from "sys" import argv %} {% from "cylc.flow" import LOG %} {% from "pathlib" import Path %} {% if argv[1] == "play" %} this = should cause cylc play to fail {% endif %} {% set SPATH = Path.cwd().__str__() %} {% do LOG.warning(argv[1] + ":" + SPATH) %} [scheduling] initial cycle point = 1500 [[graph]] P1Y = foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/07-vr-remote.t0000664000175000017500000000332215202510242026361 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall restart) # Test that args for re-invocation are correct: export REQUIRE_PLATFORM='loc:remote runner:background fs:shared' . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config '' """ [scheduler] [[run hosts]] available = ${CYLC_TEST_HOST} """ # Setup WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow_stop/flow.cylc" . run_ok "${TEST_NAME_BASE}-install" \ cylc vip \ --workflow-name "${WORKFLOW_NAME}" \ --no-detach # It validates and restarts: # Change source workflow and run vr: TEST_NAME="${TEST_NAME_BASE}-reinvoke" run_ok "${TEST_NAME}" cylc vr "${WORKFLOW_NAME}" --no-detach ls "${RUN_DIR}/${WORKFLOW_NAME}/runN/log/scheduler" > logdir.txt cmp_ok logdir.txt <<__HERE__ 01-start-01.log 02-start-01.log log __HERE__ # Clean Up. purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/02-vr-restart.t0000664000175000017500000000351115202510242026545 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall restart) # In this case the target workflow is stopped so cylc play is run. . "$(dirname "$0")/test_header" set_test_number 6 # Setup WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow/flow.cylc" . run_ok "setup (install)" \ cylc install \ --workflow-name "${WORKFLOW_NAME}" export WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" # It validates and restarts: # Run VR run_ok "${TEST_NAME_BASE}-runs" cylc vr "${WORKFLOW_NAME}" # Grep for vr reporting revalidation, reinstallation and playing the workflow. grep "\$" "${TEST_NAME_BASE}-runs.stdout" > VIPOUT.txt named_grep_ok "${TEST_NAME_BASE}-it-revalidated" "$ cylc validate --against-source" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc reinstall" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-played" "cylc play" "VIPOUT.txt" # Clean Up. run_ok "${TEST_NAME_BASE}-stop" cylc stop "${WORKFLOW_NAME}" --now --now purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/09-vr-icp-now/0000775000175000017500000000000015202510242026257 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/09-vr-icp-now/flow.cylc0000664000175000017500000000037015202510242030102 0ustar alastairalastair[scheduler] [[events]] restart timeout = PT0S [scheduling] initial cycle point = 2020 final cycle point = 2020 [[graph]] P1Y = foo [runtime] [[foo]] [[[simulation]]] default run length = PT0S cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/test_header0000777000175000017500000000000015202510242032345 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/09-vr-icp-now.t0000664000175000017500000000264315202510242026451 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Ensure that validate step of `cylc vr` does not set the --icp option for the # restart step, as this would cause an InputError. # See https://github.com/cylc/cylc-flow/issues/6262 . "$(dirname "$0")/test_header" set_test_number 2 WORKFLOW_ID=$(workflow_id) cp -r "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/flow.cylc" . run_ok "${TEST_NAME_BASE}-vip" cylc vip . \ --workflow-name "${WORKFLOW_ID}" \ --no-detach \ --no-run-name \ --mode simulation echo "# Some Comment" >> flow.cylc run_ok "${TEST_NAME_BASE}-vr" \ cylc vr "${WORKFLOW_ID}" --no-detach --mode simulation cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/04-vr-fail-validate.t0000664000175000017500000000345615202510242027575 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall restart) # Changes to the source cause VR to bail on validation. . "$(dirname "$0")/test_header" set_test_number 5 # Setup WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow/flow.cylc" . run_ok "setup (vip)" \ cylc vip --debug \ --workflow-name "${WORKFLOW_NAME}" \ --no-run-name # Change source workflow and run vr: # Cut the runtime section out of the source flow. head -n 5 > tmp < flow.cylc cat tmp > flow.cylc TEST_NAME="${TEST_NAME_BASE}" run_fail "${TEST_NAME}" cylc vr "${WORKFLOW_NAME}" # Grep for reporting of revalidation, reinstallation, reloading and playing: named_grep_ok "${TEST_NAME_BASE}-it-tried" \ "$ cylc validate --against-source" "${TEST_NAME}.stdout" named_grep_ok "${TEST_NAME_BASE}-it-failed" \ "WorkflowConfigError" "${TEST_NAME}.stderr" # Clean Up: run_ok "${TEST_NAME_BASE}-stop" cylc stop "${WORKFLOW_NAME}" --now --now purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/06-vip-named-run.t0000664000175000017500000000324215202510242027123 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vip` (Validate Install Play) . "$(dirname "$0")/test_header" set_test_number 5 WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp -r "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/flow.cylc" . cp -r "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/reference.log" . run_ok "${TEST_NAME_BASE}-from-path" \ cylc vip --no-detach --debug \ --workflow-name "${WORKFLOW_NAME}" \ --initial-cycle-point=1300 \ --run-name sardine \ --reference-test grep_ok "13000101T0000Z" "${TEST_NAME_BASE}-from-path.stdout" grep "\$" "${TEST_NAME_BASE}-from-path.stdout" > VIPOUT.txt named_grep_ok "${TEST_NAME_BASE}-it-validated" "$ cylc validate" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc install" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-played" "$ cylc play" "VIPOUT.txt" purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/01-vr-reload.t0000664000175000017500000000411315202510242026325 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `cylc vr` (Validate Reinstall) # In this case the target workflow is running so cylc reload is run. . "$(dirname "$0")/test_header" set_test_number 7 # Setup (Must be a running workflow, note the unusual absence of --no-detach) WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cp "${TEST_SOURCE_DIR}/vr_workflow/flow.cylc" . run_ok "setup (vip)" \ cylc vip --debug \ --workflow-name "${WORKFLOW_NAME}" \ --no-run-name export WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" poll_workflow_running # It validates and reloads: run_ok "${TEST_NAME_BASE}-runs" cylc vr "${WORKFLOW_NAME}" # Grep for VR reporting revalidation, reinstallation and reloading grep "\$" "${TEST_NAME_BASE}-runs.stdout" > VIPOUT.txt named_grep_ok "${TEST_NAME_BASE}-it-validated" "$ cylc validate --against-source" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-installed" "$ cylc reinstall" "VIPOUT.txt" named_grep_ok "${TEST_NAME_BASE}-it-reloaded" "$ cylc reload" "VIPOUT.txt" cylc play "${WORKFLOW_NAME}" named_grep_ok "${TEST_NAME_BASE}-it-logged-reload" \ "Reloading the workflow definition" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" # Clean Up. run_ok "${TEST_NAME_BASE}-stop" cylc stop "${WORKFLOW_NAME}" --now --now purge exit 0 cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow/0000775000175000017500000000000015202510242026411 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-combination-scripts/vr_workflow/flow.cylc0000664000175000017500000000021615202510242030233 0ustar alastairalastair[scheduling] initial cycle point = 1500 [[graph]] P1Y = foo [runtime] [[foo]] script = cylc pause ${WORKFLOW_ID} cylc-flow-8.6.4/tests/functional/cylc-combination-scripts/10-CYLC_WORKFLOW_SRC_DIR.t0000664000175000017500000000376615202510242027740 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test `CYLC_WORKFLOW_SRC_DIR.t` is set . "$(dirname "$0")/test_header" set_test_number 6 WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" cat > flow.cylc <<__HERE__ #!jinja2 # TEST: {{ CYLC_WORKFLOW_SRC_DIR }} [scheduler] allow implicit tasks = true [scheduling] [[graph]] R1 = foo __HERE__ run_ok "${TEST_NAME_BASE}-view" cylc view -p . named_grep_ok "src-path-in-view-p" "TEST: $PWD" \ "${TEST_NAME_BASE}-view.stdout" # It starts playing: run_ok "${TEST_NAME_BASE}-vip" \ cylc vip \ --pause \ --no-run-name \ --workflow-name "${WORKFLOW_NAME}" # It can get CYLC_WORKFLOW_SRC_DIR named_grep_ok "src-path-available" \ "TEST: $PWD" "${RUN_DIR}/${WORKFLOW_NAME}/log/config/flow-processed.cylc" # It can be updated with Cylc VR echo "[meta]" >> flow.cylc run_ok "${TEST_NAME_BASE}-vr" \ cylc vr "${WORKFLOW_NAME}" poll_grep "meta" "${RUN_DIR}/${WORKFLOW_NAME}/log/config/flow-processed.cylc" # It can get CYLC_WORKFLOW_SRC_DIR named_grep_ok "src-path-available" \ "TEST: $PWD" "${RUN_DIR}/${WORKFLOW_NAME}/log/config/flow-processed.cylc" cylc stop "${WORKFLOW_NAME}" purge "${WORKFLOW_NAME}" cylc-flow-8.6.4/tests/functional/runahead/0000775000175000017500000000000015202510242020700 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/02-check-default-complex.t0000664000175000017500000000421115202510242025446 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test default runahead limit behaviour is still the same . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" default-complex #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-max-cycle DB="${WORKFLOW_RUN_DIR}/log/db" run_ok "${TEST_NAME}" sqlite3 "${DB}" \ "select max(cycle) from task_states where status!='waiting'" cmp_ok "${TEST_NAME}.stdout" <<< "20100101T1200Z" # i.e. should have spawned 5 cycle points from initial T00 #------------------------------------------------------------------------------- grep_ok 'Workflow shutting down - "abort on stall timeout" is set' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/default-simple/0000775000175000017500000000000015202510242023613 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/default-simple/flow.cylc0000664000175000017500000000101115202510242025427 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100105T00 [[graph]] # Intervals are all 24 hours, but we really have a 6 hour repetition. # oops makes bar spawn as waiting to hold back the runahead T00, T06, T12, T18 = "foo & oops => bar" [runtime] [[foo]] script = false [[bar]] script = true cylc-flow-8.6.4/tests/functional/runahead/04-no-final-cycle.t0000664000175000017500000000370415202510242024112 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test runahead limit is being enforced and doesn't break with no final cycle . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" no_final #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-check-fail DB="$RUN_DIR/${WORKFLOW_NAME}/log/db" TASKS=$(sqlite3 "${DB}" "SELECT COUNT(*) FROM task_states WHERE status=='failed'") # manual comparison for the test if ((TASKS==2)); then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/03-check-default-future.t0000664000175000017500000000442115202510242025315 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test default runahead limit behaviour is still the same . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" default-future #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -v --set="FUTURE_TRIGGER_START_POINT='T04'" \ "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach --set="FUTURE_TRIGGER_START_POINT='T04'" \ "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-max-cycle DB="$RUN_DIR/${WORKFLOW_NAME}/log/db" run_ok "${TEST_NAME}" sqlite3 "${DB}" \ "select max(cycle) from task_states where name=='foo' and status=='failed'" cmp_ok "${TEST_NAME}.stdout" <<< "20100101T0400Z" # i.e. should have spawned 5 cycle points from initial T00 (wibble not spawned) #------------------------------------------------------------------------------- grep_ok 'Workflow shutting down - "abort on inactivity timeout" is set' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/05-check-default-future-2.t0000664000175000017500000000460215202510242025457 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test default runahead limit behaviour is still the same . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" default-future #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -v \ --set="FUTURE_TRIGGER_START_POINT='T02'" \ "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach \ --set="FUTURE_TRIGGER_START_POINT='T02'" \ "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-max-cycle DB="$RUN_DIR/${WORKFLOW_NAME}/log/db" run_ok "${TEST_NAME}" sqlite3 "${DB}" \ "select max(cycle) from task_states where name=='foo' and status=='failed'" cmp_ok "${TEST_NAME}.stdout" <<< "20100101T1000Z" # i.e. should have spawned 5 cycle points from initial T00, and then raised # this by PT6H due to fact that wibble spawned. #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-check-aborted LOG="$RUN_DIR/${WORKFLOW_NAME}/log/scheduler/log" grep_ok 'Workflow shutting down - "abort on inactivity timeout" is set' "${LOG}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/default-complex/0000775000175000017500000000000015202510242023771 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/default-complex/flow.cylc0000664000175000017500000000120515202510242025612 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [[events]] abort on stall timeout = True stall timeout = PT0S stall timeout = PT30S abort on stall timeout = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100105T00 [[graph]] # T00, T07, T14, ... # oops makes bar spawn as waiting PT7H = "foo & oops => bar" # T00, T12, T18... T00, T12, T18 = "foo" # T04... T04 = "run_ok" # T05... T05 = "run_ok_2" [runtime] [[foo, fail]] script = false [[bar]] script = true cylc-flow-8.6.4/tests/functional/runahead/time-limit/0000775000175000017500000000000015202510242022752 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/time-limit/flow.cylc0000664000175000017500000000112715202510242024576 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] runahead limit = PT4H initial cycle point = 2020-01-01T00 final cycle point = 2020-01-02T00 [[graph]] PT1H = "foo & spawn-bar => bar" # foo fails first cycle point only. # bar waiting should cause a runahead stall at T04 # (PT4H limit allows T00, T01, T02, T03 [runtime] [[root]] script = true [[foo]] script = """[[ "$CYLC_TASK_JOB" != '20200101T0000Z/foo/01' ]]""" cylc-flow-8.6.4/tests/functional/runahead/06-release-update.t0000664000175000017500000000424015202510242024210 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that the datastore is updated when runahead tasks are released. # GitHub #1981 . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" 'release-update' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" 1>'out' 2>&1 & CYLC_RUN_PID="$!" poll_workflow_running YYYY="$(date +%Y)" NEXT1=$(( YYYY + 1 )) poll_grep_workflow_log -E "${NEXT1}/bar.* added to the n=0 window" # sleep a little to allow the datastore to update (`cylc dump` sees the # datastore) TODO can we avoid this flaky sleep somehow? sleep 10 # (gratuitous use of --flows for test coverage) cylc dump -l --flows -t "${WORKFLOW_NAME}" | awk '{print $1 $2 $3 $7}' >'log' # The scheduler task pool should contain: # NEXT1/foo - waiting on clock trigger # NEXT1/bar - waiting, partially satisfied # The n=1 data store should also contain: # YYYY/bar - succeeded cmp_ok 'log' - <<__END__ bar,$NEXT1,waiting,[1] foo,$NEXT1,waiting,[1] __END__ run_ok "${TEST_NAME_BASE}-stop" \ cylc stop --max-polls=10 --interval=6 "${WORKFLOW_NAME}" if ! wait "${CYLC_RUN_PID}" 1>'/dev/null' 2>&1; then cat 'out' >&2 fi #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/runahead/test_header0000777000175000017500000000000015202510242027215 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/default-future/0000775000175000017500000000000015202510242023634 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/default-future/flow.cylc0000664000175000017500000000147315202510242025464 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True allow implicit tasks = True [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] initial cycle point = 20100101T00 final cycle point = 20100105T00 [[xtriggers]] never = wall_clock(P100Y) [[graph]] R1 = spawner PT1H = """ @never => bar foo """ # If wibble gets into the pool, it will demand a +PT6H raise # of the 'runahead limit'. {{ FUTURE_TRIGGER_START_POINT }}/PT6H = """ foo[+PT6H] => wibble """ [runtime] [[root]] script = true [[spawner]] script = """ # spawn wibble cylc set $CYLC_WORKFLOW_ID 20100101T0800Z/foo """ [[foo]] script = false cylc-flow-8.6.4/tests/functional/runahead/07-time-limit.t0000664000175000017500000000411215202510242023361 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test runahead limit is being enforced when specified as time limit . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "$TEST_NAME_BASE" time-limit #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "$TEST_NAME" cylc validate "$WORKFLOW_NAME" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "$TEST_NAME" cylc play --debug --no-detach "$WORKFLOW_NAME" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-max-cycle" DB="${WORKFLOW_RUN_DIR}/log/db" run_ok "$TEST_NAME" sqlite3 "$DB" \ "select max(cycle) from task_states where status!='waiting'" cmp_ok "${TEST_NAME}.stdout" <<< "20200101T0400Z" #------------------------------------------------------------------------------- grep_ok 'Workflow shutting down - "abort on stall timeout" is set' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/runahead/00-runahead.t0000664000175000017500000000404715202510242023076 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test runahead limit is being enforced . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" runahead #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-max-cycle" DB="${WORKFLOW_RUN_DIR}/log/db" run_ok "${TEST_NAME}" sqlite3 "${DB}" \ "select max(cycle) from task_states where status!='waiting'" cmp_ok "${TEST_NAME}.stdout" <<< "4" #------------------------------------------------------------------------------- grep_ok 'Workflow shutting down - "abort on stall timeout" is set' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/runahead/0000775000175000017500000000000015202510242022467 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/runahead/flow.cylc0000664000175000017500000000135215202510242024313 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] cycling mode = integer runahead limit = P3 initial cycle point = 1 final cycle point = 20 [[graph]] P1 = foo & run_ok => bar # foo fails on 1st cycle point only, succeeds on all others. # SoD: run_ok ensures bar spawns as waiting in 1st cycle pt, to # hold back the runahead. # As runahead limit is consecutive, even though cycle points 2 and # above succeed, workflow stalls after 4 cycle points. [runtime] [[root]] script = true [[foo]] script = if [[ "$CYLC_TASK_JOB" == '1/foo/01' ]]; then false; else true; fi cylc-flow-8.6.4/tests/functional/runahead/01-check-default-simple.t0000664000175000017500000000453115202510242025274 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test default runahead limit behaviour is still the same . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 5 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" default-simple #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # Testing runahead by counting tasks after a stall is tricky, so this test # might not be optimal. For instance, abort-on-stall aborts even if tasks # could immediately be released from the runahead pool. TEST_NAME="${TEST_NAME_BASE}-max-cycle" DB="${WORKFLOW_RUN_DIR}/log/db" run_ok "${TEST_NAME}" sqlite3 "${DB}" \ "select max(cycle) from task_states where status!='waiting'" cmp_ok "${TEST_NAME}.stdout" <<< "20100102T0000Z" # i.e. should have spawned 5 cycle points from initial 01T00 #------------------------------------------------------------------------------- grep_ok 'Workflow shutting down - "abort on stall timeout" is set' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/runahead/release-update/0000775000175000017500000000000015202510242023600 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/release-update/flow.cylc0000664000175000017500000000060215202510242025421 0ustar alastairalastair[scheduler] allow implicit tasks = True cycle point format = %Y [scheduling] initial cycle point = now final cycle point = +P1Y runahead limit = P0 [[special tasks]] clock-trigger = foo(P0Y) [[graph]] P1Y = """ foo => bar bar[-P1Y] => bar """ # (or sequential bar) [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/runahead/no_final/0000775000175000017500000000000015202510242022465 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/runahead/no_final/flow.cylc0000664000175000017500000000102615202510242024307 0ustar alastairalastair# Should shutdown stalled with 2 failed tasks due to P1 runahead limit [scheduler] cycle point time zone = Z [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT1M [scheduling] runahead limit = P1 initial cycle point = 20100101T00 [[xtriggers]] never = wall_clock(P100Y) [[graph]] PT6H = """ foo @never => bar """ [runtime] [[foo, bar]] script = false cylc-flow-8.6.4/tests/functional/optional-outputs/0000775000175000017500000000000015202510242022457 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/06-c7backcompat-family/0000775000175000017500000000000015202510242026517 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/06-c7backcompat-family/suite.rc0000664000175000017500000000315715202510242030204 0ustar alastairalastair# This workflow is not valid at Cylc 8 due to output optionality clashes. # In Cylc 7 back compat mode it should stall with all 6 "stall" tasks unsatisfied. [task parameters] x = 1..12 [[templates]] x = %(x)02d [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] [[dependencies]] [[[R1]]] graph = """ # run tasks should all run # stall tasks should all be blocked GOOD:succeed-all => run01 GOOD:succeed-any => run02 GOOD:fail-all => stall01 GOOD:fail-any => stall02 GOOD:finish-all => run03 GOOD:finish-any => run04 BAD:succeed-all => stall03 BAD:succeed-any => stall04 BAD:fail-all => run05 BAD:fail-any => run06 BAD:finish-all => run07 BAD:finish-any => run08 UGLY:succeed-all => stall05 UGLY:succeed-any => run09 UGLY:fail-all => stall06 UGLY:fail-any => run10 UGLY:finish-all => run11 UGLY:finish-any => run12 """ [runtime] [[GOOD]] # all pass [[BAD]] # all fail [[UGLY]] # some pass, some fail [[_good_1, _good_2]] inherit = GOOD script = true [[_good_3]] inherit = UGLY script = true [[_bad_1, _bad_2]] inherit = BAD script = false [[_bad_3]] inherit = UGLY script = false [[run, stall]] cylc-flow-8.6.4/tests/functional/optional-outputs/07-finish-fail-c7-backcompat.t0000664000175000017500000000334415202510242027676 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------- # Test handling of failed tasks in finish triggers in back-compat mode. # See comments in finish-fail-c7/suite.rc . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" finish-fail-c7 # Validate with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c7" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG_1=$(python -c \ 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "${DEPR_MSG_1}" "${TEST_NAME}.stderr" # Stall expected at FCP (but not at runahead limit). workflow_run_fail "${TEST_NAME_BASE}-run" cylc play --no-detach --debug "${WORKFLOW_NAME}" grep_workflow_log_ok grep-0 "Workflow stalled" grep_workflow_log_ok grep-1 "ERROR - Incomplete tasks:" grep_workflow_log_ok grep-2 "1/foo did not complete the required outputs" grep_workflow_log_ok grep-3 "2/foo did not complete the required outputs" purge cylc-flow-8.6.4/tests/functional/optional-outputs/03-c7backcompat.t0000664000175000017500000000413215202510242025422 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check Cylc 7 backward compatibility for success/fail branching. # A bunch of unit tests in test_graph_parser.py check that outputs are handled # the same for a wide variety of graphs. This functional test should be # sufficient to check the resulting validation and run time behaviour. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate_as_c8" run_fail "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" ERR="GraphParseError: Opposite outputs .* must both be optional if both are used" grep_ok "${ERR}" "${TEST_NAME}.stderr" # Rename config to "suite.rc" mv "${WORKFLOW_RUN_DIR}/flow.cylc" "${WORKFLOW_RUN_DIR}/suite.rc" ln -s "${WORKFLOW_RUN_DIR}/suite.rc" "${WORKFLOW_RUN_DIR}/flow.cylc" # It should now validate, with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c7" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG_1=$(python -c \ 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "${DEPR_MSG_1}" "${TEST_NAME}.stderr" # And it should run without stalling with an incomplete task. workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test --debug "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/finish-fail-c7/0000775000175000017500000000000015202510242025157 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/finish-fail-c7/suite.rc0000664000175000017500000000156515202510242026645 0ustar alastairalastair# When a task with a finish trigger fails: # Cylc 7: # No runahead stall (failed tasks are ignored in computing runahead) but # stall when nothing else to do (e.g. final cycle point) due to failed tasks # in the pool. # Cylc 8 back-compat mode: # Replicate Cylc 7 behaviour by making success outputs required and ignoring # incomplete tasks in runahead computation. # Cylc 8 non-back-compat mode: # No runahead stall and no stall at final cycle point, because finish # triggers imply success is optional (i.e. no incomplete tasks created). [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 runahead limit = P0 [[dependencies]] [[[P1]]] graph = "foo:finish" [runtime] [[foo]] script = false cylc-flow-8.6.4/tests/functional/optional-outputs/06-c7backcompat-family.t0000664000175000017500000000364115202510242026710 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Cylc 7 stall backward compatibility, complex family case. . "$(dirname "$0")/test_header" set_test_number 12 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # It should now validate, with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c7" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG_1=$(python -c \ 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "${DEPR_MSG_1}" "${TEST_NAME}.stderr" # Should stall and abort with unsatisfied "stall" tasks. workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --debug "${WORKFLOW_NAME}" grep_workflow_log_ok grep-1 "Workflow stalled" grep_workflow_log_ok grep-2 "WARNING - Partially satisfied prerequisites" grep_workflow_log_ok grep-3 "1/stall01 is waiting on" grep_workflow_log_ok grep-4 "1/stall02 is waiting on" grep_workflow_log_ok grep-5 "1/stall03 is waiting on" grep_workflow_log_ok grep-6 "1/stall04 is waiting on" grep_workflow_log_ok grep-7 "1/stall05 is waiting on" grep_workflow_log_ok grep-8 "1/stall06 is waiting on" grep_workflow_log_ok grep-9 'Workflow shutting down \- "abort on stall timeout" is set' purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/08-finish-fail-c7-c8.t0000664000175000017500000000307615202510242026107 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------- # Test handling of failed tasks in finish triggers (non back-compat mode). # See comments in finish-fail-c7/suite.rc . "$(dirname "$0")/test_header" set_test_number 3 # (Note install will issue a back-compat mode message here). install_workflow "${TEST_NAME_BASE}" finish-fail-c7 # Turn of back-compat mode: mv "${WORKFLOW_RUN_DIR}/suite.rc" "${WORKFLOW_RUN_DIR}/flow.cylc" # Validate with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c8" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG="graph items were automatically upgraded" # (not back-compat) grep_ok "${DEPR_MSG}" "${TEST_NAME}.stderr" # No stall expected. workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach --debug "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/optional-outputs/02-no-stall-on-optional.t0000664000175000017500000000174215202510242027055 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check missing optional outputs do not cause a stall at the runahead limit. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/optional-outputs/03-c7backcompat/0000775000175000017500000000000015202510242025235 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/03-c7backcompat/reference.log0000664000175000017500000000012715202510242027676 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/optional-outputs/03-c7backcompat/flow.cylc0000664000175000017500000000024115202510242027055 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """ foo => bar foo:fail => baz foo => !baz """ cylc-flow-8.6.4/tests/functional/optional-outputs/04-c7backcompat-blocked-task.t0000664000175000017500000000325515202510242027771 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Cylc 7 stall backward compatibility, single parent case . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # It should now validate, with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c7" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG_1=$(python -c \ 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "${DEPR_MSG_1}" "${TEST_NAME}.stderr" # Should stall and abort with an unsatisfied prerequisite. workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test --debug "${WORKFLOW_NAME}" grep_workflow_log_ok grep-1 "WARNING - Partially satisfied prerequisites" grep_workflow_log_ok grep-2 "Workflow stalled" grep_workflow_log_ok grep-3 "1/bar is waiting on \['1/foo:x'\]" grep_workflow_log_ok grep-4 'Workflow shutting down \- "abort on stall timeout" is set' purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/01-stall-on-incomplete/0000775000175000017500000000000015202510242026563 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/01-stall-on-incomplete/reference.log0000664000175000017500000000007315202510242031224 0ustar alastairalastairInitial point: 1 Final point: None 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/optional-outputs/01-stall-on-incomplete/flow.cylc0000664000175000017500000000060115202510242030403 0ustar alastairalastair# Should stall at runhead limit with incomplete foo [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 runahead limit = P0 [[graph]] P1 = "foo:x & foo:y => bar" [runtime] [[foo]] script = "cylc message x" [[[outputs]]] x = x y = y [[bar]] cylc-flow-8.6.4/tests/functional/optional-outputs/01-stall-on-incomplete.t0000664000175000017500000000261515202510242026754 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that incomplete tasks count toward runahead limit. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test --debug "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" grep_ok "Incomplete tasks" "${LOG}" grep_ok '1/foo did not complete the required outputs:\n(.*\n){3}.*y\n' "${LOG}" -Pizo grep_ok "Workflow stalled" "${LOG}" purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/00-stall-on-partial/0000775000175000017500000000000015202510242026057 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/00-stall-on-partial/reference.log0000664000175000017500000000007315202510242030520 0ustar alastairalastairInitial point: 1 Final point: None 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/optional-outputs/00-stall-on-partial/flow.cylc0000664000175000017500000000061315202510242027702 0ustar alastairalastair# Should stall at runhead limit with partially satisfied bar [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 runahead limit = P0 [[graph]] P1 = "foo:x & foo:y? => bar" [runtime] [[foo]] script = "cylc message x" [[[outputs]]] x = x y = y [[bar]] cylc-flow-8.6.4/tests/functional/optional-outputs/02-no-stall-on-optional/0000775000175000017500000000000015202510242026664 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/02-no-stall-on-optional/reference.log0000664000175000017500000000012015202510242031316 0ustar alastairalastairInitial point: 1 Final point: 2 1/foo -triggered off [] 2/foo -triggered off [] cylc-flow-8.6.4/tests/functional/optional-outputs/02-no-stall-on-optional/flow.cylc0000664000175000017500000000062715202510242030514 0ustar alastairalastair# Should not stall at runhead limit with incomplete foo [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 runahead limit = P0 [[graph]] P1 = "foo:y? => bar" [runtime] [[foo]] script = "cylc message x" [[[outputs]]] x = x y = y [[bar]] cylc-flow-8.6.4/tests/functional/optional-outputs/test_header0000777000175000017500000000000015202510242030774 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/00-stall-on-partial.t0000664000175000017500000000261515202510242026250 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that partially satisfied prerequisites count toward runahead limit. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test --debug "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" grep_ok "Partially satisfied prerequisites" "${LOG}" grep_ok "1/bar is waiting on \['1/foo:y'\]" "${LOG}" grep_ok "Workflow stalled" "${LOG}" purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/05-c7backcompat-blocked-task-2.t0000664000175000017500000000323615202510242030130 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Cylc 7 stall backward compatibility, multi-parent case . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # It should now validate, with a deprecation message TEST_NAME="${TEST_NAME_BASE}-validate_as_c7" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" DEPR_MSG_1=$(python -c \ 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "${DEPR_MSG_1}" "${TEST_NAME}.stderr" # Should stall and abort with an unsatisfied prerequisite. workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test --debug "${WORKFLOW_NAME}" grep_workflow_log_ok grep-1 "WARNING - Partially satisfied prerequisites" grep_workflow_log_ok grep-2 "Workflow stalled" grep_workflow_log_ok grep-3 "1/baz is waiting on" grep_workflow_log_ok grep-4 'Workflow shutting down \- "abort on stall timeout" is set' purge exit cylc-flow-8.6.4/tests/functional/optional-outputs/05-c7backcompat-blocked-task-2/0000775000175000017500000000000015202510242027737 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/05-c7backcompat-blocked-task-2/suite.rc0000664000175000017500000000103115202510242031411 0ustar alastairalastair[scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] [[dependencies]] [[[R1]]] graph = """ # Workflow should stall # - Cylc 8: incomplete foo, bar # - Cylc 7 back-compat mode: unsatisfied waiting baz foo:x => baz bar:x => baz """ [runtime] [[FOOB]] script = true [[[outputs]]] x = x [[foo, bar]] inherit = FOOB [[baz]] cylc-flow-8.6.4/tests/functional/optional-outputs/04-c7backcompat-blocked-task/0000775000175000017500000000000015202510242027577 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/optional-outputs/04-c7backcompat-blocked-task/suite.rc0000664000175000017500000000071615202510242031262 0ustar alastairalastair[scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] [[dependencies]] [[[R1]]] graph = """ # Workflow should stall # - Cylc 8: incomplete foo # - Cylc 7 back-compat mode: unsatisfied waiting bar foo:x => bar """ [runtime] [[foo]] script = true [[[outputs]]] x = x [[bar]] cylc-flow-8.6.4/tests/functional/cylc-set/0000775000175000017500000000000015202510242020634 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/01-off-flow-pre/0000775000175000017500000000000015202510242023355 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/01-off-flow-pre/reference.log0000664000175000017500000000073315202510242026021 0ustar alastairalastair1/c_cold -triggered off [] in flow 1 1/a_cold -triggered off [] in flow 1 1/b_cold -triggered off [] in flow 1 1/a -triggered off ['1/a_cold'] in flow 1 1/b -triggered off ['1/a', '1/b_cold'] in flow 1 1/c -triggered off ['1/b', '1/c_cold'] in flow 1 1/reflow -triggered off ['1/c'] in flow 1 1/a -triggered off ['1/a_cold'] in flow 2 1/b -triggered off ['1/a', '1/b_cold'] in flow 2 1/c -triggered off ['1/b', '1/c_cold'] in flow 2 1/reflow -triggered off ['1/c'] in flow 2 cylc-flow-8.6.4/tests/functional/cylc-set/01-off-flow-pre/flow.cylc0000664000175000017500000000203715202510242025202 0ustar alastairalastair# start a new flow after setting off-flow prerequites to avoid stall. [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True inactivity timeout = PT30S abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ # the tasks we want the flow to run a => b => c => reflow # the off-flow prerequisites a_cold => a b_cold => b c_cold => c """ [runtime] [[a, b, c]] [[a_cold, b_cold, c_cold]] [[reflow]] script = """ if (( CYLC_TASK_SUBMIT_NUMBER == 1 )); then # set off-flow prerequisites (and trigger 1/a) cylc set --flow=new \ --pre=1/a_cold:succeeded \ --pre=1/b_cold:succeeded \ --pre=1/c_cold:succeeded \ ${CYLC_WORKFLOW_ID}//1/a \ ${CYLC_WORKFLOW_ID}//1/b \ ${CYLC_WORKFLOW_ID}//1/c fi """ cylc-flow-8.6.4/tests/functional/cylc-set/02-off-flow-out.t0000664000175000017500000000401715202510242023566 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 2 - Set off-flow outputs to prevent a new flow from stalling. # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#2-set-off-flow-prerequisites-to-prep-for-a-new-flow . "$(dirname "$0")/test_header" set_test_number 11 install_and_validate reftest_run # Check that we set: # - all the required outputs of a_cold # - the requested and implied outputs of b_cold and c_cold grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a1" '1/a_cold.* setting implied output: submitted' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a2" '1/a_cold.* setting implied output: started' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a3" '1/a_cold.* completed' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a1" '1/b_cold.* setting implied output: submitted' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a2" '1/b_cold.* setting implied output: started' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-b3" '1/b_cold.* completed' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a1" '1/c_cold.* setting implied output: submitted' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-a2" '1/c_cold.* setting implied output: started' grep_workflow_log_ok "${TEST_NAME_BASE}-grep-c3" '1/c_cold.* completed' purge cylc-flow-8.6.4/tests/functional/cylc-set/04-switch.t0000664000175000017500000000406315202510242022546 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 5 - Set and complete a future switch task with the "--wait" flag # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#5-set-switch-tasks-at-an-optional-branch-point-to-direct-the-future-flow . "$(dirname "$0")/test_header" set_test_number 5 install_and_validate reftest_run # The branch-point task foo should be recorded as succeeded. sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT status FROM task_states WHERE name is 'foo'" > db-foo.2 cmp_ok "db-foo.2" - << __OUT__ succeeded __OUT__ # the outputs of foo should be recorded as: # a, succeeded # and the implied outputs (of succeeded) as well: # submitted, started sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT outputs FROM task_outputs WHERE name is 'foo'" > db-foo.1 # Json string list of outputs from the db may not be ordered correctly. python3 - << __END__ > db-foo.2 import json with open("db-foo.1", 'r') as f: print( ','.join( sorted( json.load(f) ) ) ) __END__ cmp_ok "db-foo.2" - << __OUT__ a,started,submitted,succeeded __OUT__ # Check the flow-wait worked grep_workflow_log_ok check-wait "1/foo.* spawning outputs after flow-wait" -E purge cylc-flow-8.6.4/tests/functional/cylc-set/06-parentless.t0000664000175000017500000000233615202510242023430 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 7 - Check spawning a parentless task without ignoring xtriggers. # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#7-spawning-parentless-tasks . "$(dirname "$0")/test_header" set_test_number 3 install_and_validate REFTEST_OPTS="--start-task=1800/a" reftest_run grep_workflow_log_ok "${TEST_NAME_BASE}-clock" "xtrigger succeeded: wall_clock" purge cylc-flow-8.6.4/tests/functional/cylc-set/08-switch2.t0000664000175000017500000000215015202510242022627 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 5 - Set and complete a future switch task. # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#5-set-switch-tasks-at-an-optional-branch-point-to-direct-the-future-flow . "$(dirname "$0")/test_header" set_test_number 2 reftest purge cylc-flow-8.6.4/tests/functional/cylc-set/05-expire.t0000664000175000017500000000265115202510242022543 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 6 - check that forced task expiry works # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#6-expire-a-task . "$(dirname "$0")/test_header" set_test_number 4 install_and_validate reftest_run sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT status FROM task_states WHERE name is 'bar'" > db-bar.1 cmp_ok "db-bar.1" - << __OUT__ expired __OUT__ sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT outputs FROM task_outputs WHERE name is 'bar'" > db-bar.2 cmp_ok "db-bar.2" - << __OUT__ {"expired": "(manually completed)"} __OUT__ purge cylc-flow-8.6.4/tests/functional/cylc-set/02-off-flow-out/0000775000175000017500000000000015202510242023377 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/02-off-flow-out/reference.log0000664000175000017500000000073315202510242026043 0ustar alastairalastair1/c_cold -triggered off [] in flow 1 1/a_cold -triggered off [] in flow 1 1/b_cold -triggered off [] in flow 1 1/a -triggered off ['1/a_cold'] in flow 1 1/b -triggered off ['1/a', '1/b_cold'] in flow 1 1/c -triggered off ['1/b', '1/c_cold'] in flow 1 1/reflow -triggered off ['1/c'] in flow 1 1/a -triggered off ['1/a_cold'] in flow 2 1/b -triggered off ['1/a', '1/b_cold'] in flow 2 1/c -triggered off ['1/b', '1/c_cold'] in flow 2 1/reflow -triggered off ['1/c'] in flow 2 cylc-flow-8.6.4/tests/functional/cylc-set/02-off-flow-out/flow.cylc0000664000175000017500000000161415202510242025224 0ustar alastairalastair# start a new flow after setting off-flow outputs to avoid stall. [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True inactivity timeout = PT30S abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ # the tasks we want the flow to run a => b => c => reflow # the off-flow prerequisites a_cold => a b_cold => b c_cold => c """ [runtime] [[a, b, c]] [[a_cold, b_cold, c_cold]] [[reflow]] script = """ if (( CYLC_TASK_SUBMIT_NUMBER == 1 )); then # set off-flow outputs of x_cold cylc set --flow=new \ ${CYLC_WORKFLOW_ID}//1/a_cold \ ${CYLC_WORKFLOW_ID}//1/b_cold \ ${CYLC_WORKFLOW_ID}//1/c_cold fi """ cylc-flow-8.6.4/tests/functional/cylc-set/09-set-skip.t0000664000175000017500000000221215202510242023003 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # # Skip Mode proposal example: # https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md # The cylc set --out option should accept the skip value # which should set the outputs defined in # [runtime][][skip]outputs. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-set/03-set-failed/0000775000175000017500000000000015202510242023071 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/03-set-failed/flow.cylc0000664000175000017500000000060615202510242024716 0ustar alastairalastair# A single task that dies silently, requiring set to failed [scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] init-script = cylc__job__disable_fail_signals script = """ cylc__job__wait_cylc_message_started exit 1 """ cylc-flow-8.6.4/tests/functional/cylc-set/00-set-succeeded/0000775000175000017500000000000015202510242023566 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/00-set-succeeded/reference.log0000664000175000017500000000034515202510242026231 0ustar alastairalastair1/setter -triggered off ['1/bar', '1/foo'] in flow 1 1/foo -triggered off [] in flow 1 1/bar -triggered off [] in flow 1 1/post_m1 -triggered off ['1/bar', '1/foo'] in flow 1 1/post_m2 -triggered off ['1/bar', '1/foo'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/00-set-succeeded/flow.cylc0000664000175000017500000000211315202510242025406 0ustar alastairalastair# 1. foo and bar fail incomplete. # 2. setter sets foo and bar to succeeded. # 3. foo and bar are completed, post runs, scheduler shuts down. [scheduler] [[events]] inactivity timeout = PT30S abort on inactivity timeout = True expected task failures = 1/foo, 1/bar [task parameters] m = 1..2 [scheduling] [[graph]] R1 = """ foo & bar => post foo:started & bar:started => setter """ [runtime] [[post]] [[foo, bar]] script = false [[setter]] script = """ # wait for foo and bar to fail. for TASK in foo bar do cylc workflow-state \ ${CYLC_WORKFLOW_ID}//${CYLC_TASK_CYCLE_POINT}/${TASK}:failed \ --max-polls=20 --interval=1 done # set foo succeeded (via --output) cylc set -o succeeded $CYLC_WORKFLOW_ID//$CYLC_TASK_CYCLE_POINT/foo # set bar succeeded (via default) cylc set $CYLC_WORKFLOW_ID//$CYLC_TASK_CYCLE_POINT/bar """ cylc-flow-8.6.4/tests/functional/cylc-set/03-set-failed.t0000664000175000017500000000403715202510242023262 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 4 -check that we can set a dead orphaned job to failed. # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#4-set-jobs-to-failed-when-a-job-platform-is-known-to-be-down . "$(dirname "$0")/test_header" set_test_number 3 install_and_validate run_ok play-it cylc play --debug "${WORKFLOW_NAME}" poll_grep_workflow_log -E "1/foo.* \(internal\)submitted" cylc set -o failed "${WORKFLOW_NAME}//1/foo" # Check the log for: # - set completion message # - implied outputs reported as already completed poll_grep_workflow_log -E "1/foo.* => failed" poll_grep_workflow_log -E "1/foo.* did not complete the required outputs" cylc stop --now --now --interval=2 --max-polls=5 "${WORKFLOW_NAME}" # Check the DB records all the outputs. sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT outputs FROM task_outputs WHERE name is 'foo'" > db-foo.1 # Json string list of outputs from the db may not be ordered correctly. python3 - << __END__ > db-foo.2 import json with open("db-foo.1", 'r') as f: print( ','.join( sorted( json.load(f) ) ) ) __END__ cmp_ok "db-foo.2" - << __OUT__ failed,started,submitted __OUT__ purge cylc-flow-8.6.4/tests/functional/cylc-set/00-set-succeeded.t0000664000175000017500000000307315202510242023756 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # "cylc set" proposal examples: 1 - Carry on as if a failed task had succeeded # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#1-carry-on-as-if-a-failed-task-had-succeeded . "$(dirname "$0")/test_header" set_test_number 6 install_and_validate reftest_run for TASK in foo bar do sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT status FROM task_states WHERE name is '$TASK'" > "${TASK}.1" cmp_ok ${TASK}.1 - << "${TASK}.2" cmp_json \ "check-${TASK}-outputs" \ "${TASK}.2" \ "${TASK}.2"<<<'["submitted", "started", "succeeded", "hello"]' done purge cylc-flow-8.6.4/tests/functional/cylc-set/08-switch2/0000775000175000017500000000000015202510242022444 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/08-switch2/reference.log0000664000175000017500000000130415202510242025103 0ustar alastairalastair# 1/a runs naturally and generates the output "x" 1/a -triggered off [] in flow 1 1/x -triggered off ['1/a'] in flow 1 1/z -triggered off ['1/x'] in flow 1 # 1/a is artificially completed with the output "y" 2/y -triggered off ['2/a'] in flow 1 2/z -triggered off ['2/y'] in flow 1 # 1/a has the output "y" is artificially set but is not completed # (so 1/a will re-run and generate the output "x" naturally) 3/a -triggered off [] in flow 1 3/x -triggered off ['3/a'] in flow 1 3/y -triggered off ['3/a'] in flow 1 3/z -triggered off ['3/y'] in flow 1 # 1/a runs naturally and generates the output "x" 4/a -triggered off [] in flow 1 4/x -triggered off ['4/a'] in flow 1 4/z -triggered off ['4/x'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/08-switch2/flow.cylc0000664000175000017500000000241415202510242024270 0ustar alastairalastair # Complete a parentless switch task that already exists in the pool but is # beyond the runahead limit. Cylc should auto-spawn its next instance to # avoid premature shutdown when it is removed as complete. # (We only spawn the first runahead-limited instance of parentless tasks). [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1 final cycle point = 4 cycling mode = integer runahead limit = P0 [[graph]] P1 = """ a:x? => x a:y? => y x | y => z """ [runtime] [[a]] script = """ cylc__job__wait_cylc_message_started cylc message -- x # always go x-path """ [[[outputs]]] x = x y = y [[z]] script = """ if (( CYLC_TASK_CYCLE_POINT == 1 )); then # mark 2/a as succeeded with output y # (task will be skipped) cylc set "${CYLC_WORKFLOW_ID}//2/a" --out=y,succeeded elif (( CYLC_TASK_CYCLE_POINT == 2 )); then # mark 2/a as having generated output y # (task will re-run and generate output x in the prociess) cylc set "${CYLC_WORKFLOW_ID}//3/a" --out=y fi """ cylc-flow-8.6.4/tests/functional/cylc-set/test_header0000777000175000017500000000000015202510242027151 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/09-set-skip/0000775000175000017500000000000015202510242022621 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/09-set-skip/reference.log0000664000175000017500000000035215202510242025262 0ustar alastairalastair1/bar -triggered off [] in flow 1 1/foo -triggered off [] in flow 1 1/do_skip -triggered off ['1/bar', '1/foo'] in flow 1 1/foo_done -triggered off ['1/foo', '1/foo', '1/foo'] in flow 1 1/bar_failed -triggered off ['1/bar'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/09-set-skip/flow.cylc0000664000175000017500000000253115202510242024445 0ustar alastairalastair[meta] test_description = """ Test that cylc set --out skip satisfies all outputs which are required by the graph. """ proposal url = https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md [scheduler] allow implicit tasks = true [[events]] expected task failures = 1/bar [scheduling] [[graph]] R1 = """ # Optional out not created by set --out skip foo:no? => not_this_task # set --out skip creates required, started, submitted # and succeeded (unless failed is set): foo:yes => foo_done foo:submitted => foo_done foo:succeeded => foo_done foo:started => do_skip # set --out skip creates failed if that is required # by skip mode settings: bar:started => do_skip bar:failed? => bar_failed """ [runtime] [[foo]] script = sleep 100 [[[skip]]] outputs = yes [[[outputs]]] no = Don't require this task yes = Require this task [[bar]] script = sleep 100 [[[skip]]] outputs = failed [[do_skip]] script = """ cylc set --out skip ${CYLC_WORKFLOW_ID}//1/foo \ ${CYLC_WORKFLOW_ID}//1/bar """ cylc-flow-8.6.4/tests/functional/cylc-set/01-off-flow-pre.t0000664000175000017500000000324515202510242023546 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # # "cylc set" proposal examples: 2 - Set off-flow prerequisites to prevent a new flow from stalling. # https://cylc.github.io/cylc-admin/proposal-cylc-set.html#2-set-off-flow-prerequisites-to-prep-for-a-new-flow . "$(dirname "$0")/test_header" set_test_number 8 install_and_validate reftest_run grep_workflow_log_ok "${TEST_NAME_BASE}-ab" '1/a does not depend on "1/b_cold:succeeded"' grep_workflow_log_ok "${TEST_NAME_BASE}-ac" '1/a does not depend on "1/c_cold:succeeded"' grep_workflow_log_ok "${TEST_NAME_BASE}-ba" '1/b does not depend on "1/a_cold:succeeded"' grep_workflow_log_ok "${TEST_NAME_BASE}-bc" '1/b does not depend on "1/c_cold:succeeded"' grep_workflow_log_ok "${TEST_NAME_BASE}-ca" '1/c does not depend on "1/a_cold:succeeded"' grep_workflow_log_ok "${TEST_NAME_BASE}-cb" '1/c does not depend on "1/b_cold:succeeded"' purge cylc-flow-8.6.4/tests/functional/cylc-set/06-parentless/0000775000175000017500000000000015202510242023237 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/06-parentless/reference.log0000664000175000017500000000030415202510242025675 0ustar alastairalastairStart task: ['1800/a'] 18000101T0000Z/a -triggered off [] in flow 1 18000101T0000Z/x -triggered off [] in flow 1 18000101T0000Z/b -triggered off ['18000101T0000Z/a', '18000101T0000Z/x'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/06-parentless/flow.cylc0000664000175000017500000000073615202510242025070 0ustar alastairalastair# Start this with --start-task=1800/a. # Task a's script should spawn x. # The log should show a clock-trigger check before x runs. [scheduler] [[events]] inactivity timeout = PT30S abort on inactivity timeout = True [scheduling] initial cycle point = 1800 [[graph]] R1 = """ a => b @wall_clock => x => b """ [runtime] [[a]] script = cylc set --pre=all "${CYLC_WORKFLOW_ID}//1800/x" [[b, x]] cylc-flow-8.6.4/tests/functional/cylc-set/05-expire/0000775000175000017500000000000015202510242022352 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/05-expire/reference.log0000664000175000017500000000012315202510242025007 0ustar alastairalastair1/expirer -triggered off [] in flow 1 1/foo -triggered off ['1/expirer'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/05-expire/flow.cylc0000664000175000017500000000107315202510242024176 0ustar alastairalastair# Expire an inactive task, so it won't run. [scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = """ # bar and baz should not run if bar expires expirer => foo => bar? => baz bar:expired? """ [runtime] [[expirer]] script = """ cylc set --output=expired ${CYLC_WORKFLOW_ID}//1/bar """ [[foo]] [[bar, baz]] script = false cylc-flow-8.6.4/tests/functional/cylc-set/04-switch/0000775000175000017500000000000015202510242022356 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-set/04-switch/reference.log0000664000175000017500000000011615202510242025015 0ustar alastairalastair1/switcher -triggered off [] in flow 1 1/a -triggered off ['1/foo'] in flow 1 cylc-flow-8.6.4/tests/functional/cylc-set/04-switch/flow.cylc0000664000175000017500000000146015202510242024202 0ustar alastairalastair# Set outputs of inactive task to direct the flow at an optional branch point. [scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = """ switcher => foo foo:a? => a foo:b? => b """ [runtime] [[switcher]] script = """ cylc set --output=a,succeeded --wait ${CYLC_WORKFLOW_ID}//1/foo # wait for command actioned, to avoid race condition cylc__job__poll_grep_workflow_log "actioned" """ [[foo]] script = "cylc message b" # always go b-way if I run [[[outputs]]] a = a b = b [[a]] [[b]] script = false cylc-flow-8.6.4/tests/functional/events/0000775000175000017500000000000015202510242020415 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/28-inactivity.t0000775000175000017500000000235615202510242023225 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, abort on inactivity . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok "inactivity timer timed out" "${TEST_NAME_BASE}-run.stderr" purge exit cylc-flow-8.6.4/tests/functional/events/timeout/0000775000175000017500000000000015202510242022103 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/timeout/flow.cylc0000664000175000017500000000043215202510242023725 0ustar alastairalastair[meta] description = This workflow is supposed to time out [scheduler] [[events]] workflow timeout = PT6S abort on workflow timeout = True [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = "cylc pause $CYLC_WORKFLOW_ID" cylc-flow-8.6.4/tests/functional/events/33-task-event-job-logs-retrieve-3.t0000775000175000017500000000400615202510242026606 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote job logs retrieval OK with only "job.out" on a succeeded task. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config "" " [platforms] [[blackbriar]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True retrieve job logs retry delays = 2*PT5S " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test -vv --no-detach "${WORKFLOW_NAME}" sed "/'job-logs-retrieve'/!d" \ "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job-activity.log" \ >'edited-activities.log' cmp_ok 'edited-activities.log' <<'__LOG__' [(('job-logs-retrieve', 'failed'), 1) ret_code] 1 [(('job-logs-retrieve', 'failed'), 1) err] File(s) not retrieved: job.err [(('job-logs-retrieve', 'failed'), 1) ret_code] 1 [(('job-logs-retrieve', 'failed'), 1) err] File(s) not retrieved: job.err __LOG__ exists_ok "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.out" exists_fail "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.err" purge exit cylc-flow-8.6.4/tests/functional/events/34-task-abort.t0000775000175000017500000000500515202510242023100 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job abort-with-message and interaction with failed handler. . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # Check failed handler only call on last try. LOG="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job-activity.log" grep "event-handler" "${LOG}" > 'edited-job-activity.log' cmp_ok 'edited-job-activity.log' <<'__LOG__' [(('event-handler-00', 'failed'), 2) cmd] echo "!!!FAILED!!!" failed 1/foo 2 '"ERROR: rust never sleeps"' [(('event-handler-00', 'failed'), 2) ret_code] 0 [(('event-handler-00', 'failed'), 2) out] !!!FAILED!!! failed 1/foo 2 "ERROR: rust never sleeps" __LOG__ #------------------------------------------------------------------------------- # Check job stdout stops at the abort call. LOG="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job.out" # ...before abort grep_ok 'ONE' "${LOG}" # ...after abort grep_fail 'TWO' "${LOG}" #------------------------------------------------------------------------------- # Check only one CYLC_JOB_EXIT message written. JOB_STATUS="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job.status" run_ok "${TEST_NAME_BASE}-message-count" \ test "$(grep -c '^CYLC_JOB_EXIT=' "$JOB_STATUS")" -eq '1' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/events/41-late.t0000775000175000017500000000242215202510242021754 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test late event handler . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok 'late (late-time=.*)' "${WORKFLOW_RUN_DIR}/log/scheduler/log" grep_ok 'late (late-time=.*)' "${WORKFLOW_RUN_DIR}/log/scheduler/my-handler.out" purge exit cylc-flow-8.6.4/tests/functional/events/38-task-event-handler-custom.t0000775000175000017500000000301615202510242026041 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test custom severity event handling. . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" FOO_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job-activity.log" WORKFLOW_LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" grep_ok \ "\[(('event-handler-00', 'custom-1'), 1) out\] !!CUSTOM!! 1/foo fugu Data ready for barring" \ "${FOO_ACTIVITY_LOG}" grep_ok "1/foo.*Data ready for barring" "${WORKFLOW_LOG}" -E grep_ok "1/foo.*Data ready for bazzing" "${WORKFLOW_LOG}" -E grep_ok "1/foo.*Aren't the hydrangeas nice" "${WORKFLOW_LOG}" -E purge cylc-flow-8.6.4/tests/functional/events/19-workflow-event-mail-globalcfg0000777000175000017500000000000015202510242032473 218-workflow-event-mailustar alastairalastaircylc-flow-8.6.4/tests/functional/events/38-task-event-handler-custom/0000775000175000017500000000000015202510242025651 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/38-task-event-handler-custom/reference.log0000664000175000017500000000016615202510242030315 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/events/38-task-event-handler-custom/flow.cylc0000664000175000017500000000171115202510242027474 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """ foo:a => bar foo:b => baz """ [runtime] [[root]] script = true [[[events]]] custom handlers = echo !!CUSTOM!! %(point)s/%(name)s %(fish)s %(message)s [[[meta]]] fish = trout [[foo]] script = """ cylc__job__wait_cylc_message_started # Output message for triggering, and custom event handler. cylc message -p CUSTOM "Data ready for barring" # Generic message, not for triggering or custom event handler. cylc message "Aren't the hydrangeas nice?" # Output message for triggering, not custom event handler. cylc message "Data ready for bazzing" """ [[[outputs]]] a = "Data ready for barring" b = "Data ready for bazzing" [[[meta]]] fish = fugu cylc-flow-8.6.4/tests/functional/events/06-stall-timeout-ref-simulation.t0000775000175000017500000000346515202510242026577 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow reference test simulation stall timeout workflow . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'stall-timeout-ref' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" RUN_MODE="$(basename "$0" | sed "s/.*-ref-\(.*\).t/\1/g")" workflow_run_fail "${TEST_NAME}" \ cylc play --reference-test --mode="${RUN_MODE}" --debug --no-detach \ "${WORKFLOW_NAME}" grep_ok "WARNING - stall timer timed out after PT1S" "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/events/21-workflow-event-handlers-globalcfg0000777000175000017500000000000015202510242034211 220-workflow-event-handlersustar alastairalastaircylc-flow-8.6.4/tests/functional/events/48-workflow-aborted.t0000775000175000017500000000261115202510242024326 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, abort on stall setting . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" >'log' grep_ok 'CRITICAL - Workflow shutting down - contact file modified' 'log' cmp_ok "${WORKFLOW_RUN_DIR}/handler.out" <<'__OUT__' abort contact file modified __OUT__ purge exit cylc-flow-8.6.4/tests/functional/events/19-workflow-event-mail-globalcfg.t0000777000175000017500000000000015202510242033177 218-workflow-event-mail.tustar alastairalastaircylc-flow-8.6.4/tests/functional/events/26-workflow-stalled-dump-prereq/0000775000175000017500000000000015202510242026401 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/26-workflow-stalled-dump-prereq/reference.log0000664000175000017500000000021615202510242031041 0ustar alastairalastairInitial point: 20100101T0000Z Final point: None 20100101T0000Z/foo -triggered off [] 20100101T0000Z/bar -triggered off ['20100101T0000Z/foo'] cylc-flow-8.6.4/tests/functional/events/26-workflow-stalled-dump-prereq/flow.cylc0000664000175000017500000000077615202510242030236 0ustar alastairalastair[scheduler] UTC mode = True # Ignore DST [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 20100101T0000Z/bar [scheduling] initial cycle point = 20100101T0000Z [[graph]] # will abort on stall with unhandled failed bar T00, T06, T12, T18 = foo[-PT6H] & bar[-PT6H] => foo => bar => qux T12 = qux[-PT6H] => baz [runtime] [[root]] script = true [[foo,baz, qux]] [[bar]] script = false cylc-flow-8.6.4/tests/functional/events/41-late/0000775000175000017500000000000015202510242021564 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/41-late/bin/0000775000175000017500000000000015202510242022334 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/41-late/bin/my-handler0000775000175000017500000000014315202510242024320 0ustar alastairalastair#!/usr/bin/env bash set -eu ME="$(basename "$0")" echo "$@" >>"${CYLC_WORKFLOW_LOG_DIR}/${ME}.out" cylc-flow-8.6.4/tests/functional/events/41-late/flow.cylc0000664000175000017500000000044615202510242023413 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = now [[graph]] R1 = t1 => t2 [runtime] [[t1]] script = sleep 61 [[t2]] script = true [[[events]]] late offset = PT1M late handlers = my-handler %(message)s cylc-flow-8.6.4/tests/functional/events/27-workflow-stalled-dump-prereq-fam.t0000775000175000017500000000346515202510242027343 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, dump unmet prereqs on stall . "$(dirname "$0")/test_header" set_test_number 9 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" grep_ok '"abort on stall timeout" is set' "${TEST_NAME_BASE}-run.stderr" grep_ok "ERROR - Incomplete tasks:" "${TEST_NAME_BASE}-run.stderr" grep_ok "1/foo did not complete the required outputs:\n.*succeeded" \ "${TEST_NAME_BASE}-run.stderr" \ -Pizo grep_ok "WARNING - Partially satisfied prerequisites:" \ "${TEST_NAME_BASE}-run.stderr" grep_ok "1/f_1 is waiting on \['1/foo:succeeded'\]" \ "${TEST_NAME_BASE}-run.stderr" grep_ok "1/f_2 is waiting on \['1/foo:succeeded'\]" \ "${TEST_NAME_BASE}-run.stderr" grep_ok "1/f_3 is waiting on \['1/foo:succeeded'\]" \ "${TEST_NAME_BASE}-run.stderr" purge exit cylc-flow-8.6.4/tests/functional/events/20-workflow-event-handlers.t0000775000175000017500000000415215202510242025615 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, flexible interface . "$(dirname "$0")/test_header" set_test_number 4 OPT_SET= if [[ "${TEST_NAME_BASE}" == *-globalcfg ]]; then create_test_global_config "" " [scheduler] [[events]] handlers = echo 'Your %(workflow)s workflow has a %(event)s event and URL %(workflow_url)s and workflow-priority as %(workflow-priority)s and workflow-UUID as %(uuid)s.' handler events = startup" OPT_SET='-s GLOBALCFG=True' fi install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach ${OPT_SET} "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log" WORKFLOW_UUID="$(sqlite3 "${LOGD}/db" "SELECT value FROM workflow_params WHERE key=='uuid_str'")" LOG_FILE="${LOGD}/scheduler/log" grep_ok "\\[('workflow-event-handler-00', 'startup') ret_code\\] 0" "${LOG_FILE}" grep_ok "\\[('workflow-event-handler-00', 'startup') out\\] Your ${WORKFLOW_NAME} workflow has a startup event and URL http://myworkflows.com/${WORKFLOW_NAME}.html and workflow-priority as HIGH and workflow-UUID as ${WORKFLOW_UUID}." "${LOG_FILE}" purge exit cylc-flow-8.6.4/tests/functional/events/13-task-event-mail-globalcfg.t0000777000175000017500000000000015202510242031351 209-task-event-mail.tustar alastairalastaircylc-flow-8.6.4/tests/functional/events/35-task-event-handler-importance/0000775000175000017500000000000015202510242026475 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/35-task-event-handler-importance/flow.cylc0000664000175000017500000000071315202510242030321 0ustar alastairalastair[meta] priority = HIGH [scheduling] [[graph]] R1 = t1:fail => dummy [runtime] [[dummy]] script = true [[t1]] script = false [[[meta]]] URL = http://example.com importance = 3 color = red [[[events]]] failed handlers = echo 'NAME =' %(name)s 'POINT =' %(point)s 'IMPORTANCE =' %(importance)s 'COLOR =' %(color)s 'WORKFLOW-PRIORITY =' %(workflow_priority)s cylc-flow-8.6.4/tests/functional/events/24-workflow-timeout/0000775000175000017500000000000015202510242024176 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/24-workflow-timeout/flow.cylc0000664000175000017500000000074015202510242026022 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] workflow timeout = PT1S workflow timeout handlers = echo "That was quick!" abort on workflow timeout = {{ABORT}} [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ cylc__job__poll_grep_workflow_log "workflow timer timed out" if [[ "{{ABORT}}" == "True" ]]; then cylc__job__poll_grep_workflow_log "Workflow shutting down" fi """ cylc-flow-8.6.4/tests/functional/events/51-task-event-job-logs-retrieve-4.t0000664000175000017500000000531215202510242026605 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- export REQUIRE_PLATFORM="loc:remote fs:indep comms:?(tcp|ssh)" . "$(dirname "$0")/test_header" set_test_number 3 #------------------------------------------------------------------------------- # It should retry job log retrieval even if all hosts are not contactable. #------------------------------------------------------------------------------- init_workflow "${TEST_NAME_BASE}-1" <<__FLOW_CONFIG__ [scheduling] [[graph]] R1 = """ remote """ [runtime] [[remote]] # script = sleep 1 platform = ${CYLC_TEST_PLATFORM} __FLOW_CONFIG__ # configure job retries on the test platform create_test_global_config '' " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True retrieve job logs retry delays = 3*PT1S retrieve job logs command = fido " # * redirect retrieval attempts to a file where we can inspect them later # * make it look like retrieval failed due to network issues (255 ret code) JOB_LOG_RETR_CMD="${WORKFLOW_RUN_DIR}/bin/fido" RETRIEVAL_ATTEMPT_LOG="${WORKFLOW_RUN_DIR}/retrieval-attempt-log" mkdir "${WORKFLOW_RUN_DIR}/bin" cat > "${WORKFLOW_RUN_DIR}/bin/fido" <<__HERE__ #!/usr/bin/env bash echo "$@" >> "${RETRIEVAL_ATTEMPT_LOG}" exit 255 __HERE__ chmod +x "${JOB_LOG_RETR_CMD}" workflow_run_ok "${TEST_NAME_BASE}-play" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" # it should try retrieval three times # Note: it should reset bad_hosts to allow retries to happen TEST_NAME="${TEST_NAME_BASE}-retrieve-attempts" # shellcheck disable=SC2002 # (cat'ting into pipe to avoid having to sed out the filename) if [[ $(cat "${RETRIEVAL_ATTEMPT_LOG}" | wc -l) -eq 3 ]]; then ok "${TEST_NAME}" else fail "${TEST_NAME}" fi # then fail once the retries have been exhausted grep_workflow_log_ok "${TEST_NAME_BASE}-retrieve-fail" \ 'job-logs-retrieve for task event:succeeded failed' purge cylc-flow-8.6.4/tests/functional/events/00-workflow/0000775000175000017500000000000015202510242022504 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/00-workflow/flow.cylc0000664000175000017500000000103715202510242024330 0ustar alastairalastair# test some workflow event handler hooks [scheduler] [[events]] startup handlers = echo HELLO STARTUP workflow timeout = PT2S workflow timeout handlers = echo HELLO TIMEOUT abort on workflow timeout = False inactivity timeout = PT3S inactivity timeout handlers = echo HELLO INACTIVITY abort on inactivity timeout = False shutdown handlers = echo HELLO SHUTDOWN [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] platform = localhost script = sleep 10 cylc-flow-8.6.4/tests/functional/events/26-workflow-stalled-dump-prereq.t0000775000175000017500000000324715202510242026577 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, dump unmet prereqs on stall . "$(dirname "$0")/test_header" set_test_number 7 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" grep_ok '"abort on stall timeout" is set' "${TEST_NAME_BASE}-run.stderr" grep_ok "ERROR - Incomplete tasks:" "${TEST_NAME_BASE}-run.stderr" grep_ok "20100101T0000Z/bar did not complete the required outputs:\n.*succeeded" \ "${TEST_NAME_BASE}-run.stderr" \ -Pizo grep_ok "WARNING - Partially satisfied prerequisites:" \ "${TEST_NAME_BASE}-run.stderr" grep_ok "20100101T0600Z/foo is waiting on \['20100101T0000Z/bar:succeeded'\]" \ "${TEST_NAME_BASE}-run.stderr" purge exit cylc-flow-8.6.4/tests/functional/events/09-task-event-mail.t0000775000175000017500000000437415202510242024044 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test event mail, 2 different task events . "$(dirname "$0")/test_header" if ! command -v mail 2>'/dev/null'; then skip_all '"mail" command not available' fi set_test_number 6 mock_smtpd_init OPT_SET= if [[ "${TEST_NAME_BASE}" == *-globalcfg ]]; then create_test_global_config "" " [scheduler] [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ smtp = ${TEST_SMTPD_HOST} [task events] mail events = failed, retry, succeeded " OPT_SET='-s GLOBALCFG=True' else create_test_global_config " [scheduler] [[mail]] smtp = ${TEST_SMTPD_HOST} " fi install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach ${OPT_SET} "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-grep-log-1" \ grep -Pizo 'job: 1/t1/01.*\n.*event: retry' "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log-2" \ grep -Pizo 'job: 1/t1/02.*\n.*event: succeeded' "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log-3" \ grep -Pizo "Subject: \[1/t1/01 retry\].*(\n)?.* ${WORKFLOW_NAME}" "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log-4" \ grep -Pizo "Subject: \[1/t1/02 succeeded\].*(\n)?.* ${WORKFLOW_NAME}" "${TEST_SMTPD_LOG}" purge mock_smtpd_kill exit cylc-flow-8.6.4/tests/functional/events/23-workflow-stalled-handler/0000775000175000017500000000000015202510242025552 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/23-workflow-stalled-handler/reference.log0000664000175000017500000000016615202510242030216 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] cylc-flow-8.6.4/tests/functional/events/23-workflow-stalled-handler/flow.cylc0000664000175000017500000000061415202510242027376 0ustar alastairalastair[scheduler] [[events]] stall handlers = "cylc set %(workflow)s//1/bar" stall timeout = PT0S abort on stall timeout = False expected task failures = 1/bar [scheduling] [[graph]] R1 = foo => bar => baz [runtime] [[foo]] script = true [[bar]] script = false [[baz]] script = cylc remove "$CYLC_WORKFLOW_ID//1/bar" cylc-flow-8.6.4/tests/functional/events/42-late-then-restart/0000775000175000017500000000000015202510242024203 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/42-late-then-restart/bin/0000775000175000017500000000000015202510242024753 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/42-late-then-restart/bin/my-handler0000775000175000017500000000014315202510242026737 0ustar alastairalastair#!/usr/bin/env bash set -eu ME="$(basename "$0")" echo "$@" >>"${CYLC_WORKFLOW_LOG_DIR}/${ME}.out" cylc-flow-8.6.4/tests/functional/events/42-late-then-restart/flow.cylc0000664000175000017500000000051515202510242026027 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = now [[graph]] R1 = t1 => t2 [runtime] [[t1]] script = cylc stop --now "${CYLC_WORKFLOW_ID}"; sleep 61 [[t2]] script = true [[[events]]] late offset = PT1M late handlers = my-handler %(message)s cylc-flow-8.6.4/tests/functional/events/33-task-event-job-logs-retrieve-3/0000775000175000017500000000000015202510242026416 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/33-task-event-job-logs-retrieve-3/reference.log0000664000175000017500000000006715202510242031062 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/33-task-event-job-logs-retrieve-3/flow.cylc0000664000175000017500000000055315202510242030244 0ustar alastairalastair#!jinja2 [meta] title=Task Event Job Log Retrieve 1 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT1M expected task failures = 1/t1 [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = false err-script = rm -f "${CYLC_TASK_LOG_ROOT}.err" platform = blackbriar cylc-flow-8.6.4/tests/functional/events/13-task-event-mail-globalcfg0000777000175000017500000000000015202510242030645 209-task-event-mailustar alastairalastaircylc-flow-8.6.4/tests/functional/events/30-task-event-mail-2.t0000775000175000017500000000447215202510242024174 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test event mail. . "$(dirname "$0")/test_header" if ! command -v mail 2>'/dev/null'; then skip_all '"mail" command not available' fi set_test_number 15 mock_smtpd_init OPT_SET=() if [[ "${TEST_NAME_BASE}" == *-globalcfg ]]; then create_test_global_config "" " [scheduler] [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ smtp = ${TEST_SMTPD_HOST} [task events] mail events = failed, retry, succeeded " OPT_SET=(-s 'GLOBALCFG=True') else create_test_global_config " [scheduler] [[mail]] smtp = ${TEST_SMTPD_HOST} " fi install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${OPT_SET[@]}" "$WORKFLOW_NAME" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${OPT_SET[@]}" "$WORKFLOW_NAME" # 1 - retry for i in {1..5}; do run_ok "${TEST_NAME_BASE}-t${i}-01" grep -Pizo "job: 1/t${i}/01.*\n.*event: retry" "$TEST_SMTPD_LOG" done # 2 - fail for i in {1..5}; do run_ok "${TEST_NAME_BASE}-t${i}-02" grep -Pizo "job: 1/t${i}/02.*\n.*event: failed" "$TEST_SMTPD_LOG" done contains_ok "${TEST_SMTPD_LOG}" <<__LOG__ see: http://localhost/stuff/${USER}/${WORKFLOW_NAME}/ __LOG__ run_ok "${TEST_NAME_BASE}-grep-log" \ grep -qPizo "Subject: \[. tasks retry\]\n? ${WORKFLOW_NAME}" "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log" \ grep -qPizo "Subject: \[. tasks failed\]\n? ${WORKFLOW_NAME}" "${TEST_SMTPD_LOG}" purge mock_smtpd_kill cylc-flow-8.6.4/tests/functional/events/21-workflow-event-handlers-globalcfg.t0000777000175000017500000000000015202510242034715 220-workflow-event-handlers.tustar alastairalastaircylc-flow-8.6.4/tests/functional/events/50-ref-test-fail/0000775000175000017500000000000015202510242023301 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/50-ref-test-fail/reference.log0000664000175000017500000000012415202510242025737 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/events/50-ref-test-fail/flow.cylc0000664000175000017500000000015215202510242025122 0ustar alastairalastair[scheduling] [[graph]] R1 = t1 => t2 => t3 [runtime] [[t1, t2, t3]] script = true cylc-flow-8.6.4/tests/functional/events/37-workflow-event-bad-custom-template.t0000775000175000017500000000260115202510242027671 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, flexible interface . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run1" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" MESSAGE="('workflow-event-handler-00', 'startup') bad template: echo %(rubbish)s" run_ok "${TEST_NAME_BASE}-run1-log" grep -q -F "ERROR - ${MESSAGE}" "${LOG}" purge exit cylc-flow-8.6.4/tests/functional/events/18-workflow-event-mail/0000775000175000017500000000000015202510242024554 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/18-workflow-event-mail/reference.log0000664000175000017500000000006715202510242027220 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/18-workflow-event-mail/flow.cylc0000664000175000017500000000100715202510242026375 0ustar alastairalastair#!jinja2 [meta] title=Workflow Event Mail [scheduler] {% if GLOBALCFG is not defined %} [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ [[events]] mail events = startup, shutdown {% endif %}{# not GLOBALCFG is not defined #} [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=true cylc-flow-8.6.4/tests/functional/events/11-cycle-task-event-job-logs-retrieve.t0000775000175000017500000000420015202510242027533 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote job logs retrieval, requires compatible version of cylc on remote # job host. export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" create_test_global_config '' " [platforms] [[_retrieve]] $(cylc config -i "[platforms][$CYLC_TEST_PLATFORM]") retrieve job logs = True install target = $CYLC_TEST_PLATFORM [[_no_retrieve]] $(cylc config -i "[platforms][$CYLC_TEST_PLATFORM]") retrieve job logs = False install target = $CYLC_TEST_PLATFORM " run_ok "${TEST_NAME_BASE}-validate" \ cylc validate -s "HOST='${CYLC_TEST_HOST}'" "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" # There are 2 remote tasks. One with "retrieve job logs = True", one without. # Only t1 should have job.err and job.out retrieved. sed "/'job-logs-retrieve'/!d" \ "${WORKFLOW_RUN_DIR}/log/job/20200202T0202Z/t"{1,2}'/'{01,02,03}'/job-activity.log' \ >'edited-activities.log' cmp_ok 'edited-activities.log' <<__LOG__ [(('job-logs-retrieve', 'retry'), 1) ret_code] 0 [(('job-logs-retrieve', 'retry'), 2) ret_code] 0 [(('job-logs-retrieve', 'succeeded'), 3) ret_code] 0 __LOG__ purge exit cylc-flow-8.6.4/tests/functional/events/03-stall-timeout.t0000775000175000017500000000340515202510242023632 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow stall timeout workflow . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" stall-timeout #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok "WARNING - stall timer timed out after PT6S" "${TEST_NAME}.stderr" grep_ok 'Workflow shutting down - "abort on stall timeout" is set' \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/events/34-task-abort/0000775000175000017500000000000015202510242022710 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/34-task-abort/reference.log0000664000175000017500000000016315202510242025351 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] 1/handled -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/events/34-task-abort/flow.cylc0000664000175000017500000000075615202510242024543 0ustar alastairalastair[meta] title = "Test job abort with retries and failed handler" [scheduler] [[events]] expected task failures = 1/foo [scheduling] [[graph]] R1 = "foo:fail => handled" [runtime] [[foo]] script = """ echo ONE cylc__job_abort "ERROR: rust never sleeps" echo TWO""" execution retry delays = PT0S [[[events]]] failed handlers = echo "!!!FAILED!!!" %(event)s %(id)s %(submit_num)s %(message)s [[handled]] script = true cylc-flow-8.6.4/tests/functional/events/stall-timeout-ref/0000775000175000017500000000000015202510242023772 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/stall-timeout-ref/reference.log0000664000175000017500000000007015202510242026430 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/events/stall-timeout-ref/flow.cylc0000664000175000017500000000053615202510242025621 0ustar alastairalastair[meta] description = This workflow is supposed to time out [scheduler] [[events]] abort on stall timeout = True stall timeout = PT1S expected task failures = 1/foo [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = false [[[simulation]]] fail cycle points = 1 cylc-flow-8.6.4/tests/functional/events/25-held-not-stalled.t0000775000175000017500000000240115202510242024166 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test abort on stall does not apply to a workflow started with --hold-after # See also tests/functional/pause-resume/02-paused-not-stalled.t . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --hold-after=0 --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/events/27-workflow-stalled-dump-prereq-fam/0000775000175000017500000000000015202510242027143 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/27-workflow-stalled-dump-prereq-fam/reference.log0000664000175000017500000000012015202510242031575 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/goo -triggered off [] cylc-flow-8.6.4/tests/functional/events/27-workflow-stalled-dump-prereq-fam/flow.cylc0000664000175000017500000000070515202510242030770 0ustar alastairalastair[scheduler] UTC mode = True # Ignore DST allow implicit tasks = True [[events]] abort on stall timeout = true stall timeout = PT0S expected task failures = 1/foo [scheduling] [[graph]] # will abort on stall with unhandled failed foo R1 = """foo & goo => FAM FAM:succeed-any => bar""" [runtime] [[foo]] script = false [[FAM]] [[f_1, f_2, f_3]] inherit = FAM cylc-flow-8.6.4/tests/functional/events/35-task-event-handler-importance.t0000775000175000017500000000330115202510242026662 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test new runtime config item importance #2289 . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------ set_test_number 3 #------------------------------------------------------------------------------ install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" T1_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/t1/NN/job-activity.log" grep_ok \ "\\[(('event-handler-00', 'failed'), 1) out\\] NAME = t1 POINT = 1 IMPORTANCE = 3 COLOR = red WORKFLOW-PRIORITY = HIGH" \ "${T1_ACTIVITY_LOG}" #------------------------------------------------------------------------------ purge cylc-flow-8.6.4/tests/functional/events/43-late-spawn.t0000775000175000017500000000243415202510242023107 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test late event, with spawned tasks that are also late . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" cylc play --debug --no-detach "${WORKFLOW_NAME}" grep -c 'WARNING.*late (late-time=.*)' \ <"${WORKFLOW_RUN_DIR}/log/scheduler/log" >'grep-log.out' cmp_ok 'grep-log.out' <<<'2' purge exit cylc-flow-8.6.4/tests/functional/events/29-task-event-mail-1.t0000775000175000017500000000345615202510242024204 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test event mail, single task event . "$(dirname "$0")/test_header" if ! command -v mail 2>'/dev/null'; then skip_all '"mail" command not available' fi set_test_number 5 mock_smtpd_init create_test_global_config " [scheduler] [[mail]] smtp = ${TEST_SMTPD_HOST} " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "$WORKFLOW_NAME" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "$WORKFLOW_NAME" run_ok "${TEST_NAME_BASE}-grep-log-1" \ grep -Pizo "job: 1/t1/01.*\n.*event: retry.*\n.*" "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log-2" grep \ "see: http://localhost/stuff/${USER}/${WORKFLOW_NAME}/" \ "${TEST_SMTPD_LOG}" run_ok "${TEST_NAME_BASE}-grep-log-2" \ grep -Pizo "Subject: \\[1/t1/01 retry\\].*(\n)?.*${WORKFLOW_NAME}" "${TEST_SMTPD_LOG}" purge mock_smtpd_kill exit cylc-flow-8.6.4/tests/functional/events/43-late-spawn/0000775000175000017500000000000015202510242022714 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/43-late-spawn/flow.cylc0000664000175000017500000000034715202510242024543 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 2015 final cycle point = 2016 [[graph]] P1Y = t1 [runtime] [[t1]] script = true [[[events]]] late offset = PT1M cylc-flow-8.6.4/tests/functional/events/20-workflow-event-handlers/0000775000175000017500000000000015202510242025423 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/20-workflow-event-handlers/reference.log0000664000175000017500000000006715202510242030067 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/20-workflow-event-handlers/flow.cylc0000664000175000017500000000103615202510242027246 0ustar alastairalastair#!jinja2 [meta] title = Workflow Event Mail URL = http://myworkflows.com/${CYLC_WORKFLOW_ID}.html workflow-priority = HIGH [scheduler] [[events]] {% if GLOBALCFG is not defined %} handlers = echo 'Your %(workflow)s workflow has a %(event)s event and URL %(workflow_url)s and workflow-priority as %(workflow-priority)s and workflow-UUID as %(uuid)s.' handler events = startup {% endif %}{# not GLOBALCFG is not defined #} [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true cylc-flow-8.6.4/tests/functional/events/32-task-event-job-logs-retrieve-2.t0000775000175000017500000000341415202510242026606 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote job logs retrieval OK with only "job.out" on a succeeded task. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config "" " [platforms] [[treadstone]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test -vv --no-detach "${WORKFLOW_NAME}" sed "/'job-logs-retrieve'/!d" \ "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job-activity.log" \ >'edited-activities.log' cmp_ok 'edited-activities.log' <<'__LOG__' [(('job-logs-retrieve', 'succeeded'), 1) ret_code] 0 __LOG__ exists_ok "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.out" exists_fail "${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.err" purge cylc-flow-8.6.4/tests/functional/events/45-task-event-handler-multi-warning.t0000775000175000017500000000510115202510242027317 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that simultaneous warnings from a task job are all handled by the # warning event handler. # https://github.com/cylc/cylc-flow/issues/2806 . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ cylc message -s WARNING -- ${CYLC_WORKFLOW_ID} ${CYLC_TASK_JOB} "cat" cylc message -s WARNING -- ${CYLC_WORKFLOW_ID} ${CYLC_TASK_JOB} "dog" cylc message -s WARNING -- ${CYLC_WORKFLOW_ID} ${CYLC_TASK_JOB} "fish" cylc message -s WARNING -- ${CYLC_WORKFLOW_ID} ${CYLC_TASK_JOB} "guinea pig" """ [[[events]]] handler events = warning handlers = echo "HANDLED %(message)s" __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" \ | sed -n -e 's/^.*\(\[(('"'"'event-handler-00'"'"'.*$\)/\1/p' >'log' contains_ok log <<__END__ [(('event-handler-00', 'warning-1'), 1) cmd] echo "HANDLED cat" [(('event-handler-00', 'warning-1'), 1) ret_code] 0 [(('event-handler-00', 'warning-1'), 1) out] HANDLED cat [(('event-handler-00', 'warning-2'), 1) cmd] echo "HANDLED dog" [(('event-handler-00', 'warning-2'), 1) ret_code] 0 [(('event-handler-00', 'warning-2'), 1) out] HANDLED dog [(('event-handler-00', 'warning-3'), 1) cmd] echo "HANDLED fish" [(('event-handler-00', 'warning-3'), 1) ret_code] 0 [(('event-handler-00', 'warning-3'), 1) out] HANDLED fish [(('event-handler-00', 'warning-4'), 1) cmd] echo "HANDLED 'guinea pig'" [(('event-handler-00', 'warning-4'), 1) ret_code] 0 [(('event-handler-00', 'warning-4'), 1) out] HANDLED 'guinea pig' __END__ purge exit cylc-flow-8.6.4/tests/functional/events/47-long-output.t0000775000175000017500000000651115202510242023335 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that a long output from an event handler is not going to hang or die. . "$(dirname "$0")/test_header" if ! python3 -c 'from select import poll' 2>'/dev/null'; then skip_all '"select.poll" not supported on this OS' fi set_test_number 10 create_test_global_config "" " [scheduler] process pool timeout = PT10S " # Long STDOUT output init_workflow "${TEST_NAME_BASE}" <<__FLOW_CONFIG__ [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true [[[events]]] succeeded handlers = cat "${CYLC_REPO_DIR}/COPYING" "${CYLC_REPO_DIR}/COPYING" "${CYLC_REPO_DIR}/COPYING" && echo __FLOW_CONFIG__ cd "$WORKFLOW_RUN_DIR" || exit 1 cat >'reference.log' <<'__REFLOG__' Initial point: 1 Final point: 1 1/t1 -triggered off [] __REFLOG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" >'catlog' sed -n 's/^.*\(GNU GENERAL PUBLIC LICENSE\)/\1/p' 'catlog' >'log-1' contains_ok 'log-1' <<'__LOG__' GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE __LOG__ run_ok "log-event-handler-00-out" \ grep -qF "[(('event-handler-00', 'succeeded'), 1) out]" 'catlog' run_ok "log-event-handler-ret-code" \ grep -qF "[(('event-handler-00', 'succeeded'), 1) ret_code] 0" 'catlog' purge # REPEAT: Long STDERR output init_workflow "${TEST_NAME_BASE}" <<__FLOW_CONFIG__ [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true [[[events]]] succeeded handlers = cat "${CYLC_REPO_DIR}/COPYING" "${CYLC_REPO_DIR}/COPYING" "${CYLC_REPO_DIR}/COPYING" >&2 && echo __FLOW_CONFIG__ cd "${WORKFLOW_RUN_DIR}" || exit 1 cat >'reference.log' <<'__REFLOG__' Initial point: 1 Final point: 1 1/t1 -triggered off [] __REFLOG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" >'catlog' sed -n 's/^.*\(GNU GENERAL PUBLIC LICENSE\)/\1/p' 'catlog' >'log-1' contains_ok 'log-1' <<'__LOG__' GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE __LOG__ run_ok "log-event-handler-00-err" \ grep -qF "[(('event-handler-00', 'succeeded'), 1) err]" 'catlog' run_ok "log-event-handler-00-ret-code" \ grep -qF "[(('event-handler-00', 'succeeded'), 1) ret_code] 0" 'catlog' purge exit cylc-flow-8.6.4/tests/functional/events/48-workflow-aborted/0000775000175000017500000000000015202510242024136 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/48-workflow-aborted/reference.log0000664000175000017500000000007315202510242026577 0ustar alastairalastairInitial point: 1 Final point: 1 1/modify -triggered off [] cylc-flow-8.6.4/tests/functional/events/48-workflow-aborted/flow.cylc0000664000175000017500000000135315202510242025763 0ustar alastairalastair[scheduler] [[main loop]] [[[health check]]] interval = PT1S [[events]] abort on inactivity timeout = true inactivity timeout = PT1M abort on stall timeout = true stall timeout = PT1M abort handlers = echo %(event)s %(message)s >"${CYLC_WORKFLOW_RUN_DIR}/handler.out" [scheduling] [[graph]] R1 = modify => t2 [runtime] [[modify]] script = """ # Pause the workflow, so it does not shutdown cylc pause "${CYLC_WORKFLOW_ID}" # Extra content in workflow contact file should cause health check to fail echo 'TIME=MONEY' >>"${CYLC_WORKFLOW_RUN_DIR}/.service/contact" """ [[t2]] script = true cylc-flow-8.6.4/tests/functional/events/test_header0000777000175000017500000000000015202510242026732 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/events/28-inactivity/0000775000175000017500000000000015202510242023027 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/28-inactivity/flow.cylc0000664000175000017500000000043015202510242024647 0ustar alastairalastair[scheduler] UTC mode = True [[events]] inactivity timeout = PT10S abort on inactivity timeout = True [scheduling] [[graph]] R1 = foo [runtime] [[foo]] init-script = cylc__job__disable_fail_signals ERR EXIT script = exit 1 cylc-flow-8.6.4/tests/functional/events/10-task-event-job-logs-retrieve/0000775000175000017500000000000015202510242026251 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/10-task-event-job-logs-retrieve/reference.log0000664000175000017500000000014515202510242030712 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t1 -triggered off [] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/10-task-event-job-logs-retrieve/flow.cylc0000664000175000017500000000041615202510242030075 0ustar alastairalastair#!jinja2 [meta] title=Task Event Job Log Retrieve [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=test "${CYLC_TASK_TRY_NUMBER}" -eq 3 platform = {{ PLATFORM }} [[[job]]] execution retry delays=PT0S, 2*PT1S cylc-flow-8.6.4/tests/functional/events/37-workflow-event-bad-custom-template/0000775000175000017500000000000015202510242027502 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/37-workflow-event-bad-custom-template/reference.log0000664000175000017500000000006715202510242032146 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/37-workflow-event-bad-custom-template/flow.cylc0000664000175000017500000000026315202510242031326 0ustar alastairalastair[scheduler] [[events]] handlers = echo %(rubbish)s handler events = startup [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=true cylc-flow-8.6.4/tests/functional/events/02-multi.t0000775000175000017500000000425515202510242022164 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test list of multiple event handlers. . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] [[events]] inactivity timeout = PT30S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true [[[events]]] started handlers = echo %(workflow)s, echo %(name)s, echo %(start_time)s __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" JOB_STFILE="${WORKFLOW_RUN_DIR}/log/job/1/t1/01/job.status" JOB_START_TIME="$(sed -n 's/^CYLC_JOB_INIT_TIME=//p' "${JOB_STFILE}")" cylc cat-log "${WORKFLOW_NAME}" \ | sed -n -e 's/^.*\(\[(('"'"'event-handler-0.'"'"'.*$\)/\1/p' | sort >'log' cmp_ok log <<__END__ [(('event-handler-00', 'started'), 1) cmd] echo ${WORKFLOW_NAME} [(('event-handler-00', 'started'), 1) out] ${WORKFLOW_NAME} [(('event-handler-00', 'started'), 1) ret_code] 0 [(('event-handler-01', 'started'), 1) cmd] echo t1 [(('event-handler-01', 'started'), 1) out] t1 [(('event-handler-01', 'started'), 1) ret_code] 0 [(('event-handler-02', 'started'), 1) cmd] echo ${JOB_START_TIME} [(('event-handler-02', 'started'), 1) out] ${JOB_START_TIME} [(('event-handler-02', 'started'), 1) ret_code] 0 __END__ purge exit cylc-flow-8.6.4/tests/functional/events/09-task-event-mail/0000775000175000017500000000000015202510242023644 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/09-task-event-mail/reference.log0000664000175000017500000000011615202510242026303 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/09-task-event-mail/flow.cylc0000664000175000017500000000104115202510242025463 0ustar alastairalastair#!jinja2 [meta] title=Task Event Mail [scheduler] {% if GLOBALCFG is not defined %} [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ {% endif %}{# not GLOBALCFG is not defined #} [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=test "${CYLC_TASK_TRY_NUMBER}" -eq 2 [[[job]]] execution retry delays = PT5S {% if GLOBALCFG is not defined %} [[[events]]] mail events = failed, retry, succeeded {% endif %}{# not GLOBALCFG is not defined #} cylc-flow-8.6.4/tests/functional/events/10-task-event-job-logs-retrieve.t0000775000175000017500000000451615202510242026447 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote job logs retrieval, requires compatible version of cylc on remote # job host. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 4 OPT_SET= create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True retrieve job logs retry delays = PT5S " OPT_SET='-s GLOBALCFG=True' install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate ${OPT_SET} \ -s "PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach ${OPT_SET} \ -s "PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" sed "/'job-logs-retrieve'/!d" \ "${WORKFLOW_RUN_DIR}/log/job/1/t1/"{01,02,03}"/job-activity.log" \ >'edited-activities.log' cmp_ok 'edited-activities.log' <<'__LOG__' [(('job-logs-retrieve', 'retry'), 1) ret_code] 0 [(('job-logs-retrieve', 'retry'), 2) ret_code] 0 [(('job-logs-retrieve', 'succeeded'), 3) ret_code] 0 __LOG__ grep -F 'will run after' "${WORKFLOW_RUN_DIR}/log/scheduler/log" \ | cut -d' ' -f 4-12 | sort >"edited-log" cmp_ok 'edited-log' <<'__LOG__' 1/t1/01 handler:job-logs-retrieve for task event:retry will run after PT5S 1/t1/02 handler:job-logs-retrieve for task event:retry will run after PT5S 1/t1/03 handler:job-logs-retrieve for task event:succeeded will run after PT5S __LOG__ purge exit cylc-flow-8.6.4/tests/functional/events/32-task-event-job-logs-retrieve-2/0000775000175000017500000000000015202510242026414 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/32-task-event-job-logs-retrieve-2/reference.log0000664000175000017500000000006715202510242031060 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/32-task-event-job-logs-retrieve-2/flow.cylc0000664000175000017500000000030415202510242030234 0ustar alastairalastair[meta] title = Task Event Job Log Retrieve 1 [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = rm -f "${CYLC_TASK_LOG_ROOT}.err" platform = treadstone cylc-flow-8.6.4/tests/functional/events/01-timeout.t0000775000175000017500000000334615202510242022517 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow timeout . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" timeout #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok "WARNING - workflow timer timed out after PT6S" "${TEST_NAME}.stderr" grep_ok 'Workflow shutting down - "abort on workflow timeout" is set' \ "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/events/17-task-event-job-logs-retrieve-command0000777000175000017500000000000015202510242035371 210-task-event-job-logs-retrieveustar alastairalastaircylc-flow-8.6.4/tests/functional/events/18-workflow-event-mail.t0000775000175000017500000000366115202510242024752 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test event mail. . "$(dirname "$0")/test_header" if ! command -v mail 2>'/dev/null'; then skip_all '"mail" command not available' fi set_test_number 3 mock_smtpd_init OPT_SET= if [[ "${TEST_NAME_BASE}" == *-globalcfg ]]; then create_test_global_config "" " [scheduler] [[events]] mail events = startup, shutdown [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ smtp = ${TEST_SMTPD_HOST}" OPT_SET='-s GLOBALCFG=True' else create_test_global_config " [scheduler] [[mail]] smtp = ${TEST_SMTPD_HOST} " fi install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate ${OPT_SET} "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach ${OPT_SET} "${WORKFLOW_NAME}" contains_ok "${TEST_SMTPD_LOG}" <<__LOG__ event: startup message: workflow starting event: shutdown message: AUTOMATIC see: http://localhost/stuff/${USER}/${WORKFLOW_NAME}/ __LOG__ purge mock_smtpd_kill exit cylc-flow-8.6.4/tests/functional/events/30-task-event-mail-2/0000775000175000017500000000000015202510242023775 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/30-task-event-mail-2/flow.cylc0000664000175000017500000000075415202510242025626 0ustar alastairalastair#!jinja2 [meta] title = Task Event Mail [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ task event batch interval = PT8S [scheduling] [[graph]] R1 = t1? & t2? & t3? & t4? & t5? [runtime] [[t1, t2, t3, t4, t5]] script = false execution retry delays = PT15S [[[events]]] mail events = failed, retry cylc-flow-8.6.4/tests/functional/events/stall-timeout/0000775000175000017500000000000015202510242023220 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/stall-timeout/flow.cylc0000664000175000017500000000037315202510242025046 0ustar alastairalastair[meta] description = This workflow is supposed to time out [scheduler] [[events]] stall timeout = PT6S abort on stall timeout = True [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = false cylc-flow-8.6.4/tests/functional/events/11-cycle-task-event-job-logs-retrieve/0000775000175000017500000000000015202510242027347 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/11-cycle-task-event-job-logs-retrieve/reference.log0000664000175000017500000000042215202510242032006 0ustar alastairalastairInitial point: 20200202T0202Z Final point: 20200202T0202Z 20200202T0202Z/t1 -triggered off [] 20200202T0202Z/t1 -triggered off [] 20200202T0202Z/t1 -triggered off [] 20200202T0202Z/t2 -triggered off [] 20200202T0202Z/t2 -triggered off [] 20200202T0202Z/t2 -triggered off [] cylc-flow-8.6.4/tests/functional/events/11-cycle-task-event-job-logs-retrieve/flow.cylc0000664000175000017500000000074515202510242031200 0ustar alastairalastair[meta] title = Task Event Job Log Retrieve [scheduler] UTC mode = True cycle point format = %Y%m%dT%H%MZ [scheduling] initial cycle point = 20200202T0202Z final cycle point = 20200202T0202Z [[graph]] R1 = T [runtime] [[T]] script = test "${CYLC_TASK_TRY_NUMBER}" -eq 3 execution retry delays = PT0S, 2*PT1S [[t1]] inherit = T platform = _retrieve [[t2]] inherit = T platform = _no_retrieve cylc-flow-8.6.4/tests/functional/events/04-stall-timeout-ref-live.t0000775000175000017500000000345715202510242025351 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow reference test live stall timeout workflow . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" 'stall-timeout-ref' #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" RUN_MODE="$(basename "$0" | sed "s/.*-ref-\(.*\).t/\1/g")" workflow_run_fail "${TEST_NAME}" \ cylc play --reference-test --mode="${RUN_MODE}" --debug --no-detach \ "${WORKFLOW_NAME}" grep_ok "WARNING - stall timer timed out after PT1S" "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/events/25-held-not-stalled/0000775000175000017500000000000015202510242024001 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/25-held-not-stalled/flow.cylc0000664000175000017500000000051215202510242025622 0ustar alastairalastair[scheduler] [[events]] abort on inactivity timeout = False abort on stall timeout = True stall timeout = PT0S inactivity timeout handlers = cylc release --all '%(workflow)s' inactivity timeout = PT5S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true cylc-flow-8.6.4/tests/functional/events/24-workflow-timeout.t0000775000175000017500000000277715202510242024403 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow timeout event and handler . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate \ -s "ABORT=False" "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach -s "ABORT=False" "${WORKFLOW_NAME}" grep_workflow_log_ok grep-1 "That was quick!" purge install_workflow "${TEST_NAME_BASE}-2" "${TEST_NAME_BASE}" workflow_run_fail "${TEST_NAME_BASE}-run-2" \ cylc play --debug --no-detach -s "ABORT=True" "${WORKFLOW_NAME}" grep_workflow_log_ok grep-2 'Workflow shutting down - "abort on workflow timeout" is set' purge cylc-flow-8.6.4/tests/functional/events/42-late-then-restart.t0000775000175000017500000000314515202510242024376 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test late event handler with restart. Event should be emitted once. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-run" cylc play --debug --no-detach "${WORKFLOW_NAME}" run_ok "${TEST_NAME_BASE}-restart" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" # Check that the workflow has emitted a single late event. grep -c 'WARNING.*late (late-time=.*)' \ <(cat "${WORKFLOW_RUN_DIR}/log/scheduler/"*.log) \ >'grep-log.out' cmp_ok 'grep-log.out' <<<'1' grep -c 'late (late-time=.*)' \ "${WORKFLOW_RUN_DIR}/log/scheduler/my-handler.out" \ > 'grep-my-handler.out' cmp_ok 'grep-my-handler.out' <<<'1' purge exit cylc-flow-8.6.4/tests/functional/events/17-task-event-job-logs-retrieve-command.t0000775000175000017500000000463615202510242030075 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote job logs retrieval custom command, requires compatible version of # cylc on remote job host. export REQUIRE_PLATFORM='loc:remote' . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] retrieve job logs = True retrieve job logs command = my-rsync " OPT_SET='-s GLOBALCFG=True' install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" mkdir -p "${RUN_DIR}/${WORKFLOW_NAME}/bin" cat >"${RUN_DIR}/${WORKFLOW_NAME}/bin/my-rsync" <<'__BASH__' #!/usr/bin/env bash set -eu echo "$@" >>"${CYLC_WORKFLOW_LOG_DIR}/my-rsync.log" exec rsync -a "$@" __BASH__ chmod +x "${RUN_DIR}/${WORKFLOW_NAME}/bin/my-rsync" # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-validate" \ cylc validate ${OPT_SET} -s "PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" # shellcheck disable=SC2086 workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach ${OPT_SET} \ -s "PLATFORM='${CYLC_TEST_PLATFORM}'" "${WORKFLOW_NAME}" WORKFLOW_LOG_D="${RUN_DIR}/${WORKFLOW_NAME}/log" sed 's/^.* -v //' "${WORKFLOW_LOG_D}/scheduler/my-rsync.log" >'my-rsync.log.edited' OPT_HEAD='--include=/1 --include=/1/t1' OPT_TAIL='--exclude=/**' ARGS="${CYLC_TEST_HOST}:cylc-run/${WORKFLOW_NAME}/log/job/ ${WORKFLOW_LOG_D}/job/" cmp_ok 'my-rsync.log.edited' <<__LOG__ ${OPT_HEAD} --include=/1/t1/01 --include=/1/t1/01/** ${OPT_TAIL} ${ARGS} ${OPT_HEAD} --include=/1/t1/02 --include=/1/t1/02/** ${OPT_TAIL} ${ARGS} ${OPT_HEAD} --include=/1/t1/03 --include=/1/t1/03/** ${OPT_TAIL} ${ARGS} __LOG__ purge exit cylc-flow-8.6.4/tests/functional/events/00-workflow.t0000775000175000017500000000253115202510242022675 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handlers . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_workflow_log_ok grep-startup "HELLO STARTUP" grep_workflow_log_ok grep-timeout "HELLO TIMEOUT" grep_workflow_log_ok grep-inactivity "HELLO INACTIVITY" grep_workflow_log_ok grep-shutdown "HELLO SHUTDOWN" cylc-flow-8.6.4/tests/functional/events/23-workflow-stalled-handler.t0000775000175000017500000000173115202510242025744 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow event handler, flexible interface with stall handler . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/events/29-task-event-mail-1/0000775000175000017500000000000015202510242024004 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/events/29-task-event-mail-1/reference.log0000664000175000017500000000011615202510242026443 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/events/29-task-event-mail-1/flow.cylc0000664000175000017500000000056615202510242025636 0ustar alastairalastair#!jinja2 [meta] title=Task Event Mail [scheduler] [[mail]] footer = see: http://localhost/stuff/%(owner)s/%(workflow)s/ [scheduling] [[graph]] R1=t1 [runtime] [[t1]] script=test "${CYLC_TASK_TRY_NUMBER}" -eq 2 [[[job]]] execution retry delays = PT1S [[[events]]] mail events = failed, retry cylc-flow-8.6.4/tests/functional/events/46-task-output-as-event.t0000775000175000017500000000472215202510242025061 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that known task outputs can be used as events. . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script=""" cylc message -- ${CYLC_WORKFLOW_ID} ${CYLC_TASK_JOB} \ 'rose' 'lily' 'iris' 'WARNING:poison ivy' """ [[[outputs]]] rose = rose lily = lily iris = iris [[[events]]] handler events = rose, lily, iris, warning, arsenic # (arsenic is an invalid event) handlers = echo %(message)s __FLOW_CONFIG__ TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "$TEST_NAME" cylc validate "${WORKFLOW_NAME}" dump_std "${TEST_NAME}" grep_ok 'WARNING - Invalid event name.*arsenic' "${TEST_NAME}.stderr" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" \ | sed -n -e 's/^.*\(\[(('"'"'event-handler-00'"'"'.*$\)/\1/p' | sort >'log' cmp_ok log <<__END__ [(('event-handler-00', 'iris'), 1) cmd] echo iris [(('event-handler-00', 'iris'), 1) out] iris [(('event-handler-00', 'iris'), 1) ret_code] 0 [(('event-handler-00', 'lily'), 1) cmd] echo lily [(('event-handler-00', 'lily'), 1) out] lily [(('event-handler-00', 'lily'), 1) ret_code] 0 [(('event-handler-00', 'rose'), 1) cmd] echo rose [(('event-handler-00', 'rose'), 1) out] rose [(('event-handler-00', 'rose'), 1) ret_code] 0 [(('event-handler-00', 'warning-1'), 1) cmd] echo 'poison ivy' [(('event-handler-00', 'warning-1'), 1) out] poison ivy [(('event-handler-00', 'warning-1'), 1) ret_code] 0 __END__ purge exit cylc-flow-8.6.4/tests/functional/directives/0000775000175000017500000000000015202510242021252 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/loadleveler/0000775000175000017500000000000015202510242023550 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/loadleveler/reference.log0000664000175000017500000000017415202510242026213 0ustar alastairalastairInitial point: 1 Final point: 1 1/rem1 -triggered off [ 1/rem2 -triggered off ['1/rem1' 1/killer -triggered 0off ['1/rem2'] cylc-flow-8.6.4/tests/functional/directives/loadleveler/flow.cylc0000664000175000017500000000126015202510242025372 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = rem2.1 [scheduling] [[graph]] R1 = """ rem1 => rem2 rem2:start => killer => !rem2 """ [runtime] [[LLSETTINGS]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[directives]]] class = serial job_type = serial notification = error wall_clock_limit = '120,60' [[rem1]] inherit = LLSETTINGS script = "sleep 10; true" [[rem2]] inherit = LLSETTINGS script = "sleep 30" [[killer]] script = cylc kill "$CYLC_WORKFLOW_ID//*/rem2"; sleep 10 cylc-flow-8.6.4/tests/functional/directives/03-pbs.t0000777000175000017500000000000015202510242025336 200-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/slurm/0000775000175000017500000000000015202510242022414 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/slurm/reference.log0000664000175000017500000000017515202510242025060 0ustar alastairalastairInitial point: 1 Final point: 1 1/rem1 -triggered off [] 1/rem2 -triggered off ['1/rem1'] 1/killer -triggered off ['1/rem2'] cylc-flow-8.6.4/tests/functional/directives/slurm/flow.cylc0000664000175000017500000000107615202510242024243 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/rem2 [scheduling] [[graph]] R1 = """ rem1 => rem2 rem2:start => killer => !rem2 """ [runtime] [[SLURM_SETTINGS]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[directives]]] --time = 02:00 [[rem1]] inherit = SLURM_SETTINGS script = "sleep 10; true" [[rem2]] inherit = SLURM_SETTINGS script = "sleep 30" [[killer]] script = cylc kill "$CYLC_WORKFLOW_ID//*/rem2"; sleep 10 cylc-flow-8.6.4/tests/functional/directives/pbs/0000775000175000017500000000000015202510242022036 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/pbs/reference.log0000664000175000017500000000017515202510242024502 0ustar alastairalastairInitial point: 1 Final point: 1 1/rem1 -triggered off [] 1/rem2 -triggered off ['1/rem1'] 1/killer -triggered off ['1/rem2'] cylc-flow-8.6.4/tests/functional/directives/pbs/flow.cylc0000664000175000017500000000113315202510242023657 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = rem2.1 [scheduling] [[graph]] R1 = """ rem1 => rem2 rem2:start => killer => !rem2 """ [runtime] [[PBS_SETTINGS]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[directives]]] -l walltime=00:60:00 -l cput=00:02:00 [[rem1]] inherit = PBS_SETTINGS script = "sleep 10; true" [[rem2]] inherit = PBS_SETTINGS script = "sleep 30" [[killer]] script = cylc kill "$CYLC_WORKFLOW_ID//*/rem2"; sleep 10 cylc-flow-8.6.4/tests/functional/directives/02-slurm.t0000777000175000017500000000000015202510242025713 200-loadleveler.tustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/01-at.t0000664000175000017500000000172615202510242022267 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test at submission export REQUIRE_PLATFORM='runner:at comms:tcp' . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/directives/test_header0000777000175000017500000000000015202510242027567 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/01-at/0000775000175000017500000000000015202510242022074 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/directives/01-at/reference.log0000664000175000017500000000017515202510242024540 0ustar alastairalastairInitial point: 1 Final point: 1 1/rem1 -triggered off [] 1/rem2 -triggered off ['1/rem1'] 1/killer -triggered off ['1/rem2'] cylc-flow-8.6.4/tests/functional/directives/01-at/flow.cylc0000664000175000017500000000077615202510242023731 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] expected task failures = 1/rem2 [scheduling] [[graph]] R1 = """ rem1 => rem2 rem2:start => killer => !rem2 """ [runtime] [[ATSETTINGS]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[rem1]] inherit = ATSETTINGS script = "sleep 10; true" [[rem2]] inherit = ATSETTINGS script = "sleep 30" [[killer]] script = cylc kill "$CYLC_WORKFLOW_ID//*/rem2"; sleep 10 cylc-flow-8.6.4/tests/functional/directives/00-loadleveler.t0000664000175000017500000000234715202510242024160 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job runner directives # (the job runner is given by filename, e.g. 02-slurm.t) JOB_RUNNER="${0##*\/??-}" JOB_RUNNER_NAME="${JOB_RUNNER%%.t}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER_NAME} comms:tcp" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 reftest "${TEST_NAME_BASE}" "${JOB_RUNNER_NAME}" purge exit cylc-flow-8.6.4/tests/functional/graphing/0000775000175000017500000000000015202510242020710 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/06-family-or.t0000775000175000017500000000420515202510242023223 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test: family-OR logic pre-initial simplification bug (#1626). . "$(dirname "$0")/test_header" set_test_number 3 cat >'flow.cylc' <<'__FLOW_CONFIG__' [scheduler] UTC mode = True [scheduling] initial cycle point = 2000 [[graph]] T00 = """ A? B? A[-PT24H]:fail-any? | B[-PT24H]:fail-any? => c """ [runtime] [[A]] [[B]] [[a1]] inherit = A [[b1a, b2a, b3a]] inherit = B [[c]] __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${PWD}" graph_workflow "${PWD}" "graph.plain" \ -g A -g B -g X 20000101T0000Z 20000103T0000Z cmp_ok 'graph.plain' - <<'__GRAPH__' edge "20000101T0000Z/A" "20000102T0000Z/c" edge "20000101T0000Z/B" "20000102T0000Z/c" edge "20000102T0000Z/A" "20000103T0000Z/c" edge "20000102T0000Z/B" "20000103T0000Z/c" graph node "20000101T0000Z/A" "A\n20000101T0000Z" node "20000101T0000Z/B" "B\n20000101T0000Z" node "20000101T0000Z/c" "c\n20000101T0000Z" node "20000102T0000Z/A" "A\n20000102T0000Z" node "20000102T0000Z/B" "B\n20000102T0000Z" node "20000102T0000Z/c" "c\n20000102T0000Z" node "20000103T0000Z/A" "A\n20000103T0000Z" node "20000103T0000Z/B" "B\n20000103T0000Z" node "20000103T0000Z/c" "c\n20000103T0000Z" stop __GRAPH__ grep_ok "Ignoring undefined family X" "graph.plain.err" exit cylc-flow-8.6.4/tests/functional/graphing/11-nested-fam/0000775000175000017500000000000015202510242023152 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/11-nested-fam/graph.plain.ungrouped.ref0000664000175000017500000000011415202510242030056 0ustar alastairalastairedge "1/foo" "1/bar" graph node "1/bar" "bar\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/11-nested-fam/graph.plain.ref0000664000175000017500000000006615202510242026055 0ustar alastairalastairedge "1/TOP" "1/TOP" graph node "1/TOP" "TOP\n1" stop cylc-flow-8.6.4/tests/functional/graphing/11-nested-fam/flow.cylc0000664000175000017500000000022215202510242024771 0ustar alastairalastair[scheduling] [[graph]] R1 = foo => bar [runtime] [[TOP]] [[MID]] inherit = TOP [[foo, bar]] inherit = MID cylc-flow-8.6.4/tests/functional/graphing/08-ungrouped/0000775000175000017500000000000015202510242023145 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/08-ungrouped/graph.plain.ungrouped.ref0000664000175000017500000000025315202510242030055 0ustar alastairalastairedge "1/foo" "1/bar1" edge "1/foo" "1/bar2" edge "1/foo" "1/bar3" graph node "1/bar1" "bar1\n1" node "1/bar2" "bar2\n1" node "1/bar3" "bar3\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/08-ungrouped/graph.plain.ref0000664000175000017500000000011415202510242026042 0ustar alastairalastairedge "1/foo" "1/BAR" graph node "1/BAR" "BAR\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/08-ungrouped/flow.cylc0000664000175000017500000000020415202510242024764 0ustar alastairalastair[scheduling] [[graph]] R1 = foo => BAR [runtime] [[foo]] [[BAR]] [[bar1, bar2, bar3]] inherit = BAR cylc-flow-8.6.4/tests/functional/graphing/09-ref-graph.t0000664000175000017500000000325115202510242023177 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that "cylc graph --reference -O foo.ref WORKFLOW" works. GitHub #2249. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" 'new.ref' "--group=" cmp_ok 'new.ref' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/07-stop-at-final-point/0000775000175000017500000000000015202510242024741 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/07-stop-at-final-point/graph.plain.ref0000664000175000017500000000152215202510242027642 0ustar alastairalastairedge "2015-01-01/foo" "2015-01-01/bar" edge "2015-01-01/foo" "2015-01-01/baz" edge "2015-01-01/foo" "2015-01-02/foo" edge "2015-01-01/start" "2015-01-01/foo" edge "2015-01-02/foo" "2015-01-02/bar" edge "2015-01-02/foo" "2015-01-02/baz" edge "2015-01-02/foo" "2015-01-03/foo" edge "2015-01-03/baz" "2015-01-03/stop" edge "2015-01-03/foo" "2015-01-03/bar" edge "2015-01-03/foo" "2015-01-03/baz" graph node "2015-01-01/bar" "bar\n2015-01-01" node "2015-01-01/baz" "baz\n2015-01-01" node "2015-01-01/foo" "foo\n2015-01-01" node "2015-01-01/start" "start\n2015-01-01" node "2015-01-02/bar" "bar\n2015-01-02" node "2015-01-02/baz" "baz\n2015-01-02" node "2015-01-02/foo" "foo\n2015-01-02" node "2015-01-03/bar" "bar\n2015-01-03" node "2015-01-03/baz" "baz\n2015-01-03" node "2015-01-03/foo" "foo\n2015-01-03" node "2015-01-03/stop" "stop\n2015-01-03" stop cylc-flow-8.6.4/tests/functional/graphing/07-stop-at-final-point/flow.cylc0000664000175000017500000000043115202510242026562 0ustar alastairalastair#!Jinja2 [scheduler] cycle point format = %Y-%m-%d allow implicit tasks = True [scheduling] initial cycle point = 2015-01-01 final cycle point = 2015-01-03 [[graph]] R1 = start => foo P1D = foo[-P1D] => foo => bar & baz R1/P0D = baz => stop cylc-flow-8.6.4/tests/functional/graphing/05-suicide-family.t0000664000175000017500000000363015202510242024225 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that a suicide-triggered family plots as a collapsed family node (#1526). . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- graph_workflow "${WORKFLOW_NAME}" 'graph.plain' "--group=" cmp_ok graph.plain "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- graph_workflow "${WORKFLOW_NAME}" 'graph.plain.suicide' --show-suicide "--group=" cmp_ok 'graph.plain.suicide' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.suicide.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/07-stop-at-final-point.t0000664000175000017500000000403415202510242025127 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that graphing stops at workflow final cycle point. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph-npoints" graph_workflow "${WORKFLOW_NAME}" 'graph.plain.test1' --set="STOP_CRITERION='number of cycle points = 6'" cmp_ok 'graph.plain.test1' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph-final-point" graph_workflow "${WORKFLOW_NAME}" 'graph.plain.test2' --set="STOP_CRITERION='final cycle point = 2015-01-05'" cmp_ok 'graph.plain.test2' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/02-icp-task-missing/0000775000175000017500000000000015202510242024311 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/02-icp-task-missing/graph.plain.ref0000664000175000017500000000052715202510242027216 0ustar alastairalastairedge "20150304T2100Z/NZC1" "20150305T0000Z/pc" edge "20150305T2100Z/NZC1" "20150306T0000Z/pc" graph node "20150304T0000Z/pc" "pc\n20150304T0000Z" node "20150304T2100Z/NZC1" "NZC1\n20150304T2100Z" node "20150305T0000Z/pc" "pc\n20150305T0000Z" node "20150305T2100Z/NZC1" "NZC1\n20150305T2100Z" node "20150306T0000Z/pc" "pc\n20150306T0000Z" stop cylc-flow-8.6.4/tests/functional/graphing/02-icp-task-missing/flow.cylc0000664000175000017500000000047315202510242026140 0ustar alastairalastair#!jinja2 [scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20150304T00 final cycle point = +P2D [[graph]] {% if INCLUDE_R1 is defined and INCLUDE_R1 -%} R1 = pc {% endif %} #R1 = pc T00 = NZC1[-PT3H] => pc T21 = NZC1 cylc-flow-8.6.4/tests/functional/graphing/08-ungrouped.t0000664000175000017500000000353015202510242023333 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that ungrouped graphing works. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- graph_workflow "${WORKFLOW_NAME}" 'graph.plain' "--group=" cmp_ok graph.plain "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- graph_workflow "${WORKFLOW_NAME}" 'graph.plain.ungrouped' cmp_ok graph.plain.ungrouped \ "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ungrouped.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/02-icp-task-missing.t0000664000175000017500000000404115202510242024475 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test for GH bug #955, missing initial task depending on pre-initial. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-r1" graph_workflow "${WORKFLOW_NAME}" 'r1.graph.plain.test' \ --set='INCLUDE_R1="true"' \ 20150304T00Z +P2D cmp_ok 'r1.graph.plain.test' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-no-r1" graph_workflow "${WORKFLOW_NAME}" 'no-r1.graph.plain.test' \ --set='INCLUDE_R1="false"' \ 20150304T00Z +P2D cmp_ok 'no-r1.graph.plain.test' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/00-boundaries/0000775000175000017500000000000015202510242023260 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/00-boundaries/20140808T06.graph.plain.ref0000664000175000017500000000024715202510242027363 0ustar alastairalastairgraph node "20140808T0600+12/foo" "foo\n20140808T0600+12" node "20140808T1200+12/foo" "foo\n20140808T1200+12" node "20140808T1800+12/foo" "foo\n20140808T1800+12" stop cylc-flow-8.6.4/tests/functional/graphing/00-boundaries/20140808T00.graph.plain.ref0000664000175000017500000000041615202510242027353 0ustar alastairalastairedge "20140808T0000+12/foo" "20140808T0000+12/bar" graph node "20140808T0000+12/bar" "bar\n20140808T0000+12" node "20140808T0000+12/foo" "foo\n20140808T0000+12" node "20140808T0600+12/foo" "foo\n20140808T0600+12" node "20140808T1200+12/foo" "foo\n20140808T1200+12" stop cylc-flow-8.6.4/tests/functional/graphing/00-boundaries/flow.cylc0000664000175000017500000000027415202510242025106 0ustar alastairalastair [scheduler] cycle point time zone = +12 allow implicit tasks = True [scheduling] initial cycle point = 20140808T00 [[graph]] R//PT6H = foo T00 = foo => bar cylc-flow-8.6.4/tests/functional/graphing/10-ghost-nodes/0000775000175000017500000000000015202510242023360 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/10-ghost-nodes/graph.plain.ref0000664000175000017500000000027015202510242026260 0ustar alastairalastairedge "1/foo" "2/foo" edge "3/foo" "4/foo" edge "4/foo" "5/foo" graph node "1/foo" "foo\n1" node "2/foo" "foo\n2" node "3/foo" "foo\n3" node "4/foo" "foo\n4" node "5/foo" "foo\n5" stop cylc-flow-8.6.4/tests/functional/graphing/10-ghost-nodes/flow.cylc0000664000175000017500000000020715202510242025202 0ustar alastairalastair[scheduling] initial cycle point = 1 cycling mode = integer [[graph]] P1!3 = foo[-P1] => foo [runtime] [[foo]] cylc-flow-8.6.4/tests/functional/graphing/00-boundaries.t0000664000175000017500000000343615202510242023453 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test graphing for a workflow with two different cycling intervals. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # Two tests: for POINT in 20140808T00 20140808T06; do TEST_NAME="${TEST_NAME_BASE}-graph-${POINT}" graph_workflow "${WORKFLOW_NAME}" "${POINT}.graph.plain" "${POINT}" cmp_ok "${POINT}.graph.plain" \ "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/${POINT}.graph.plain.ref" done #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/09-ref-graph/0000775000175000017500000000000015202510242023011 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/09-ref-graph/graph.ref0000664000175000017500000000041615202510242024611 0ustar alastairalastairedge "20140808T0000+12/foo" "20140808T0000+12/bar" graph node "20140808T0000+12/bar" "bar\n20140808T0000+12" node "20140808T0000+12/foo" "foo\n20140808T0000+12" node "20140808T0600+12/foo" "foo\n20140808T0600+12" node "20140808T1200+12/foo" "foo\n20140808T1200+12" stop cylc-flow-8.6.4/tests/functional/graphing/09-ref-graph/flow.cylc0000664000175000017500000000027415202510242024637 0ustar alastairalastair [scheduler] cycle point time zone = +12 allow implicit tasks = True [scheduling] initial cycle point = 20140808T00 [[graph]] R//PT6H = foo T00 = foo => bar cylc-flow-8.6.4/tests/functional/graphing/test_header0000777000175000017500000000000015202510242027225 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/01-namespace/0000775000175000017500000000000015202510242023062 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/01-namespace/graph.plain.ref0000664000175000017500000000030015202510242025754 0ustar alastairalastairedge "FAM" "f1" edge "FAM" "f2" edge "FAM" "f3" edge "TOP" "FAM" edge "root" "TOP" graph node "FAM" "FAM" node "TOP" "TOP" node "f1" "f1" node "f2" "f2" node "f3" "f3" node "root" "root" stop cylc-flow-8.6.4/tests/functional/graphing/01-namespace/flow.cylc0000664000175000017500000000020115202510242024676 0ustar alastairalastair[scheduling] [[graph]] R1 = FAM [runtime] [[TOP]] [[FAM]] inherit = TOP [[f1,f2,f3]] inherit = FAM cylc-flow-8.6.4/tests/functional/graphing/09-close-fam.t0000664000175000017500000000256215202510242023176 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that ungrouped graphing works. . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" SRCD="${TEST_SOURCE_DIR}/${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" 'graph.plain' "--group=" cmp_ok 'graph.plain' "${SRCD}/graph.plain.ref" graph_workflow "${WORKFLOW_NAME}" 'graph.plain.ungrouped' cmp_ok 'graph.plain.ungrouped' "${SRCD}/graph.plain.ungrouped.ref" purge exit cylc-flow-8.6.4/tests/functional/graphing/10-ghost-nodes.t0000664000175000017500000000317015202510242023546 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that cylc-graph displays ghost nodes where appropriate. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- graph_workflow "${WORKFLOW_NAME}" 'graph.plain' 1 5 cmp_ok 'graph.plain' "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/11-nested-fam.t0000664000175000017500000000252115202510242023337 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that ungrouped graphing works. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" SRCD="${TEST_SOURCE_DIR}/${TEST_NAME_BASE}" graph_workflow "${WORKFLOW_NAME}" 'graph.plain' "--group=" cmp_ok 'graph.plain' "${SRCD}/graph.plain.ref" graph_workflow "${WORKFLOW_NAME}" 'graph.plain.ungrouped' cmp_ok 'graph.plain.ungrouped' "${SRCD}/graph.plain.ungrouped.ref" purge exit cylc-flow-8.6.4/tests/functional/graphing/01-namespace.t0000664000175000017500000000343415202510242023253 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test namespace graphing. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" 'graph.plain' -n cmp_ok graph.plain "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" TEST_NAME="${TEST_NAME_BASE}-err" graph_workflow "${WORKFLOW_NAME}" 'graph.plain2' -n -g X grep_ok "Cannot combine \-\-group and \-\-namespaces." "graph.plain2.err" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/graphing/09-close-fam/0000775000175000017500000000000015202510242023004 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/09-close-fam/graph.plain.ungrouped.ref0000664000175000017500000000034715202510242027720 0ustar alastairalastairedge "1/foo" "1/wam" edge "1/foo" "1/x" edge "1/foo" "1/y" edge "1/wam" "1/bar" edge "1/x" "1/bar" edge "1/y" "1/bar" graph node "1/bar" "bar\n1" node "1/foo" "foo\n1" node "1/wam" "wam\n1" node "1/x" "x\n1" node "1/y" "y\n1" stop cylc-flow-8.6.4/tests/functional/graphing/09-close-fam/graph.plain.ref0000664000175000017500000000016715202510242025711 0ustar alastairalastairedge "1/FAM" "1/bar" edge "1/foo" "1/FAM" graph node "1/FAM" "FAM\n1" node "1/bar" "bar\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/09-close-fam/flow.cylc0000664000175000017500000000032415202510242024626 0ustar alastairalastair[scheduling] [[graph]] R1 = foo => FAM:succeed-all => bar [runtime] [[FAM]] [[BAM]] inherit = FAM [[wam]] inherit = FAM [[x, y]] inherit = BAM [[foo, bar]] cylc-flow-8.6.4/tests/functional/graphing/05-suicide-family/0000775000175000017500000000000015202510242024036 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/graphing/05-suicide-family/graph.plain.suicide.ref0000664000175000017500000000011415202510242030357 0ustar alastairalastairedge "1/foo" "1/BAR" graph node "1/BAR" "BAR\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/05-suicide-family/graph.plain.ref0000664000175000017500000000004115202510242026732 0ustar alastairalastairgraph node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/functional/graphing/05-suicide-family/flow.cylc0000664000175000017500000000020515202510242025656 0ustar alastairalastair[scheduling] [[graph]] R1 = foo => !BAR [runtime] [[foo]] [[BAR]] [[bar1, bar2, bar3]] inherit = BAR cylc-flow-8.6.4/tests/functional/pause-resume/0000775000175000017500000000000015202510242021524 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/01-beyond-stop/0000775000175000017500000000000015202510242024205 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/01-beyond-stop/reference.log0000664000175000017500000000022115202510242026641 0ustar alastairalastair20140101T00/foo -triggered off [] 20140101T00/pause_resume -triggered off ['20140101T00/foo'] 20140101T00/bar -triggered off ['20140101T00/foo'] cylc-flow-8.6.4/tests/functional/pause-resume/01-beyond-stop/flow.cylc0000664000175000017500000000146315202510242026034 0ustar alastairalastair[meta] title = "pause/resume a workflow with tasks held beyond workflow stop point" description = """ Resuming a paused workflow should not release tasks beyond the workflow stop point. """ # https://github.com/cylc/cylc-flow/pull/1144 # See also tests/f/hold-release/01-beyond-stop [scheduler] cycle point format = %Y%m%dT%H [scheduling] initial cycle point = 20140101T00 stop after cycle point = 20140101T00 [[graph]] R1 = foo => pause_resume T00 = foo => bar [runtime] [[pause_resume]] # When this task runs foo will be held beyond the workflow stop point. script = """ cylc pause $CYLC_WORKFLOW_ID cylc play $CYLC_WORKFLOW_ID """ [[foo]] script = true [[bar]] script = true cylc-flow-8.6.4/tests/functional/pause-resume/21-client/0000775000175000017500000000000015202510242023222 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/21-client/reference.log0000664000175000017500000000006715202510242025666 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/pause-resume/21-client/flow.cylc0000664000175000017500000000041115202510242025041 0ustar alastairalastair#!jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = P1M [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true cylc-flow-8.6.4/tests/functional/pause-resume/12-pause-then-retry/0000775000175000017500000000000015202510242025160 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/12-pause-then-retry/reference.log0000664000175000017500000000034615202510242027624 0ustar alastairalastairFinal point: 1 1/t-submit-retry-able -triggered off [] 1/t-retry-able -triggered off [] 1/t-pause -triggered off ['1/t-retry-able', '1/t-submit-retry-able'] 1/t-retry-able -triggered off [] 1/t-submit-retry-able -triggered off [] cylc-flow-8.6.4/tests/functional/pause-resume/12-pause-then-retry/flow.cylc0000664000175000017500000000370615202510242027011 0ustar alastairalastair[meta] title = Test: run task - pause workflow - task job retry - resume workflow [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] [[graph]] R1 = """ t-retry-able:start => t-pause t-submit-retry-able:submit => t-pause """ [runtime] [[t-pause]] script = """ cylc pause "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log 'Command "pause" actioned' # Poll t-submit-retry-able, should return submit-fail cylc poll "${CYLC_WORKFLOW_ID}//*/t-submit-retry-able" # Allow t-retry-able to continue rm -f "${CYLC_WORKFLOW_RUN_DIR}/file" cylc__job__poll_grep_workflow_log -E \ '1/t-retry-able/01:running.* => waiting' cylc__job__poll_grep_workflow_log -E \ '1/t-submit-retry-able/01:submitted.* => waiting' # Resume the workflow cylc play "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log -E \ '1/t-retry-able:waiting.* => waiting\(queued\)' cylc__job__poll_grep_workflow_log -E \ '1/t-submit-retry-able:waiting.* => waiting\(queued\)' """ [[t-retry-able]] script = """ if ((CYLC_TASK_SUBMIT_NUMBER == 1)); then touch "${CYLC_WORKFLOW_RUN_DIR}/file" while [[ -e "${CYLC_WORKFLOW_RUN_DIR}/file" ]]; do sleep 1 done false fi """ execution retry delays = PT5S [[t-submit-retry-able]] init-script = """ if [[ "${CYLC_TASK_JOB}" == *"/01" ]]; then cylc__job__disable_fail_signals ERR EXIT exit 1 fi """ script = true submission retry delays = PT5S cylc-flow-8.6.4/tests/functional/pause-resume/00-workflow/0000775000175000017500000000000015202510242023613 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/00-workflow/reference.log0000664000175000017500000000024315202510242026253 0ustar alastairalastair20140101T00/pause_resume -triggered off [] 20140101T00/foo -triggered off ['20140101T00/pause_resume'] 20140101T00/bar -triggered off ['20140101T00/pause_resume'] cylc-flow-8.6.4/tests/functional/pause-resume/00-workflow/flow.cylc0000664000175000017500000000117615202510242025443 0ustar alastairalastair[meta] title = "pause/resume test workflow" # https://github.com/cylc/cylc-flow/pull/843 # See also tests/f/hold-release/00-workflow [scheduler] cycle point format = %Y%m%dT%H [scheduling] initial cycle point = 20140101T00 final cycle point = 20140101T00 [[graph]] R1 = "pause_resume => foo & bar" T00, T06 = "bar" [runtime] [[pause_resume]] script = """ wait cylc pause "${CYLC_WORKFLOW_ID}" cylc__job__poll_grep_workflow_log 'Command "pause" actioned' cylc play "${CYLC_WORKFLOW_ID}" """ [[foo,bar]] script = true cylc-flow-8.6.4/tests/functional/pause-resume/02-paused-not-stalled.t0000664000175000017500000000304015202510242025632 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that abort on stall does not apply to a paused workflow # See also tests/functional/events/25-held-not-stalled.t . "$(dirname "$0")/test_header" set_test_number 2 init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduler] [[events]] abort on inactivity timeout = False abort on stall timeout = True stall timeout = PT0S inactivity timeout handlers = cylc play '%(workflow)s' inactivity timeout = PT5S [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = true __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --pause --no-detach "${WORKFLOW_NAME}" purge cylc-flow-8.6.4/tests/functional/pause-resume/01-beyond-stop.t0000664000175000017500000000172215202510242024374 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test pause and resume, with tasks present beyond stop point. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/pause-resume/test_header0000777000175000017500000000000015202510242032416 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/pause-resume/12-pause-then-retry.t0000775000175000017500000000172615202510242025356 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow pause => retry and submit-retry => workflow resume . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/pause-resume/21-client.t0000775000175000017500000000307015202510242023412 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test resume paused workflow using the "cylc client" command. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" cylc play --reference-test --pause --debug --no-detach "${WORKFLOW_NAME}" \ 1>"${TEST_NAME_BASE}.out" 2>&1 & CYLC_RUN_PID=$! poll_workflow_running read -r -d '' resume <<_args_ {"request_string": " mutation { resume(workflows: [\"${WORKFLOW_NAME}\"]){ result } } ", "variables": null} _args_ # shellcheck disable=SC2086 run_ok "${TEST_NAME_BASE}-client" \ cylc client "${WORKFLOW_NAME}" 'graphql' < <(echo ${resume}) run_ok "${TEST_NAME_BASE}-run" wait "${CYLC_RUN_PID}" purge exit cylc-flow-8.6.4/tests/functional/pause-resume/00-workflow.t0000664000175000017500000000173315202510242024004 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test workflow pause and resume, with cycling and async tasks present. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cli/0000775000175000017500000000000015202510242017660 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cli/00-cycle-points.t0000664000175000017500000000437215202510242022701 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that initial and final cycle points can be overridden by the CLI. # Ref. Github Issue #1406. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- delete_db TEST_NAME=${TEST_NAME_BASE}-run-override # This will fail if the in-workflow final cycle point does not get overridden. workflow_run_ok "${TEST_NAME}" cylc play --fcp=2015-04 --icp=2015-04 --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- delete_db TEST_NAME=${TEST_NAME_BASE}-run-fail # This should fail as the final cycle point is < the initial one. workflow_run_fail "${TEST_NAME}" cylc play --fcp=2015-03 --icp=2015-04 --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cli/03-verbosity.t0000775000175000017500000000407415202510242022323 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test "cylc verbosity" . "$(dirname "$0")/test_header" set_test_number 4 # Test illegal log level TEST_NAME="${TEST_NAME_BASE}-bad" run_fail "$TEST_NAME" cylc verbosity duck quack grep_ok 'InputError: Illegal logging level, duck' "${TEST_NAME}.stderr" # Test good log level TEST_NAME="${TEST_NAME_BASE}-good" init_workflow "${TEST_NAME_BASE}" << '__FLOW__' [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = setter => getter [runtime] [[setter]] script = """ echo "CYLC_VERBOSE: $CYLC_VERBOSE" [[ "$CYLC_VERBOSE" != 'true' ]] echo "CYLC_DEBUG: $CYLC_DEBUG" [[ "$CYLC_DEBUG" != 'true' ]] cylc verbosity DEBUG "$CYLC_WORKFLOW_ID" cylc__job__poll_grep_workflow_log 'Command "set_verbosity" actioned' """ [[getter]] script = """ echo "CYLC_VERBOSE: $CYLC_VERBOSE" [[ "$CYLC_VERBOSE" == 'true' ]] echo "CYLC_DEBUG: $CYLC_DEBUG" [[ "$CYLC_DEBUG" == 'true' ]] """ __FLOW__ run_ok "${TEST_NAME}-validate" cylc validate "$WORKFLOW_NAME" workflow_run_ok "${TEST_NAME}-run" cylc play --no-detach "$WORKFLOW_NAME" purge cylc-flow-8.6.4/tests/functional/cli/05-colour.t0000664000175000017500000000406615202510242021600 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that CLI colour output is disabled if output is not to a terminal. # Uses the "script" command to make stdout log file look like a terminal. . "$(dirname "$0")/test_header" if [[ "$OSTYPE" != "linux-gnu"* ]]; then skip_all "Tests not compatibile with $OSTYPE" fi set_test_number 7 ANSI='\e\[' # No redirection. script -q -c "cylc scan -t rich" log > /dev/null 2>&1 grep_ok "$ANSI" log -P # color # FIXME: this test doesn't work because the output includes a color reset char # at the end for some reason: https://github.com/cylc/cylc-flow/issues/6467 # script -q -c "cylc scan -t rich --color=never" log > /dev/null 2>&1 # grep_fail "$ANSI" log -P # no color # Redirected. cylc scan -t rich > log grep_fail "$ANSI" log -P # no color cylc scan -t rich --color=always > log grep_ok "$ANSI" log -P # color # Check command help too (gets printed during command line parsing). # No redirection. script -q -c "cylc scan --help" log > /dev/null 2>&1 grep_ok "$ANSI" log -P # color script -q -c "cylc scan --help --color never" log > /dev/null 2>&1 grep_fail "$ANSI" log -P # no color # Redirected. cylc scan --help > log grep_fail "$ANSI" log -P # no color cylc scan --help --color=always > log grep_ok "$ANSI" log -P # color cylc-flow-8.6.4/tests/functional/cli/test_header0000777000175000017500000000000015202510242026175 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cli/00-cycle-points/0000775000175000017500000000000015202510242022506 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cli/00-cycle-points/flow.cylc0000664000175000017500000000041615202510242024332 0ustar alastairalastair[scheduler] UTC mode = true [[events]] abort on stall timeout = true stall timeout = PT20S [scheduling] initial cycle point = 2015-01 final cycle point = 2015-01 [[graph]] P1D = foo [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/cli/01-help.t0000775000175000017500000000744015202510242021223 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # cylc help and basic invocation. . "$(dirname "$0")/test_header" # Number of tests depends on the number of 'cylc' commands. set_test_number 37 # Top help run_ok "${TEST_NAME_BASE}-0" cylc run_ok "${TEST_NAME_BASE}-help" cylc help run_ok "${TEST_NAME_BASE}-help-id" cylc help id run_ok "${TEST_NAME_BASE}-help-all" cylc help all grep_ok 'trigger ...' "${TEST_NAME_BASE}-help-all.stdout" run_ok "${TEST_NAME_BASE}---help" cylc --help for FILE in \ "${TEST_NAME_BASE}-help.stdout" \ "${TEST_NAME_BASE}---help.stdout" do cmp_ok "${FILE}" "${TEST_NAME_BASE}-0.stdout" done # Sub-command - no match run_fail "${TEST_NAME_BASE}-aardvark" cylc aardvark cmp_ok "${TEST_NAME_BASE}-aardvark.stderr" <<'__STDERR__' cylc aardvark: unknown utility. Abort. Type "cylc help all" for a list of utilities. __STDERR__ # Sub-command - many matches run_fail "${TEST_NAME_BASE}-get" cylc get cmp_ok "${TEST_NAME_BASE}-get.stderr" <<'__STDERR__' cylc get: is ambiguous for: cylc get-resources cylc get-workflow-contact cylc get-workflow-version __STDERR__ # Sub-command help run_ok "${TEST_NAME_BASE}-validate--help" cylc validate --help run_ok "${TEST_NAME_BASE}-validate-h" cylc validate -h run_ok "${TEST_NAME_BASE}-help-validate" cylc help validate run_ok "${TEST_NAME_BASE}-help-va" cylc help va for FILE in \ "${TEST_NAME_BASE}-validate--help.stdout" \ "${TEST_NAME_BASE}-validate-h.stdout" \ "${TEST_NAME_BASE}-help-validate.stdout" \ "${TEST_NAME_BASE}-help-va.stdout" do cmp_ok "${FILE}" "${TEST_NAME_BASE}-validate--help.stdout" done # Alias help run_ok "${TEST_NAME_BASE}-broadcast-h" cylc broadcast -h run_ok "${TEST_NAME_BASE}-bcast-h" cylc bcast -h cmp_ok "${TEST_NAME_BASE}-broadcast-h.stdout" "${TEST_NAME_BASE}-bcast-h.stdout" # Dead end run_fail "${TEST_NAME_BASE}-broadcast-h" cylc check-software # Help with IDs and the Licence run_ok "${TEST_NAME_BASE}-help-id" cylc help id grep_ok 'Every Installed Cylc workflow has an ID' "${TEST_NAME_BASE}-help-id.stdout" run_ok "${TEST_NAME_BASE}-help-licence" cylc help licence grep_ok 'GNU GENERAL PUBLIC LICENSE' "${TEST_NAME_BASE}-help-licence.stdout" # Version run_ok "${TEST_NAME_BASE}-version" cylc version run_ok "${TEST_NAME_BASE}---version" cylc --version run_ok "${TEST_NAME_BASE}-V" cylc -V run_ok "${TEST_NAME_BASE}-version.stdout" \ test -n "${TEST_NAME_BASE}-version.stdout" cmp_ok "${TEST_NAME_BASE}-version.stdout" "${TEST_NAME_BASE}---version.stdout" cmp_ok "${TEST_NAME_BASE}-version.stdout" "${TEST_NAME_BASE}-V.stdout" # Supplementary help run_ok "${TEST_NAME_BASE}-all" cylc help all run_ok "${TEST_NAME_BASE}-id" cylc help id # Check "cylc version --long" output is correct. cylc version --long | head -n 1 > long1 echo "$(cylc version) ($(command -v cylc))" > long2 cmp_ok long1 long2 # --help with no DISPLAY while read -r ITEM; do run_ok "${TEST_NAME_BASE}-no-display-${ITEM}--help" \ env DISPLAY= cylc "${ITEM#cylc-}" --help done < <(cd "${CYLC_REPO_DIR}/bin" && ls 'cylc-'*) exit cylc-flow-8.6.4/tests/functional/post-install/0000775000175000017500000000000015202510242021542 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/post-install/test_header0000777000175000017500000000000015202510242030057 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/post-install/00-log-vc-info.t0000664000175000017500000000320515202510242024264 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test logging of source dir version control information occurs post install . "$(dirname "$0")/test_header" if ! command -v 'git' > /dev/null; then skip_all 'git not installed' fi set_test_number 4 make_rnd_workflow cd "${RND_WORKFLOW_SOURCE}" || exit 1 cat > 'flow.cylc' << __FLOW__ [scheduling] [[graph]] R1 = foo __FLOW__ git init git add 'flow.cylc' git commit -am 'Initial commit' run_ok "${TEST_NAME_BASE}-install" cylc install VCS_INFO_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/vcs.json" exists_ok "$VCS_INFO_FILE" # Basic check, unit tests cover this in more detail: grep_ok '"version control system": "git"' "$VCS_INFO_FILE" -F DIFF_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/uncommitted.diff" exists_ok "$DIFF_FILE" # Expected to be empty but should exist purge_rnd_workflow cylc-flow-8.6.4/tests/functional/offset/0000775000175000017500000000000015202510242020377 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/03-final-next-chain.t0000664000175000017500000000173515202510242024137 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test specifying final cycle as an offset to the next occurrence of T06 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/05-long-final-chain.t0000664000175000017500000000165315202510242024121 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test a 3 item offset . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/04-cycle-offset-chain/0000775000175000017500000000000015202510242024263 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/04-cycle-offset-chain/reference.log0000664000175000017500000000036515202510242026730 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100101T1200+01 20100101T0000+01/foo -triggered off ['20091231T1800+01/foo'] 20100101T0600+01/foo -triggered off ['20100101T0000+01/foo'] 20100101T1200+01/foo -triggered off ['20100101T0600+01/foo'] cylc-flow-8.6.4/tests/functional/offset/04-cycle-offset-chain/flow.cylc0000664000175000017500000000036415202510242026111 0ustar alastairalastair[scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 final cycle point = T06+PT06H [[graph]] T00, T06, T12, T18 = "foo[-P1D+PT18H] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/offset/05-long-final-chain/0000775000175000017500000000000015202510242023727 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/05-long-final-chain/reference.log0000664000175000017500000000075115202510242026373 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100102T1200+01 20100101T0000+01/foo -triggered off ['20091231T1800+01/foo'] 20100101T0600+01/foo -triggered off ['20100101T0000+01/foo'] 20100101T1200+01/foo -triggered off ['20100101T0600+01/foo'] 20100101T1800+01/foo -triggered off ['20100101T1200+01/foo'] 20100102T0000+01/foo -triggered off ['20100101T1800+01/foo'] 20100102T0600+01/foo -triggered off ['20100102T0000+01/foo'] 20100102T1200+01/foo -triggered off ['20100102T0600+01/foo'] cylc-flow-8.6.4/tests/functional/offset/05-long-final-chain/flow.cylc0000664000175000017500000000036215202510242025553 0ustar alastairalastair[scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 final cycle point = T06+P1D+PT6H [[graph]] T00, T06, T12, T18 = "foo[-PT6H] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/offset/00-final-simple/0000775000175000017500000000000015202510242023174 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/00-final-simple/reference.log0000664000175000017500000000065415202510242025642 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100106T0000+01 20100101T0000+01/foo -triggered off ['20091231T0000+01/foo'] 20100102T0000+01/foo -triggered off ['20100101T0000+01/foo'] 20100103T0000+01/foo -triggered off ['20100102T0000+01/foo'] 20100104T0000+01/foo -triggered off ['20100103T0000+01/foo'] 20100105T0000+01/foo -triggered off ['20100104T0000+01/foo'] 20100106T0000+01/foo -triggered off ['20100105T0000+01/foo'] cylc-flow-8.6.4/tests/functional/offset/00-final-simple/flow.cylc0000664000175000017500000000033115202510242025014 0ustar alastairalastair[scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 final cycle point = +P5D [[graph]] T00 = "foo[-P1D] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/offset/02-final-chain.t0000664000175000017500000000170515202510242023157 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test specifying final cycle as an offset chain . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/02-final-chain/0000775000175000017500000000000015202510242022767 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/02-final-chain/reference.log0000664000175000017500000000065415202510242025435 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100102T0600+01 20100101T0000+01/foo -triggered off ['20091231T1800+01/foo'] 20100101T0600+01/foo -triggered off ['20100101T0000+01/foo'] 20100101T1200+01/foo -triggered off ['20100101T0600+01/foo'] 20100101T1800+01/foo -triggered off ['20100101T1200+01/foo'] 20100102T0000+01/foo -triggered off ['20100101T1800+01/foo'] 20100102T0600+01/foo -triggered off ['20100102T0000+01/foo'] cylc-flow-8.6.4/tests/functional/offset/02-final-chain/flow.cylc0000664000175000017500000000035715202510242024617 0ustar alastairalastair[scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 final cycle point = +P1D+PT6H [[graph]] T00, T06, T12, T18 = "foo[-PT6H] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/offset/00-final-simple.t0000664000175000017500000000167715202510242023374 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test specifying final cycle as an offset . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/test_header0000777000175000017500000000000015202510242026714 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/01-final-next.t0000664000175000017500000000171615202510242023054 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test specifying final cycle as the next instance of T06 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/03-final-next-chain/0000775000175000017500000000000015202510242023744 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/03-final-next-chain/reference.log0000664000175000017500000000036515202510242026411 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100101T1200+01 20100101T0000+01/foo -triggered off ['20091231T1800+01/foo'] 20100101T0600+01/foo -triggered off ['20100101T0000+01/foo'] 20100101T1200+01/foo -triggered off ['20100101T0600+01/foo'] cylc-flow-8.6.4/tests/functional/offset/03-final-next-chain/flow.cylc0000664000175000017500000000035615202510242025573 0ustar alastairalastair[scheduler] cycle point time zone = +01 [scheduling] initial cycle point = 20100101T00 final cycle point = T06+PT6H [[graph]] T00, T06, T12, T18 = "foo[-PT6H] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/offset/04-cycle-offset-chain.t0000664000175000017500000000170615202510242024454 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test specifying an offset as a chain of periods . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/offset/01-final-next/0000775000175000017500000000000015202510242022662 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/offset/01-final-next/reference.log0000664000175000017500000000027015202510242025322 0ustar alastairalastairInitial point: 20100101T0000+01 Final point: 20100101T0600+01 20100101T0000+01/foo -triggered off ['20091231T1800+01/foo'] 20100101T0600+01/foo -triggered off ['20100101T0000+01/foo'] cylc-flow-8.6.4/tests/functional/offset/01-final-next/flow.cylc0000664000175000017500000000063215202510242024506 0ustar alastairalastair[scheduler] cycle point time zone = +01 [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] initial cycle point = 20100101T00 final cycle point = T06 [[graph]] T00 = "foo[-PT6H] => foo" T06 = "foo[-PT6H] => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/functional/cylc-get-site-config/0000775000175000017500000000000015202510242023025 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-site-config/01-defaults.t0000664000175000017500000000226515202510242025244 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test site-config defaults actually validate (GitHub #1865). . "$(dirname "$0")/test_header" set_test_number 1 # Empty it (of non-default global-tests.cylc items, which would then be retrieved # by "cylc config" below). echo '' > "$CYLC_CONF_PATH/global.cylc" # Replace it entirely with system defaults. cylc config -d > "$CYLC_CONF_PATH/global.cylc" # Check that the new file parses OK. run_ok "${TEST_NAME_BASE}" cylc config cylc-flow-8.6.4/tests/functional/cylc-get-site-config/04-homeless.t0000664000175000017500000000310415202510242025250 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Check undefined $HOME does not break: # a) use of $HOME in global config (GitHub #2895) # b) global config Jinja2 support (GitHub #5155) . "$(dirname "$0")/test_header" set_test_number 5 # shellcheck disable=SC2016 create_test_global_config '' ' [install] [[symlink dirs]] [[[localhost]]] run = $HOME/dr-malcolm ' run_ok "${TEST_NAME_BASE}" \ env -u HOME \ cylc config --item='[install][symlink dirs][localhost]run' cmp_ok "${TEST_NAME_BASE}.stdout" <<<"\$HOME/dr-malcolm" # The test global config is created with #!Jinja2 at the top, in case of any # Jinja2 code in global-tests.cylc. Parsec Jinja2 support uses $HOME to find # custom filters etc. GitHub #5155. for DIR in Filters Tests Globals; do grep_ok "\$HOME undefined: can't load ~/.cylc/Jinja2$DIR" "${TEST_NAME_BASE}.stderr" done cylc-flow-8.6.4/tests/functional/cylc-get-site-config/02-jinja2.t0000664000175000017500000000221015202510242024601 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test site-config defaults actually validate (GitHub #1865). . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config '#!jinja2' ' {% set UTC_MODE = True %} [scheduler] UTC mode = {{UTC_MODE}}' run_ok "${TEST_NAME_BASE}" cylc config -d --item='[scheduler]UTC mode' cmp_ok "${TEST_NAME_BASE}.stdout" <<<'True' cmp_ok "${TEST_NAME_BASE}.stderr" <'/dev/null' exit cylc-flow-8.6.4/tests/functional/cylc-get-site-config/test_header0000777000175000017500000000000015202510242031342 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-get-site-config/05-host-bool-override.t0000664000175000017500000000272615202510242027166 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test site-config host item boolean override (GitHub #2282). . "$(dirname "$0")/test_header" set_test_number 2 cat etc/global.cylc <<'__hi__' [platforms] [[desktop\d\d|laptop\d\d]] [[sugar]] login hosts = localhost job runner = slurm [[hpc]] login hosts = hpcl1, hpcl2 retrieve job logs = True job runner = pbs [[hpcl1-bg]] login hosts = hpcl1 retrieve job logs = True job runner = background [[hpcl2-bg]] login hosts = hpcl2 retrieve job logs = True job runner = background __hi__ export CYLC_CONF_PATH="${PWD}/etc" run_ok "${TEST_NAME_BASE}" cylc config cmp_ok "${TEST_NAME_BASE}.stderr" . # Test site-config host item boolean override (GitHub #2282). . "$(dirname "$0")/test_header" set_test_number 3 create_test_global_config '' ' [platforms] [[localhost]] use login shell = True [[mytesthost]] use login shell = False' run_ok "${TEST_NAME_BASE}" \ cylc config --item='[platforms][mytesthost]use login shell' cmp_ok "${TEST_NAME_BASE}.stdout" <<<'False' cmp_ok "${TEST_NAME_BASE}.stderr" <'/dev/null' exit cylc-flow-8.6.4/tests/functional/cylc-get-site-config/00-basic.t0000664000175000017500000000346515202510242024520 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test getting the global config . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-config" run_ok "${TEST_NAME}.validate" cylc config #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-get-items" run_fail "${TEST_NAME}.non-existent" \ cylc config --item='[this][doesnt]exist' #------------------------------------------------------------------------------- VAL1="$(cylc config -d --item '[platforms][localhost]use login shell')" VAL2="$(cylc config -d | sed -n '/\[\[localhost\]\]/,$p' | \ sed -n "0,/use login shell/s/^[ \t]*\(use login shell =.*\)/\1/p")" run_ok "${TEST_NAME_BASE}-check-output" \ test "use login shell = ${VAL1}" = "${VAL2}" #------------------------------------------------------------------------------- exit cylc-flow-8.6.4/tests/functional/broadcast/0000775000175000017500000000000015202510242021053 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/02-inherit/0000775000175000017500000000000015202510242022734 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/02-inherit/reference.log0000664000175000017500000000034315202510242025375 0ustar alastairalastairInitial point: 20140101T00 Final point: 20140101T00 20140101T00/broadcast_all -triggered off [] 20140101T00/broadcast_tag -triggered off ['20140101T00/broadcast_all'] 20140101T00/t1 -triggered off ['20140101T00/broadcast_tag'] cylc-flow-8.6.4/tests/functional/broadcast/02-inherit/flow.cylc0000664000175000017500000000167515202510242024570 0ustar alastairalastair#!jinja2 [meta] title=broadcast description=Test Broadcast Inheritance [scheduler] cycle point format = %Y%m%dT%H [scheduling] initial cycle point = 20140101T00 final cycle point = 20140101T00 [[graph]] R1 = broadcast_all => broadcast_tag T00 = broadcast_tag => t1 [runtime] [[broadcast_all]] script=""" cylc broadcast -s "[environment]ALL_0=true" -n F1 $CYLC_WORKFLOW_ID cylc broadcast -s "[environment]ALL_1=true" -n t1 $CYLC_WORKFLOW_ID """ [[broadcast_tag]] script=""" cylc broadcast -s "[environment]TAG_0=true" -n F1 -p $CYLC_TASK_CYCLE_POINT \ $CYLC_WORKFLOW_ID cylc broadcast -s "[environment]TAG_1=true" -n t1 -p $CYLC_TASK_CYCLE_POINT \ $CYLC_WORKFLOW_ID """ [[F1]] script=""" $ALL_0 $ALL_1 $TAG_0 $TAG_1 """ [[[environment]]] ALL_0=false ALL_1=false TAG_0=false TAG_1=false [[t1]] inherit=F1 cylc-flow-8.6.4/tests/functional/broadcast/08-space.t0000775000175000017500000000200515202510242022560 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast -s '[foo] bar=baz' syntax. cylc/cylc-flow#1680 . "$(dirname "$0")/test_header" set_test_number 2 export REFTEST_OPTS="--abort-if-any-task-fails" reftest exit cylc-flow-8.6.4/tests/functional/broadcast/04-empty/0000775000175000017500000000000015202510242022432 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/04-empty/reference.log0000664000175000017500000000014215202510242025070 0ustar alastairalastairInitial point: 1 Final point: 1 1/broadcast -triggered off [] 1/t1 -triggered off ['1/broadcast'] cylc-flow-8.6.4/tests/functional/broadcast/04-empty/flow.cylc0000664000175000017500000000123615202510242024257 0ustar alastairalastair[meta] title = broadcast empty description = Test broadcast of an empty string [scheduling] [[graph]] R1 = "broadcast => t1" [runtime] [[broadcast]] script = """ cylc broadcast -s '[environment]EMPTY=' -p '1' -n 't1' "${CYLC_WORKFLOW_ID}" \ | tee 'broadcast.out' diff -u - 'broadcast.out' <<__OUT__ Broadcast set: + [${CYLC_TASK_CYCLE_POINT}/t1] [environment]EMPTY= __OUT__ """ [[t1]] script = """ printenv EMPTY | tee 'echo.out' diff -u - 'echo.out' <<<'' """ [[[environment]]] EMPTY=full cylc-flow-8.6.4/tests/functional/broadcast/05-bad-point/0000775000175000017500000000000015202510242023152 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/05-bad-point/flow.cylc0000664000175000017500000000113315202510242024773 0ustar alastairalastair[meta] title=broadcast bad point description=Test broadcast to an invalid cycle point fails. # And see github #1415 - it did cause the scheduler to abort. [scheduler] [[events]] abort on stall timeout = True stall timeout=PT1M [scheduling] initial cycle point = 20150808 final cycle point = 20150808 [[graph]] P1M = broadcast [runtime] [[broadcast]] script=""" # Broadcast to an integer point, not valid for this workflow; and # fail if the broadcast succeeds (it should fail). ! cylc broadcast -s 'title=foo' -p '1' "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/broadcast/00-simple.t0000775000175000017500000000501615202510242022753 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcasts . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --no-detach --reference-test "${WORKFLOW_NAME}" sort "${WORKFLOW_RUN_DIR}/share/broadcast.log" >'broadcast.log.sorted' cmp_ok 'broadcast.ref' 'broadcast.log.sorted' DB_FILE="${WORKFLOW_RUN_DIR}/log/db" NAME='select-broadcast-events.out' sqlite3 "${DB_FILE}" \ 'SELECT change, point, namespace, key, value FROM broadcast_events ORDER BY time, change, point, namespace, key' >"${NAME}" cmp_ok "${NAME}" <<'__SELECT__' +|*|root|[environment]BCAST|ROOT +|20100808T00|foo|[environment]BCAST|FOO +|*|bar|[environment]BCAST|BAR +|20100809T00|baz|[environment]BCAST|BAZ +|20100809T00|qux|[environment]BCAST|QUX -|20100809T00|qux|[environment]BCAST|QUX +|*|wibble|[environment]BCAST|WIBBLE -|*|wibble|[environment]BCAST|WIBBLE +|*|ENS|[environment]BCAST|ENS +|*|ENS1|[environment]BCAST|ENS1 +|20100809T00|m2|[environment]BCAST|M2 +|*|m7|[environment]BCAST|M7 +|*|m8|[environment]BCAST|M8 +|*|m9|[environment]BCAST|M9 __SELECT__ NAME='select-broadcast-states.out' sqlite3 "${DB_FILE}" \ 'SELECT point, namespace, key, value FROM broadcast_states ORDER BY point, namespace, key' >"${NAME}" cmp_ok "${NAME}" <<'__SELECT__' *|ENS|[environment]BCAST|ENS *|ENS1|[environment]BCAST|ENS1 *|bar|[environment]BCAST|BAR *|m7|[environment]BCAST|M7 *|m8|[environment]BCAST|M8 *|m9|[environment]BCAST|M9 *|root|[environment]BCAST|ROOT 20100808T00|foo|[environment]BCAST|FOO 20100809T00|baz|[environment]BCAST|BAZ 20100809T00|m2|[environment]BCAST|M2 __SELECT__ purge exit cylc-flow-8.6.4/tests/functional/broadcast/07-timeout/0000775000175000017500000000000015202510242022765 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/07-timeout/reference.log0000664000175000017500000000026215202510242025426 0ustar alastairalastairInitial point: 20100808T0000Z Final point: 20100809T0000Z 20100808T0000Z/send_broadcast -triggered off [] 20100808T0000Z/timeout -triggered off ['20100808T0000Z/send_broadcast'] cylc-flow-8.6.4/tests/functional/broadcast/07-timeout/flow.cylc0000664000175000017500000000122515202510242024610 0ustar alastairalastair[meta] title = "test workflow for broadcast timeout functionality" [scheduler] UTC mode = True [scheduling] initial cycle point = 20100808T0000Z final cycle point = 20100808T0000Z [[graph]] R1 = send_broadcast => timeout [runtime] [[send_broadcast]] script = """ cylc broadcast -n timeout --point=20100808T0000Z --set='[events]execution timeout=PT1S' $CYLC_WORKFLOW_ID """ [[timeout]] script = """ cylc__job__poll_grep_workflow_log -E \ "${CYLC_TASK_ID}.* execution timeout after PT1S" """ [[[events]]] execution timeout = PT1M cylc-flow-8.6.4/tests/functional/broadcast/09-remote.t0000775000175000017500000000175715202510242022776 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast from remote task job. export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 2 reftest purge exit cylc-flow-8.6.4/tests/functional/broadcast/09-remote/0000775000175000017500000000000015202510242022574 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/09-remote/reference.log0000664000175000017500000000022515202510242025234 0ustar alastairalastairInitial point: 19990101T0000Z Final point: 19990101T0000Z 19990101T0000Z/t1 -triggered off [] 19990101T0000Z/t2 -triggered off ['19990101T0000Z/t1'] cylc-flow-8.6.4/tests/functional/broadcast/09-remote/flow.cylc0000664000175000017500000000061715202510242024423 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] initial cycle point = 1999 final cycle point = 1999 [[graph]] P1Y = t1 => t2 [runtime] [[t1]] script = """ cylc broadcast -v -v --debug "${CYLC_WORKFLOW_ID}" \ -n t2 -s 'script=true' """ platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[t2]] script = false cylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel.t0000775000175000017500000000170415202510242023630 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast cancel with settings in a file . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/07-timeout.t0000775000175000017500000000167015202510242023161 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcasts a timeout setting . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/11-file-2/0000775000175000017500000000000015202510242022350 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/11-file-2/broadcast-1.cylc0000664000175000017500000000003415202510242025321 0ustar alastairalastairscript=printenv CYLC_FOOBAR cylc-flow-8.6.4/tests/functional/broadcast/11-file-2/broadcast-2.cylc0000664000175000017500000000006215202510242025323 0ustar alastairalastair[environment] CYLC_FOOBAR=""" foo bar baz """ cylc-flow-8.6.4/tests/functional/broadcast/11-file-2/reference.log0000664000175000017500000000012415202510242025006 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/broadcast/11-file-2/flow.cylc0000664000175000017500000000050015202510242024166 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] [[graph]] R1 = "t1 => t2" [runtime] [[t1]] script = """ cylc broadcast -n 't2' \ -F "${CYLC_WORKFLOW_RUN_DIR}/broadcast-1.cylc" \ -F "${CYLC_WORKFLOW_RUN_DIR}/broadcast-2.cylc" \ "${CYLC_WORKFLOW_ID}" """ [[t2]] script = false cylc-flow-8.6.4/tests/functional/broadcast/14-display-format.t0000664000175000017500000000450015202510242024414 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test the --format option. . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" << '__EOF__' [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = foo [runtime] [[foo]] pre-script = """ cylc broadcast "$CYLC_WORKFLOW_ID" \ -p "$CYLC_TASK_CYCLE_POINT" -n "$CYLC_TASK_NAME" \ --set '[environment]horse=dorothy' \ --set 'post-script=echo "$horse"' """ script = """ cylc broadcast "$CYLC_WORKFLOW_ID" --display --format json > out.json cylc broadcast "$CYLC_WORKFLOW_ID" --display --format raw > raw1.stdout # Test deprecated option: cylc broadcast "$CYLC_WORKFLOW_ID" --display --raw 1> raw2.stdout 2> raw2.stderr """ __EOF__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach FOO_WORK_DIR="${WORKFLOW_RUN_DIR}/work/1/foo" TEST_NAME="${TEST_NAME_BASE}-cmp-json" cmp_json "$TEST_NAME" "${FOO_WORK_DIR}/out.json" << '__EOF__' { "1": { "foo": { "environment": { "horse": "dorothy" }, "post-script": "echo \"$horse\"" } } } __EOF__ cmp_ok "${FOO_WORK_DIR}/raw1.stdout" << '__EOF__' {'1': {'foo': {'environment': {'horse': 'dorothy'}, 'post-script': 'echo "$horse"'}}} __EOF__ cmp_ok "${FOO_WORK_DIR}/raw2.stdout" "${FOO_WORK_DIR}/raw1.stdout" cmp_ok "${FOO_WORK_DIR}/raw2.stderr" << __EOF__ DEPRECATED: the --raw option will be removed at Cylc 8.7; use --format=raw instead. __EOF__ purge cylc-flow-8.6.4/tests/functional/broadcast/08-space/0000775000175000017500000000000015202510242022373 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/08-space/reference.log0000664000175000017500000000025115202510242025032 0ustar alastairalastairInitial point: 20200202T0000Z Final point: 20200202T0000Z 20200202T0000Z/broadcast -triggered off [] 20200202T0000Z/test-env -triggered off ['20200202T0000Z/broadcast'] cylc-flow-8.6.4/tests/functional/broadcast/08-space/flow.cylc0000664000175000017500000000112315202510242024213 0ustar alastairalastair[meta] title=broadcast section-space-key description=Test broadcast set section-space-key syntax [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout=PT1M [scheduling] initial cycle point = 20200202 final cycle point = 20200202 [[graph]] P1M = "broadcast => test-env" [runtime] [[broadcast]] script=""" cylc broadcast -s '[environment] FOO=${FOO:-foo}' -n 'test-env' "${CYLC_WORKFLOW_ID}" """ [[test-env]] script=""" test "${FOO}" = 'foo' """ [[[environment]]] FOO=bar cylc-flow-8.6.4/tests/functional/broadcast/03-expire.t0000775000175000017500000000167215202510242022765 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast --expire=CYCLE_POINT . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/10-file-1.t0000775000175000017500000000167015202510242022542 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast settings in a file . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/12-file-stdin.t0000775000175000017500000000167115202510242023526 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast settings from STDIN . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/11-file-2.t0000775000175000017500000000167115202510242022545 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast settings in 2 files . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/06-bad-namespace.t0000775000175000017500000000226715202510242024155 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that broadcast to an undefined namespace fails. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" cylc play --debug --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/broadcast/test_header0000777000175000017500000000000015202510242027370 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/14-broadcast-checkpoint/0000775000175000017500000000000015202510242025364 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/14-broadcast-checkpoint/flow.cylc0000664000175000017500000000064415202510242027213 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] [[graph]] R1 = "t1 => t2" [runtime] [[t1]] script = """ cylc broadcast -s "[environment]VERSE = the quick brown fox" "${CYLC_WORKFLOW_ID}" cylc checkpoint "${CYLC_WORKFLOW_ID}" test1 """ [[t2]] script = """ cylc broadcast -s "[environment]PHRASE = the quick brown fox" "${CYLC_WORKFLOW_ID}" cylc checkpoint "${CYLC_WORKFLOW_ID}" test2 """ cylc-flow-8.6.4/tests/functional/broadcast/06-bad-namespace/0000775000175000017500000000000015202510242023756 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/06-bad-namespace/flow.cylc0000664000175000017500000000101215202510242025573 0ustar alastairalastair[meta] title=broadcast bad namespace description=Test broadcast to an undefined namespace fails. [scheduler] [[events]] abort on stall timeout = True stall timeout=PT1M [scheduling] initial cycle point = 20150808 final cycle point = 20150808 [[graph]] P1M = broadcast [runtime] [[broadcast]] script=""" # Broadcast to an undefined namespace; fail if the broadcast succeeds (it # should fail). ! cylc broadcast -s 'title=foo' -n 'zilch' "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/broadcast/00-simple/0000775000175000017500000000000015202510242022561 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/00-simple/expected-prep.out0000664000175000017500000000256515202510242026067 0ustar alastairalastairBroadcast set: + [*/root] [environment]BCAST=ROOT Broadcast set: + [20100808T00/foo] [environment]BCAST=FOO Broadcast set: + [*/bar] [environment]BCAST=BAR Broadcast set: + [20100809T00/baz] [environment]BCAST=BAZ Broadcast set: + [20100809T00/qux] [environment]BCAST=QUX Broadcast cancelled: - [20100809T00/qux] [environment]BCAST=QUX Broadcast set: + [*/wibble] [environment]BCAST=WIBBLE Broadcast cancelled: - [*/wibble] [environment]BCAST=WIBBLE Broadcast set: + [*/ENS] [environment]BCAST=ENS Broadcast set: + [*/ENS1] [environment]BCAST=ENS1 Broadcast set: + [20100809T00/m2] [environment]BCAST=M2 Broadcast set: + [*/m7] [environment]BCAST=M7 Broadcast set: + [*/m8] [environment]BCAST=M8 Broadcast set: + [*/m9] [environment]BCAST=M9 * |-ENS | `-environment | `-BCAST ENS |-ENS1 | `-environment | `-BCAST ENS1 |-bar | `-environment | `-BCAST BAR |-m7 | `-environment | `-BCAST M7 |-m8 | `-environment | `-BCAST M8 |-m9 | `-environment | `-BCAST M9 `-root `-environment `-BCAST ROOT 20100808T00 `-foo `-environment `-BCAST FOO 20100809T00 |-baz | `-environment | `-BCAST BAZ `-m2 `-environment `-BCAST M2 cylc-flow-8.6.4/tests/functional/broadcast/00-simple/expected-prep.err0000664000175000017500000000071015202510242026036 0ustar alastairalastairERROR: No broadcast to cancel/clear for these options: --cancel=[environment]BCAST --namespace=m4 --point=20100809T00 ERROR: No broadcast to cancel/clear for these options: --cancel=[environment]BCAST --namespace=m5 --point=* ERROR: No broadcast to cancel/clear for these options: --namespace=m6 ERROR: No broadcast to cancel/clear for these options: --namespace=m7 ERROR: No broadcast to cancel/clear for these options: --namespace=ENS3 cylc-flow-8.6.4/tests/functional/broadcast/00-simple/reference.log0000664000175000017500000000670715202510242025234 0ustar alastairalastairInitial point: 20100808T00 Final point: 20100809T00 20100808T00/prep -triggered off [] 20100808T00/foo -triggered off ['20100808T00/prep'] 20100808T00/check -triggered off ['20100808T00/prep'] 20100808T00/bar -triggered off ['20100808T00/foo'] 20100808T00/qux -triggered off ['20100808T00/foo'] 20100808T00/baz -triggered off ['20100808T00/foo'] 20100808T00/wibble -triggered off ['20100808T00/foo'] 20100809T00/foo -triggered off [] 20100808T00/m1 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m2 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m3 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m4 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m5 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m6 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m7 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m8 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/m9 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/n1 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100808T00/o1 -triggered off ['20100808T00/bar', '20100808T00/baz', '20100808T00/qux', '20100808T00/wibble'] 20100809T00/bar -triggered off ['20100809T00/foo'] 20100809T00/qux -triggered off ['20100809T00/foo'] 20100809T00/baz -triggered off ['20100809T00/foo'] 20100809T00/wibble -triggered off ['20100809T00/foo'] 20100808T00/wobble -triggered off ['20100808T00/m1', '20100808T00/m2', '20100808T00/m3', '20100808T00/m4', '20100808T00/m5', '20100808T00/m6', '20100808T00/m7', '20100808T00/m8', '20100808T00/m9', '20100808T00/n1', '20100808T00/o1'] 20100809T00/m2 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m8 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m5 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m4 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m7 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m6 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m1 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/n1 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/o1 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m3 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/m9 -triggered off ['20100809T00/bar', '20100809T00/baz', '20100809T00/qux', '20100809T00/wibble'] 20100809T00/wobble -triggered off ['20100809T00/m1', '20100809T00/m2', '20100809T00/m3', '20100809T00/m4', '20100809T00/m5', '20100809T00/m6', '20100809T00/m7', '20100809T00/m8', '20100809T00/m9', '20100809T00/n1', '20100809T00/o1'] cylc-flow-8.6.4/tests/functional/broadcast/00-simple/flow.cylc0000664000175000017500000001236415202510242024412 0ustar alastairalastair[meta] title = "test workflow for broadcast functionality" description = """ The first task broadcasts an environment variable "BCAST" to various cycles and namespaces. Then each task writes its point, name, and value of BCAST to a log for comparison with the expected result. """ [scheduler] cycle point format = %Y%m%dT%H allow implicit tasks = True [scheduling] initial cycle point = 20100808T00 final cycle point = 20100809T00 # Ensure the first cycle finishes before the second (last) one, so that the # broadcast to foo in the first cycle expires before shutdown: runahead limit = P0 [[graph]] R1 = "prep => check & foo" T00 = """ foo => bar & baz & qux & wibble => ENS? ENS:finish-all => wobble """ [runtime] [[root]] pre-script = "echo $CYLC_TASK_CYCLE_POINT $CYLC_TASK_NAME BCAST is $BCAST | tee -a $BCASTLOG" script = "true" # no sleep [[[environment]]] BCAST = ${BCAST:-(not set)} BCASTLOG = ${CYLC_WORKFLOW_SHARE_DIR}/broadcast.log PREPLOG = ${CYLC_WORKFLOW_SHARE_DIR}/prep [[prep]] pre-script = "rm -f $BCASTLOG $PREPLOG" script = """ set +x { # broadcast to all cycles and namespaces: cylc broadcast -s "[environment]BCAST = ROOT" $CYLC_WORKFLOW_ID # broadcast to 20100808T00/foo: cylc broadcast -p 20100808T00 -n foo -s "[environment]BCAST = FOO" $CYLC_WORKFLOW_ID # broadcast to bar at all cycles: cylc broadcast -n bar -s "[environment]BCAST = BAR" $CYLC_WORKFLOW_ID # broadcast to baz at 20100809T00: cylc broadcast -n baz -p 20100809T00 -s "[environment]BCAST = BAZ" $CYLC_WORKFLOW_ID # broadcast to qux at 20100809T00, then cancel it: cylc broadcast -n qux -p 20100809T00 -s "[environment]BCAST = QUX" $CYLC_WORKFLOW_ID cylc broadcast -n qux -p 20100809T00 --cancel "[environment]BCAST" $CYLC_WORKFLOW_ID # broadcast to wibble at all cycles, then clear it: cylc broadcast -n wibble -s "[environment]BCAST = WIBBLE" $CYLC_WORKFLOW_ID cylc broadcast -n wibble --clear $CYLC_WORKFLOW_ID # broadcast to all members of ENS, all cycles: cylc broadcast -n ENS -s "[environment]BCAST = ENS" $CYLC_WORKFLOW_ID # broadcast to all members of ENS1, all cycles: cylc broadcast -n ENS1 -s "[environment]BCAST = ENS1" $CYLC_WORKFLOW_ID # broadcast to a single member m2 of ENS1, in 20100809T00: cylc broadcast -n m2 -p 20100809T00 -s "[environment]BCAST = M2" $CYLC_WORKFLOW_ID # cancel broadcast to m4 of ENS1, in 20100809T00 (will not work): ! cylc broadcast -n m4 -p 20100809T00 --cancel "[environment]BCAST" $CYLC_WORKFLOW_ID # cancel broadcast to m5 of ENS1 at all cycles (will not work): ! cylc broadcast -n m5 --cancel "[environment]BCAST" $CYLC_WORKFLOW_ID # clear broadcast to m6 of ENS1 at all cycles (will not work): ! cylc broadcast -n m6 --clear $CYLC_WORKFLOW_ID # clear, then reset, broadcast to m7 of ENS1 at all cycles: ! cylc broadcast -n m7 --clear $CYLC_WORKFLOW_ID cylc broadcast -n m7 -s "[environment]BCAST = M7" $CYLC_WORKFLOW_ID # reset broadcast to m8 of ENS1 at 20100809T00 cylc broadcast -n m8 -s "[environment]BCAST = M8" $CYLC_WORKFLOW_ID # reset broadcast to m9 of ENS1 at all cycles cylc broadcast -n m9 -s "[environment]BCAST = M9" $CYLC_WORKFLOW_ID # clear broadcast for ENS3 (will not work): ! cylc broadcast -n ENS3 --clear $CYLC_WORKFLOW_ID } 1>${PREPLOG}.out 2>${PREPLOG}.err """ [[check]] # Check that the broadcasts performed by the previous task were # recorded properly by the scheduler (doing this in another task # gives time for the datastore to update broadcast data). script = """ # list the result to prep task stdout: cylc broadcast --display $CYLC_WORKFLOW_ID \ 1>>${PREPLOG}.out 2>>${PREPLOG}.err set -x sed -i '/DEBUG -/d' ${PREPLOG}.out sed -i '/\(DEBUG\|WARNING\|ERROR\) -/d' ${PREPLOG}.err # workaround for platforms affected by https://github.com/cylc/cylc-flow/issues/3585 sed -i '/BASH_XTRACEFD/d' ${PREPLOG}.err diff -u "${CYLC_WORKFLOW_RUN_DIR}/expected-prep.out" ${PREPLOG}.out diff -u "${CYLC_WORKFLOW_RUN_DIR}/expected-prep.err" ${PREPLOG}.err """ [[ENS]] [[ENS1]] inherit = ENS [[m1,m2,m3,m4,m5,m6,m7,m8,m9]] inherit = ENS1 [[ENS2]] inherit = ENS [[n1]] inherit = ENS2 [[ENS3]] inherit = ENS [[o1]] inherit = ENS3 [[wobble]] script = """ if [[ "${CYLC_TASK_CYCLE_POINT}" == "20100809T00" ]]; then sleep 5 fi """ cylc-flow-8.6.4/tests/functional/broadcast/00-simple/broadcast.ref0000664000175000017500000000177415202510242025232 0ustar alastairalastair20100808T00 bar BCAST is BAR 20100808T00 baz BCAST is ROOT 20100808T00 check BCAST is ROOT 20100808T00 foo BCAST is FOO 20100808T00 m1 BCAST is ENS1 20100808T00 m2 BCAST is ENS1 20100808T00 m3 BCAST is ENS1 20100808T00 m4 BCAST is ENS1 20100808T00 m5 BCAST is ENS1 20100808T00 m6 BCAST is ENS1 20100808T00 m7 BCAST is M7 20100808T00 m8 BCAST is M8 20100808T00 m9 BCAST is M9 20100808T00 n1 BCAST is ENS 20100808T00 o1 BCAST is ENS 20100808T00 qux BCAST is ROOT 20100808T00 wibble BCAST is ROOT 20100808T00 wobble BCAST is ROOT 20100809T00 bar BCAST is BAR 20100809T00 baz BCAST is BAZ 20100809T00 foo BCAST is ROOT 20100809T00 m1 BCAST is ENS1 20100809T00 m2 BCAST is M2 20100809T00 m3 BCAST is ENS1 20100809T00 m4 BCAST is ENS1 20100809T00 m5 BCAST is ENS1 20100809T00 m6 BCAST is ENS1 20100809T00 m7 BCAST is M7 20100809T00 m8 BCAST is M8 20100809T00 m9 BCAST is M9 20100809T00 n1 BCAST is ENS 20100809T00 o1 BCAST is ENS 20100809T00 qux BCAST is ROOT 20100809T00 wibble BCAST is ROOT 20100809T00 wobble BCAST is ROOT cylc-flow-8.6.4/tests/functional/broadcast/03-expire/0000775000175000017500000000000015202510242022567 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/03-expire/reference.log0000664000175000017500000000111215202510242025223 0ustar alastairalastairInitial point: 20200101T0000Z Final point: 20250101T0000Z 20200101T0000Z/broadcast -triggered off [] 20250101T0000Z/broadcast -triggered off [] 20200101T0000Z/t1 -triggered off ['20200101T0000Z/broadcast'] 20250101T0000Z/t1 -triggered off ['20250101T0000Z/broadcast'] 20200101T0000Z/broadcast-expire -triggered off ['20150101T0000Z/t2', '20200101T0000Z/t1'] 20200101T0000Z/t2 -triggered off ['20200101T0000Z/broadcast-expire'] 20250101T0000Z/broadcast-expire -triggered off ['20200101T0000Z/t2', '20250101T0000Z/t1'] 20250101T0000Z/t2 -triggered off ['20250101T0000Z/broadcast-expire'] cylc-flow-8.6.4/tests/functional/broadcast/03-expire/flow.cylc0000664000175000017500000000355515202510242024422 0ustar alastairalastair[meta] title=broadcast expire description=Test broadcast expire option [scheduler] UTC mode = True [scheduling] initial cycle point = 2020 final cycle point = 2025 [[graph]] P5Y=""" broadcast => t1 => broadcast-expire => t2 t2[-P5Y] => broadcast-expire """ [runtime] [[broadcast]] script = """ cylc broadcast \ -s '[environment]FABRIC=Wool' \ -s "[environment]ORGANISM=sheep" \ -p "${CYLC_TASK_CYCLE_POINT}" \ -n 'F1' \ "${CYLC_WORKFLOW_ID}" \ | tee 'broadcast.out' """ post-script = """ diff -u - 'broadcast.out' <<__OUT__ Broadcast set: + [${CYLC_TASK_CYCLE_POINT}/F1] [environment]FABRIC=Wool + [${CYLC_TASK_CYCLE_POINT}/F1] [environment]ORGANISM=sheep __OUT__ """ [[broadcast-expire]] script = """ NEXT_CYCLE_POINT=$(cylc cycletime --offset=P5Y) cylc broadcast --expire="${NEXT_CYCLE_POINT}" "${CYLC_WORKFLOW_ID}" \ | tee 'broadcast.out' """ post-script = """ diff -u - 'broadcast.out' <<__OUT__ Broadcast cancelled: - [${CYLC_TASK_CYCLE_POINT}/F1] [environment]FABRIC=Wool - [${CYLC_TASK_CYCLE_POINT}/F1] [environment]ORGANISM=sheep __OUT__ """ [[F1]] script = """ echo "${FABRIC} is from ${ORGANISM}." | tee 'echo.out' """ [[[environment]]] FABRIC=Silk ORGANISM=silk worm [[t1]] inherit=F1 post-script=""" diff -u - 'echo.out' <<<'Wool is from sheep.' """ [[t2]] inherit=F1 post-script=""" diff -u - 'echo.out' <<<'Silk is from silk worm.' """ cylc-flow-8.6.4/tests/functional/broadcast/04-empty.t0000775000175000017500000000167015202510242022626 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcast of an empty string . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel/0000775000175000017500000000000015202510242023436 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel/broadcast-1.cylc0000664000175000017500000000017515202510242026415 0ustar alastairalastairscript=printenv CYLC_FOOBAR && ! printenv CYLC_QUX [environment] CYLC_FOOBAR=""" foo bar baz """ CYLC_QUX=quux corge cylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel/broadcast-2.cylc0000664000175000017500000000004615202510242026413 0ustar alastairalastair[environment] CYLC_QUX=quux corge cylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel/reference.log0000664000175000017500000000012415202510242026074 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/broadcast/13-file-cancel/flow.cylc0000664000175000017500000000057315202510242025266 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] [[graph]] R1 = "t1 => t2" [runtime] [[t1]] script = """ cylc broadcast -n 't2' -F "${CYLC_WORKFLOW_RUN_DIR}/broadcast-1.cylc" "${CYLC_WORKFLOW_ID}" cylc broadcast -n 't2' -G "${CYLC_WORKFLOW_RUN_DIR}/broadcast-2.cylc" "${CYLC_WORKFLOW_ID}" """ [[t2]] script = false cylc-flow-8.6.4/tests/functional/broadcast/02-inherit.t0000775000175000017500000000170415202510242023126 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test broadcasts, with overriding inheritance. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/broadcast/05-bad-point.t0000775000175000017500000000226715202510242023351 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that broadcast to an invalid cycle point fails. . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}" cylc play --debug --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/functional/broadcast/12-file-stdin/0000775000175000017500000000000015202510242023331 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/12-file-stdin/broadcast.cylc0000664000175000017500000000011615202510242026145 0ustar alastairalastairscript=printenv CYLC_FOOBAR [environment] CYLC_FOOBAR=""" foo bar baz """ cylc-flow-8.6.4/tests/functional/broadcast/12-file-stdin/reference.log0000664000175000017500000000012415202510242025767 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/broadcast/12-file-stdin/flow.cylc0000664000175000017500000000044615202510242025160 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] [[graph]] R1 = "t1 => t2" [runtime] [[t1]] script = """ cylc broadcast -n 't2' -F - "${CYLC_WORKFLOW_ID}" \ <"${CYLC_WORKFLOW_RUN_DIR}/broadcast.cylc" """ [[t2]] script = false cylc-flow-8.6.4/tests/functional/broadcast/10-file-1/0000775000175000017500000000000015202510242022346 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/broadcast/10-file-1/broadcast.cylc0000664000175000017500000000045515202510242025170 0ustar alastairalastairscript=""" printenv CYLC_FOOBAR # This hash char should not cause the rest of the script to be stripped out # - https://github.com/cylc/cylc-flow/pull/5933 if (($CYLC_TASK_TRY_NUMBER < 2 )); then false fi """ execution retry delays = PT1S, PT2S [environment] CYLC_FOOBAR=""" foo bar baz """ cylc-flow-8.6.4/tests/functional/broadcast/10-file-1/reference.log0000664000175000017500000000016115202510242025005 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/functional/broadcast/10-file-1/flow.cylc0000664000175000017500000000037515202510242024176 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] [[graph]] R1 = "t1 => t2" [runtime] [[t1]] script = """ cylc broadcast -n 't2' -F "${CYLC_WORKFLOW_RUN_DIR}/broadcast.cylc" "${CYLC_WORKFLOW_ID}" """ [[t2]] script = false cylc-flow-8.6.4/tests/functional/intelligent-host-selection/0000775000175000017500000000000015202510242024365 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/03-polling/0000775000175000017500000000000015202510242026251 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/03-polling/flow.cylc0000664000175000017500000000250115202510242030072 0ustar alastairalastair[meta] title = "Try out scenarios for intelligent host selection." description = """ Runs two long running tasks on a known good platform and on a platform with some unreachable hosts. Once each has started trigger a task which kills each to test the execution polling. """ [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] R1 = """ goodhosttask:start => stop_g mixedhosttask:start => stop_m goodhosttask:fail & mixedhosttask:fail """ [runtime] [[goodhosttask]] # Sleep for a long time, expect to be killed by stop_g script=sleep 120 & echo $! >file; wait platform = goodhostplatform [[mixedhosttask]] # Sleep for a long time, expect to be killed by stop_m script=sleep 120 & echo $! >file; wait platform = mixedhostplatform [[stop_g]] # Kill goodhosttask when polling confirms it's started. script=""" sleep 5 # Give the badhosts list time to empty cylc kill "$CYLC_WORKFLOW_ID//1/goodhosttask" || true """ [[stop_m]] # Kill mixedhosttask when polling confirms it's started. script=""" sleep 5 # Give the badhosts list time to empty cylc kill "$CYLC_WORKFLOW_ID//1/mixedhosttask" || true """ cylc-flow-8.6.4/tests/functional/intelligent-host-selection/02-badhosts.t0000664000175000017500000000612215202510242026601 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote initialisation still fails where a task has a platform # with only unreachable hosts. # n.b. Hosts picked for unlikelyhood of names matching any real host. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 6 # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [platforms] [[badhostplatform]] job runner = my_background hosts = e9755ca30f5, 3c0b4799402 install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True [[[selection]]] method = definition order [[goodhostplatform]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True [[mixedhostplatform]] job runner = my_background hosts = unreachable_host, ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True [[[selection]]] method = 'definition order' " #------------------------------------------------------------------------------- # Uncomment to print config for manual testing of workflow. # cylc config -i '[platforms]' >&2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" LOGFILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Check that badhosttask has submit failed, but not good or mixed named_grep_ok "badhost task submit failed" \ "1/badhosttask.* submit-failed" "${LOGFILE}" named_grep_ok "goodhost suceeded" \ "1/mixedhosttask.* succeeded" "${LOGFILE}" named_grep_ok "mixedhost task suceeded" \ "1/goodhosttask.* succeeded" "${LOGFILE}" # Check that when a task fail badhosts associated with that task's platform # are removed from the badhosts set. named_grep_ok "remove task platform bad hosts after submit-fail" \ "initialisation did not complete (no hosts were reachable)" \ "${LOGFILE}" purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/07-cylc7-badhost.t0000664000175000017500000000356115202510242027446 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test task does not run on localhost if Cylc 7 syntax # [runtime][][remote]host is unreachable - # https://github.com/cylc/cylc-flow/issues/4569 . "$(dirname "$0")/test_header" set_test_number 4 # Host name picked for unlikelihood of matching any real host BAD_HOST="f65b965bb914" create_test_global_config "" " [platforms] [[badhostplatform]] hosts = ${BAD_HOST} " init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduler] [[events]] stall timeout = PT0M abort on stall timeout = True [scheduling] cycling mode = integer [[graph]] R1 = sattler [runtime] [[sattler]] [[[remote]]] host = ${BAD_HOST} __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --no-detach "${WORKFLOW_NAME}" grep_workflow_log_ok "${TEST_NAME_BASE}-grep-1" \ "platform: badhostplatform - initialisation did not complete" grep_workflow_log_ok "${TEST_NAME_BASE}-grep-2" "CRITICAL - Workflow stalled" purge cylc-flow-8.6.4/tests/functional/intelligent-host-selection/lib/0000775000175000017500000000000015202510242025133 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/lib/python/0000775000175000017500000000000015202510242026454 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/lib/python/my_background.py0000664000175000017500000000166115202510242031656 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.job_runner_handlers.background import BgCommandHandler class MyBgCommandHandler(BgCommandHandler): pass JOB_RUNNER_HANDLER = MyBgCommandHandler() cylc-flow-8.6.4/tests/functional/intelligent-host-selection/00-mixedhost/0000775000175000017500000000000015202510242026606 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/00-mixedhost/flow.cylc0000664000175000017500000000072415202510242030434 0ustar alastairalastair [meta] title = "Try out scenarios for intelligent host selection." description = """ Tasks - goodhost: a control to check that everything works - mixedhost contains some hosts that will and won't fail """ [scheduling] initial cycle point = 1 [[graph]] R1 = mixedhosttask & goodhosttask [runtime] [[root]] script = true [[goodhosttask]] platform = goodhostplatform [[mixedhosttask]] platform = mixedhostplatform cylc-flow-8.6.4/tests/functional/intelligent-host-selection/02-badhosts/0000775000175000017500000000000015202510242026413 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/02-badhosts/flow.cylc0000664000175000017500000000155015202510242030237 0ustar alastairalastair [meta] title = "Try out scenarios for intelligent host selection." description = """ Tasks: - goodhost: a control to check that everything works - badhost is always going to fail - mixedhost contains some hosts that will and won't fail """ [scheduler] [[events]] abort on stall timeout = true stall timeout = PT0S [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] # Run good and mixed as controls R1 = """ badhosttask:submit-fail? => goodhosttask & mixedhosttask mixedhosttask:submit-fail? # permit mixedhosttask to submit-fail """ [runtime] [[root]] script = true [[badhosttask]] platform = badhostplatform [[goodhosttask]] platform = goodhostplatform [[mixedhosttask]] platform = mixedhostplatform cylc-flow-8.6.4/tests/functional/intelligent-host-selection/00-mixedhost.t0000664000175000017500000000473315202510242027002 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test remote initialisation - where a task has a platform with an unreachable # host. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [platforms] [[goodhostplatform]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True [[mixedhostplatform]] job runner = my_background hosts = unreachable_host, ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True [[[selection]]] method = 'definition order' " #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" # Run a bunch of tests on the workflow logs to ensure that warning messages # produced by Intelligent Host Selection Logic have happened. named_grep_ok "unreachable host warning" \ 'unreachable_host has been added to the list of unreachable hosts' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" # Ensure that retrying in this context doesn't increment try number: grep_fail "1/mixedhosttask/02" "${WORKFLOW_RUN_DIR}/log/scheduler/log" purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/01-periodic-clear-badhosts/0000775000175000017500000000000015202510242031272 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/01-periodic-clear-badhosts/bin/0000775000175000017500000000000015202510242032042 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/01-periodic-clear-badhosts/bin/mock-ssh0000775000175000017500000000076415202510242033523 0ustar alastairalastair#!/usr/bin/env bash shift # the first argument to SSH is the host we are connecting to (ignore it) COUNT_FILE="$(dirname "$0")/count" echo 'x' >> "$COUNT_FILE" if [[ $(wc -l "$COUNT_FILE" | cut -d ' ' -f 1) -eq 1 ]]; then # the first time we make it look like an SSH failure exit 255 else # from then on we make it look like SSH is working fine # do the bare minimum to make it look like remote-init worked echo 'KEYSTARTxxxxKEYEND' echo 'REMOTE INIT DONE' exit 0 fi cylc-flow-8.6.4/tests/functional/intelligent-host-selection/01-periodic-clear-badhosts/flow.cylc0000664000175000017500000000043715202510242033121 0ustar alastairalastair[scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] R1 = a [runtime] [[a]] platform = fake-platform submission retry delays = 1*PT1S cylc-flow-8.6.4/tests/functional/intelligent-host-selection/06-from-platform-group-fails.t0000664000175000017500000000613515202510242032015 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that Cylc fails sensibly when a plaform group with no # accessible hosts is selected. # n.b. We don't care about definition order in this test becuase all # hosts and platforms fail. . "$(dirname "$0")/test_header" set_test_number 12 #------------------------------------------------------------------------------- # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [platforms] [[badhostplatform1]] job runner = my_background hosts = bad_host1, bad_host2 [[badhostplatform2]] job runner = my_background hosts = bad_host3, bad_host4 [platform groups] [[badplatformgroup]] platforms = badhostplatform1, badhostplatform2 " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" logfile="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Check workflow fails for the reason we want it to fail named_grep_ok \ "Workflow stalled with 1/bad (submit-failed)" \ "1/bad did not complete the required outputs" \ "$logfile" # Look for message indicating that remote init has failed on each bad_host # on every bad platform. platform='badhostplatform1' for host in {1..2}; do host="bad_host${host}" log_scan \ "${TEST_NAME_BASE}-remote-init-fail-${host}" \ "${logfile}" 1 0 \ "platform: ${platform} - remote init (on ${host})" \ "platform: ${platform} - Could not connect to ${host}." done platform='badhostplatform2' for host in {3..4}; do host="bad_host${host}" log_scan \ "${TEST_NAME_BASE}-remote-init-fail-${host}" \ "${logfile}" 1 0 \ "platform: ${platform} - remote init (on ${host})" \ "platform: ${platform} - Could not connect to ${host}." done # Look for message indicating that remote init has failed. named_grep_ok \ "platform: badhostplatform. - initialisation did not complete (no hosts were reachable)" \ "platform: badhostplatform. - initialisation did not complete (no hosts were reachable)" \ "${logfile}" purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/04-kill/0000775000175000017500000000000015202510242025541 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/04-kill/flow.cylc0000664000175000017500000000241415202510242027365 0ustar alastairalastair[meta] title = "Try out scenarios for intelligent host selection." description = """ Tasks - goodhost: a control to check that everything works - badhost is always going to fail - mixedhost contains some hosts that will and won't fail """ [scheduler] [[events]] expected task failures = 1/goodhosttask, 1/mixedhosttask [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] # Run good and mixed as controls R1 = """ goodhosttask & mixedhosttask goodhosttask:start => stop_g mixedhosttask:start => stop_m """ [runtime] [[root]] script = sleep 120 & echo $! >file; wait [[mystop]] script=""" sleep 5 # Give the badhosts list time to empty cylc kill "$CYLC_WORKFLOW_ID//$TASK" cylc stop $CYLC_WORKFLOW_ID """ [[goodhosttask]] platform = goodhostplatform [[mixedhosttask]] script=sleep 120 & echo $! >file; wait platform = mixedhostplatform [[stop_g]] inherit = mystop [[[environment]]] TASK = 1/goodhosttask [[stop_m]] inherit = mystop [[[environment]]] TASK = 1/mixedhosttask cylc-flow-8.6.4/tests/functional/intelligent-host-selection/04-kill.t0000664000175000017500000000510315202510242025725 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test job kill. Set the auto clearance of badhosts to be << small time # so that kill will need to retry, despite 'unreachable_host' being idetified # as unreachable by job submission. export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [scheduler] [[main loop]] [[[reset bad hosts]]] interval = PT5S [platforms] [[goodhostplatform]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} [[mixedhostplatform]] job runner = my_background hosts = unreachable_host, ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} [[[selection]]] method = 'definition order' " #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach \ "${WORKFLOW_NAME}" LOGFILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Check that when a task fail badhosts associated with that task's platform # are removed from the badhosts set. named_grep_ok "job kill fails" \ "unreachable_host has been added to the list of unreachable hosts" \ "${LOGFILE}" named_grep_ok "job kill retries & succeeds" \ "\[jobs-kill out\] \[TASK JOB SUMMARY\].*1/mixedhosttask/01" \ "${LOGFILE}" purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/03-polling.t0000664000175000017500000000610015202510242026433 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intelligent host selection for job polling. # # Set the peridic clearance of unreachable hosts by # `[scheduler][main loop][reset bad hosts]interval = PT1S` so that between # Submission of a job and execution polling the list of bad hosts will have # cleared. Having cleared bad hosts we can then test that polling goes through # the host selection process. export REQUIRE_PLATFORM='loc:remote fs:indep' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [scheduler] [[main loop]] [[[reset bad hosts]]] interval = PT1S [platforms] [[goodhostplatform]] hosts = ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True communication method = poll execution polling intervals = 10*PT3S submission polling intervals = PT0S, 5*PT2S [[mixedhostplatform]] job runner = my_background hosts = unreachable_host, ${CYLC_TEST_HOST} install target = ${CYLC_TEST_INSTALL_TARGET} retrieve job logs = True communication method = poll execution polling intervals = 10*PT3S submission polling intervals = PT0S, 5*PT2S [[[selection]]] method = 'definition order' " #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach \ "${WORKFLOW_NAME}" LOGFILE="${WORKFLOW_RUN_DIR}/log/scheduler/log" # Check that when a task fail badhosts associated with that task's platform # are removed from the badhosts set. named_grep_ok \ "job poll fails" \ "unreachable_host has been added to the list of unreachable hosts" \ "${LOGFILE}" named_grep_ok "job poll retries & succeeds" \ "\[jobs-poll out\] \[TASK JOB SUMMARY\].*1/mixedhosttask/01" \ "${LOGFILE}" purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/01-periodic-clear-badhosts.t0000664000175000017500000000606515202510242031466 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test mainloop plugin periodically clears badhosts: # * simulate remote-init failure due to SSH issues # * ensure that "reset bad hosts" allows this task to auto "submit retry" # once the bad host is cleared . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" create_test_global_config '' " [scheduler] [[main loop]] [[[reset bad hosts]]] interval = PT1S [platforms] [[fake-platform]] hosts = localhost # we set the install target to make it look like a remote platform # (and so trigger remote-init) install target = fake-install-target # we botch the SSH command so we can simulate SSH failure ssh command = $HOME/cylc-run/$WORKFLOW_NAME/bin/mock-ssh " #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play \ --debug \ --no-detach \ --abort-if-any-task-fails \ "${WORKFLOW_NAME}" # scrape platform events from the log sed -n \ 's/.* - \(platform: .*\)/\1/p' \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" \ > platform-log # check this matches expectations # we would expect: # * the task will attempt to remote-init # * this will fail (because we made it fail) # * the task will retry (because of the retry delays) # * the task will attempt to remote-init again # * the remote init will succeed this time # * the task will attempt file-installation # * file installation will fail because the install target is incorrect cmp_ok platform-log <<__HERE__ platform: fake-platform - remote init (on localhost) platform: fake-platform - initialisation did not complete platform: fake-platform - remote init (on localhost) platform: fake-platform - remote file install (on localhost) platform: fake-platform - initialisation did not complete platform: fake-platform - remote tidy (on localhost) __HERE__ purge exit 0 cylc-flow-8.6.4/tests/functional/intelligent-host-selection/test_header0000777000175000017500000000000015202510242032702 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/05-from-platform-group/0000775000175000017500000000000015202510242030526 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/05-from-platform-group/reference.log0000664000175000017500000000013215202510242033163 0ustar alastairalastairInitial point: 1 Final point: 1 1/good -triggered off [] 1/ugly -triggered off ['1/good'] cylc-flow-8.6.4/tests/functional/intelligent-host-selection/05-from-platform-group/flow.cylc0000664000175000017500000000125615202510242032355 0ustar alastairalastair#!Jinja2 [meta] title = "Try out scenarios for intelligent host selection." description = """ Tasks ===== Good ---- Should pass without problems. Ugly ---- - Fails entirely on a duff platform. - Fails on the first host of a mixed platfrom. - Succeeds on the second host of the second platform. """ [scheduler] [[events]] # abort on stalled = true [scheduling] initial cycle point = 1 [[graph]] R1 = good => ugly [runtime] [[root]] script = true [[good]] platform = goodplatformgroup [[ugly]] platform = mixedplatformgroup cylc-flow-8.6.4/tests/functional/intelligent-host-selection/06-from-platform-group-fails/0000775000175000017500000000000015202510242031623 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/intelligent-host-selection/06-from-platform-group-fails/flow.cylc0000664000175000017500000000064615202510242033454 0ustar alastairalastair[meta] title = "Try out scenarios for intelligent host selection." description = """ Tasks ===== Bad --- Fails on all hosts on all plaforms """ [scheduler] [[events]] stall timeout = PT0S [scheduling] initial cycle point = 1 [[graph]] R1 = bad [runtime] [[root]] script = true [[bad]] platform = badplatformgroup cylc-flow-8.6.4/tests/functional/intelligent-host-selection/05-from-platform-group.t0000664000175000017500000000673115202510242030722 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that Cylc Can select a host from a platform group # Failing if there is no good host _any_ platform # Succeeding if there is no bad host on any platform in the group export REQUIRE_PLATFORM='loc:remote fs:indep comms:tcp' . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 11 # Uses a fake background job runner to get around the single host restriction. create_test_global_config "" " [platforms] [[${CYLC_TEST_PLATFORM}]] # mixed host platform job runner = my_background hosts = unreachable_host, ${CYLC_TEST_HOST} [[[selection]]] method = 'definition order' [[badhostplatform]] job runner = my_background hosts = bad_host1, bad_host2 [[[selection]]] method = 'definition order' [platform groups] [[mixedplatformgroup]] platforms = badhostplatform, ${CYLC_TEST_PLATFORM} [[[selection]]] method = definition order [[goodplatformgroup]] platforms = ${CYLC_TEST_PLATFORM} [[[selection]]] method = definition order " #------------------------------------------------------------------------------- # Uncomment to print config for manual testing of workflow. # cylc config -i '[platforms]' >&2 # cylc config -i '[platform groups]' >&2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Install the fake background job runner. cp -r "${TEST_SOURCE_DIR}/lib" "${WORKFLOW_RUN_DIR}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" --reference-test # should try remote-init on bad_host{1,2} then fail log_scan \ "${TEST_NAME_BASE}-badhostplatformgroup" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" 1 0 \ 'platform: badhostplatform - remote init (on bad_host1)' \ 'platform: badhostplatform - Could not connect to bad_host1.' \ 'platform: badhostplatform - remote init (on bad_host2)' \ 'platform: badhostplatform - Could not connect to bad_host2.' \ # should try remote-init on unreachable_host, then $CYLC_TEST_HOST then pass log_scan \ "${TEST_NAME_BASE}-goodplatformgroup" \ "${WORKFLOW_RUN_DIR}/log/scheduler/log" 1 0 \ "platform: ${CYLC_TEST_PLATFORM} - remote init (on unreachable_host)" \ "platform: ${CYLC_TEST_PLATFORM} - Could not connect to unreachable_host." \ "platform: ${CYLC_TEST_PLATFORM} - remote init (on ${CYLC_TEST_HOST})" \ "platform: ${CYLC_TEST_PLATFORM} - remote file install (on ${CYLC_TEST_HOST})" \ "\[1/ugly/01:preparing\] => submitted" purge exit 0 cylc-flow-8.6.4/tests/functional/message-triggers/0000775000175000017500000000000015202510242022361 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/message-triggers/00-basic/0000775000175000017500000000000015202510242023657 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/message-triggers/00-basic/reference.log0000664000175000017500000000077715202510242026333 0ustar alastairalastairInitial point: 20140801T0000Z Final point: 20141201T0000Z 20140801T0000Z/baz -triggered off ['20140601T0000Z/foo'] 20140801T0000Z/foo -triggered off [] 20141001T0000Z/foo -triggered off [] 20140801T0000Z/bar -triggered off ['20140801T0000Z/foo'] 20141201T0000Z/foo -triggered off [] 20141001T0000Z/baz -triggered off ['20140801T0000Z/foo'] 20141001T0000Z/bar -triggered off ['20141001T0000Z/foo'] 20141201T0000Z/baz -triggered off ['20141001T0000Z/foo'] 20141201T0000Z/bar -triggered off ['20141201T0000Z/foo'] cylc-flow-8.6.4/tests/functional/message-triggers/00-basic/flow.cylc0000664000175000017500000000117715202510242025510 0ustar alastairalastair[meta] title = "test workflow for cylc-6 message triggers" [scheduler] UTC mode = True [scheduling] initial cycle point = 20140801T00 final cycle point = 20141201T00 [[graph]] P2M = """ foo:out1 => bar foo[-P2M]:out2 => baz """ [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message -- "${CYLC_WORKFLOW_ID} "${CYLC_TASK_JOB} "file 1 done" cylc message -- "${CYLC_WORKFLOW_ID} "${CYLC_TASK_JOB} "file 2 done" """ [[[outputs]]] out1 = "file 1 done" out2 = "file 2 done" [[bar, baz]] script = true cylc-flow-8.6.4/tests/functional/message-triggers/test_header0000777000175000017500000000000015202510242030676 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/message-triggers/02-action.t0000664000175000017500000000326715202510242024252 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that message triggers (custom outputs) are actioned immediately even if # nothing else is happening at the time - GitHub #2548. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" # The workflow tests that two tasks suicide immediately on message triggers. TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" # Check that final task pool indicates bar and baz ran # TODO: some final null task pool tests would be better on task_states table! TEST_NAME=${TEST_NAME_BASE}-cmp-task-pool sqlite3 "${WORKFLOW_RUN_DIR}/log/db" 'select cycle, name, status from task_pool;' > task-pool.log cmp_ok task-pool.log - <'/dev/null' purge cylc-flow-8.6.4/tests/functional/message-triggers/02-action/0000775000175000017500000000000015202510242024055 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/message-triggers/02-action/flow.cylc0000664000175000017500000000071215202510242025700 0ustar alastairalastair[scheduler] [[events]] abort on inactivity timeout = True inactivity timeout = PT30S [scheduling] [[graph]] R1 = """ foo:a => bar foo:b & bar => baz""" [runtime] [[foo]] script = """ cylc message "the quick brown fox" cylc message "jumped over the lazy dog" """ [[[outputs]]] a = "the quick brown fox" b = "jumped over the lazy dog" [[bar]] [[baz]] cylc-flow-8.6.4/tests/functional/message-triggers/00-basic.t0000664000175000017500000000172315202510242024047 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test new-style simplified message triggers (see GitHub #1761) . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/execution-time-limit/0000775000175000017500000000000015202510242023164 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/execution-time-limit/03-pbs.t0000775000175000017500000000266315202510242024367 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution time limit setting, PBS job JOB_RUNNER="${0##*\/??-}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER%%.t}" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/foo" grep_ok '#PBS -l walltime=70' "${LOGD}/01/job" purge exit cylc-flow-8.6.4/tests/functional/execution-time-limit/02-slurm.t0000775000175000017500000000266715202510242024750 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution time limit setting, slurm job JOB_RUNNER="${0##*\/??-}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER%%.t}" . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOGD="$RUN_DIR/${WORKFLOW_NAME}/log/job/1/foo" grep_ok '#SBATCH --time=0:05' "${LOGD}/01/job" purge exit cylc-flow-8.6.4/tests/functional/execution-time-limit/02-slurm/0000775000175000017500000000000015202510242024545 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/execution-time-limit/02-slurm/reference.log0000664000175000017500000000012015202510242027177 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/functional/execution-time-limit/02-slurm/flow.cylc0000664000175000017500000000065615202510242026377 0ustar alastairalastair#!jinja2 [scheduler] [[events]] inactivity timeout = PT2S [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = """ if [[ "${CYLC_TASK_SUBMIT_NUMBER}" == '1' ]]; then sleep 300 fi """ platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[job]]] execution time limit = PT5S execution retry delays = PT0S cylc-flow-8.6.4/tests/functional/execution-time-limit/test_header0000777000175000017500000000000015202510242031501 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/execution-time-limit/03-pbs0000777000175000017500000000000015202510242025412 202-slurmustar alastairalastaircylc-flow-8.6.4/tests/functional/execution-time-limit/04-polling-intervals.t0000664000175000017500000000503315202510242027244 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution time limit works correctly with polling intervals . "$(dirname "$0")/test_header" set_test_number 6 init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = nolimit & limit95M & limit1H & limit10M & limit70S [runtime] [[root]] script = "echo Hello" execution polling intervals = 3*PT30S, PT10M, PT1H [[nolimit]] [[limit95M]] execution time limit = PT95M [[limit1H]] execution time limit = PT1H [[limit10M]] execution time limit = PT10M [[limit70S]] execution time limit = PT70S __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" cylc play --debug "${WORKFLOW_NAME}" poll_grep_workflow_log "INFO - DONE" # NOTE: execution timeout polling is delayed by PT1M to let things settle # PT10M = (3*PT3S + PT9M30S) - PT1M grep_workflow_log_ok grep-limit10M "\[1/limit10M/01:running\] health: execution timeout=None, polling intervals=3\*PT30S,PT9M30S,PT2M,PT7M,..." # PT60M = (3*PT3S + PT10M + PT49M30S) - PT1M grep_workflow_log_ok grep-limit1H "\[1/limit1H/01:running\] health: execution timeout=None, polling intervals=3\*PT30S,PT10M,PT49M30S,PT2M,PT7M,..." # PT70S = (2*PT30S + PT1M10S) - PT1M grep_workflow_log_ok grep-limit70S "\[1/limit70S/01:running\] health: execution timeout=None, polling intervals=2\*PT30S,PT1M10S,PT2M,PT7M,..." # PT95M = (3*PT3S + PT10M + PT1H + PT24M30S) - PT1M grep_workflow_log_ok grep-limit95M "\[1/limit95M/01:running\] health: execution timeout=None, polling intervals=3\*PT30S,PT10M,PT1H,PT24M30S,PT2M,PT7M,..." grep_workflow_log_ok grep-no-limit "\[1/nolimit/01:running\] health: execution timeout=None, polling intervals=3\*PT30S,PT10M,PT1H,..." purge cylc-flow-8.6.4/tests/functional/deprecations/0000775000175000017500000000000015202510242021571 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/deprecations/02-overwrite/0000775000175000017500000000000015202510242024036 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/deprecations/02-overwrite/flow.cylc0000664000175000017500000000047315202510242025665 0ustar alastairalastair# Test automatic deprecation and deletion of config items as specified # in lib/cylc/cfgspec/workflow.py. [scheduling] initial cycle point = 20150808T00 # Deprecated: [[dependencies]] [[[P1D]]] graph = bar => horse # New in Cylc 8: [[graph]] P1D = foo => cat & dog cylc-flow-8.6.4/tests/functional/deprecations/01-cylc8-basic.t0000775000175000017500000000310115202510242024273 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test all current non-silent workflow obsoletions and deprecations. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 2 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate -v "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-cmp" cylc validate "${WORKFLOW_NAME}" 2> 'val.out' cmp_ok val.out "$TEST_SOURCE_DIR/${TEST_NAME_BASE}/validation.stderr" purge cylc-flow-8.6.4/tests/functional/deprecations/test_header0000777000175000017500000000000015202510242030106 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/deprecations/03-suiterc.t0000664000175000017500000000360215202510242023655 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test backwards compatibility for suite.rc files # Includes a test for warning about recurrence format 1 which changed # implementation - https://github.com/cylc/cylc-flow/pull/4554 . "$(dirname "$0")/test_header" set_test_number 3 init_suiterc() { local TEST_NAME="$1" local FLOW_CONFIG="${2:--}" WORKFLOW_NAME="${CYLC_TEST_REG_BASE}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME}" mkdir -p "${TEST_DIR}/${WORKFLOW_NAME}/" cat "${FLOW_CONFIG}" >"${TEST_DIR}/${WORKFLOW_NAME}/suite.rc" cd "${TEST_DIR}/${WORKFLOW_NAME}" || exit } init_suiterc "${TEST_NAME_BASE}" <<'__FLOW__' [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 2000 [[graph]] R2/2000/2001 = foo => bar __FLOW__ MSG=$(python -c 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate . grep_ok "$MSG" "${TEST_NAME}.stderr" grep_ok "The recurrence 'R2/2000/2001' is unlikely to behave the same way as in Cylc 7 " \ "${TEST_NAME}.stderr" cylc-flow-8.6.4/tests/functional/deprecations/01-cylc8-basic/0000775000175000017500000000000015202510242024110 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/deprecations/01-cylc8-basic/flow.cylc0000664000175000017500000000400115202510242025726 0ustar alastairalastair# Test automatic deprecation and deletion of config items as specified # in cylc/flow/cfgspec/workflow.py. [cylc] log resolved dependencies = abort if any task fails = authentication = required run mode = force run mode = task event mail interval = disable automatic shutdown = [[environment]] darmok = [[events]] mail to = mail from = mail smtp = mail footer = timeout = abort on timeout = timeout handler = inactivity = inactivity handler = aborted handler = stalled handler = startup handler = shutdown handler = abort on stalled = abort on inactivity = abort if timeout handler fails = abort if stalled handler fails = abort if inactivity handler fails = abort if startup handler fails = abort if shutdown handler fails = [[reference test]] allow task failures = live mode suite timeout = dummy mode suite timeout = dummy-local mode suite timeout = simulation mode suite timeout = required run mode = suite shutdown event handler = [[simulation]] disable suite event handlers = [[parameters]] [[parameter templates]] [scheduling] max active cycle points = 2 hold after point = initial cycle point = 20150808T00 final cycle point = 20150808T00 [[dependencies]] [[[P1D]]] graph = foo => cat & dog [runtime] [[foo, cat, dog]] extra log files = [[[job]]] shell = fish execution polling intervals = execution retry delays = execution time limit = submission polling intervals = submission retry delays = [[[events]]] mail from = mail to = mail smtp = mail retry delays = [[[suite state polling]]] interval = PT10S message = "pork scratchings" cylc-flow-8.6.4/tests/functional/deprecations/01-cylc8-basic/validation.stderr0000664000175000017500000001171315202510242027472 0ustar alastairalastairWARNING - Obsolete config items were automatically deleted. Please check your workflow and remove them permanently. WARNING - Deprecated config items were automatically upgraded. Please alter your workflow to use the new syntax. WARNING - * (8.0.0) [cylc]force run mode - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][authentication] - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc]log resolved dependencies - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc]required run mode - DELETED (OBSOLETE) WARNING - * (8.0.0) [runtime][foo, cat, dog][events]mail retry delays - DELETED (OBSOLETE) WARNING - * (8.0.0) [runtime][foo, cat, dog]extra log files - DELETED (OBSOLETE) WARNING - * (8.0.0) [runtime][foo, cat, dog][job]shell - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc]abort if any task fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc]disable automatic shutdown - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][environment] - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][reference test] - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][simulation]disable suite event handlers - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][simulation] - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc]task event mail interval -> [cylc][mail]task event batch interval - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][suite state polling] -> [runtime][foo, cat, dog][workflow state polling] - value unchanged WARNING - * (8.0.0) [cylc][parameters] -> [task parameters] - value unchanged WARNING - * (8.0.0) [cylc][parameter templates] -> [task parameters][templates] - value unchanged WARNING - * (8.0.0) [cylc][events]mail to -> [cylc][mail]to - value unchanged WARNING - * (8.0.0) [cylc][events]mail from -> [cylc][mail]from - value unchanged WARNING - * (8.0.0) [cylc][events]mail footer -> [cylc][mail]footer - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][events]mail to -> [runtime][foo, cat, dog][mail]to - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][events]mail from -> [runtime][foo, cat, dog][mail]from - value unchanged WARNING - * (8.0.0) [cylc][events]mail smtp - DELETED (OBSOLETE) - use "global.cylc[scheduler][mail]smtp" instead WARNING - * (8.0.0) [runtime][foo, cat, dog][events]mail smtp - DELETED (OBSOLETE) - use "global.cylc[scheduler][mail]smtp" instead WARNING - * (8.0.0) [scheduling]max active cycle points -> [scheduling]runahead limit - "2" -> "P1" WARNING - * (8.0.0) [scheduling]hold after point -> [scheduling]hold after cycle point - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][job]execution polling intervals -> [runtime][foo, cat, dog]execution polling intervals - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][job]execution retry delays -> [runtime][foo, cat, dog]execution retry delays - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][job]execution time limit -> [runtime][foo, cat, dog]execution time limit - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][job]submission polling intervals -> [runtime][foo, cat, dog]submission polling intervals - value unchanged WARNING - * (8.0.0) [runtime][foo, cat, dog][job]submission retry delays -> [runtime][foo, cat, dog]submission retry delays - value unchanged WARNING - * (8.0.0) [cylc][events]timeout -> [cylc][events]stall timeout - value unchanged WARNING - * (8.0.0) [cylc][events]abort on timeout -> [cylc][events]abort on stall timeout - value unchanged WARNING - * (8.0.0) [cylc][events]inactivity -> [cylc][events]inactivity timeout - value unchanged WARNING - * (8.0.0) [cylc][events]abort on inactivity -> [cylc][events]abort on inactivity timeout - value unchanged WARNING - * (8.0.0) [cylc][events]startup handler -> [cylc][events]startup handlers - value unchanged WARNING - * (8.0.0) [cylc][events]shutdown handler -> [cylc][events]shutdown handlers - value unchanged WARNING - * (8.0.0) [cylc][events]timeout handler -> [cylc][events]stall timeout handlers - value unchanged WARNING - * (8.0.0) [cylc][events]stalled handler -> [cylc][events]stall handlers - value unchanged WARNING - * (8.0.0) [cylc][events]aborted handler -> [cylc][events]abort handlers - value unchanged WARNING - * (8.0.0) [cylc][events]inactivity handler -> [cylc][events]inactivity timeout handlers - value unchanged WARNING - * (8.0.0) [cylc][events]abort on stalled - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][events]abort if startup handler fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][events]abort if shutdown handler fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][events]abort if timeout handler fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][events]abort if inactivity handler fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc][events]abort if stalled handler fails - DELETED (OBSOLETE) WARNING - * (8.0.0) [cylc] -> [scheduler] - value unchanged WARNING - graph items were automatically upgraded in "workflow definition": * (8.0.0) [scheduling][dependencies][X]graph -> [scheduling][graph]X - for X in: P1D cylc-flow-8.6.4/tests/functional/deprecations/02-overwrite.t0000664000175000017500000000264215202510242024227 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that upgraded deprecations don't overwrite existing items . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 1 #------------------------------------------------------------------------------- install_workflow "$TEST_NAME_BASE" "$TEST_NAME_BASE" #------------------------------------------------------------------------------- cylc validate "$WORKFLOW_NAME" 2> val.out grep_ok "UpgradeError" "val.out" #------------------------------------------------------------------------------- purge "$WORKFLOW_NAME" cylc-flow-8.6.4/tests/functional/workflow-host-self-id/0000775000175000017500000000000015202510242023257 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-host-self-id/00-address.t0000664000175000017500000000356415202510242025316 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Ensure that workflow contact env host IP address is defined . "$(dirname "$0")/test_header" set_test_number 2 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" get_local_ip_address() { python3 - "$1" <<'__PYTHON__' import sys from cylc.flow.hostuserutil import get_local_ip_address sys.stdout.write("%s\n" % get_local_ip_address(sys.argv[1])) __PYTHON__ } #------------------------------------------------------------------------------- MY_INET_TARGET=$(cylc config -d '--item=[scheduler][host self-identification]target') MY_HOST_IP="$(get_local_ip_address "${MY_INET_TARGET}")" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" --set="MY_HOST_IP='${MY_HOST_IP}'" create_test_global_config '' ' [scheduler] [[host self-identification]] method = address' workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" \ --set="MY_HOST_IP='${MY_HOST_IP}'" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/functional/workflow-host-self-id/00-address/0000775000175000017500000000000015202510242025121 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/workflow-host-self-id/00-address/reference.log0000664000175000017500000000006715202510242027565 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] cylc-flow-8.6.4/tests/functional/workflow-host-self-id/00-address/flow.cylc0000664000175000017500000000027515202510242026750 0ustar alastairalastair#!Jinja2 [scheduling] [[graph]] R1 = t1 [runtime] [[t1]] script = """ grep -F -q "CYLC_WORKFLOW_HOST={{MY_HOST_IP}}" "${CYLC_WORKFLOW_RUN_DIR}/.service/contact" """ cylc-flow-8.6.4/tests/functional/workflow-host-self-id/test_header0000777000175000017500000000000015202510242031574 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-install/0000775000175000017500000000000015202510242021507 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-install/04-symlinks-not-resolved.t0000775000175000017500000000323015202510242026406 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Check that symlink is not resolved to its target on installation: # When ~/foo -> ~/bar and --symlink-dirs=run=~/bar DO NOT make run=~/foo . "$(dirname "$0")/test_header" set_test_number 3 cat > flow.cylc <<__HEREDOC__ [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1500 [[graph]] R1 = foo __HEREDOC__ run_ok "cylc validate ." # Create a temporary directory to put our symlinked foo and bar in: elsewhere=$(mktemp -d) mkdir -p "${elsewhere}/foo" ln -s "${elsewhere}/foo" "bar" # Install the workflow: run_ok "$TEST_NAME_BASE" cylc install --no-run-name --symlink-dirs=run="${elsewhere}/bar" # Check the installed workflow: ls -l "$RUN_DIR/$(basename "$PWD")" > list grep_ok "bar\/cylc-run" list # Tidy up: cylc clean "$(basename "$PWD")" 2> /dev/null rm -fr "${elsewhere}" exit cylc-flow-8.6.4/tests/functional/cylc-install/06-check-logs-svn.t0000664000175000017500000000426115202510242024745 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test that we only log version control info on workflow. . "$(dirname "$0")/test_header" svn --version || skip_all "svn not installed" set_test_number 3 WORKFLOW="$(date | md5sum | awk '{print $1}')" WORKFLOW_NAME="$(workflow_id "${TEST_NAME_BASE}")" WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}" WORKDIR1="${PWD}/workdir1" # Create a workflow in a subdirectory of the test tmpdir mkdir -p "${WORKDIR1}/${WORKFLOW}" cat > "${WORKDIR1}/${WORKFLOW}/flow.cylc" <<__HEREDOC__ [scheduler] implicit tasks allowed = True [scheduling] initial cycle point = 1649 [[graph]] R1 = foo __HEREDOC__ # Touch some non-functional files: touch "${WORKDIR1}/${WORKFLOW}/test_file_in_workflow" touch "${WORKDIR1}/test_file_outside_workflow" # Create an SVN repo: svnadmin create "myrepo" svn import "${WORKDIR1}" "file:///${PWD}/myrepo/trunk" -m "foo" svn co "file:///${PWD}/myrepo/trunk" "${PWD}/elephant" cd "elephant" || exit # Make changes since commit: echo "Inside workflow" > "${WORKFLOW}/test_file_in_workflow" echo "Outside workflow" > test_file_outside_workflow # Carry out actual test: run_ok "${TEST_NAME_BASE}-install" \ cylc install "./${WORKFLOW}" --no-run-name --workflow-name "${WORKFLOW_NAME}" DIFF_FILE="${WORKFLOW_RUN_DIR}/log/version/uncommitted.diff" grep_ok "Inside workflow" "$DIFF_FILE" grep_fail "Outside workflow" "$DIFF_FILE" purge cylc-flow-8.6.4/tests/functional/cylc-install/00-simple.t0000775000175000017500000001370715202510242023415 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow installation . "$(dirname "$0")/test_header" set_test_number 22 create_test_global_config "" " [install] source dirs = ${PWD}/cylc-src " mkdir "cylc-src" # ----------------------------------------------------------------------------- # Test default name: "cylc install" (flow in $PWD, no args) TEST_NAME="${TEST_NAME_BASE}-basic" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test default name: "cylc install WORKFLOW_NAME" (flow in confgured source dir) make_rnd_workflow # Before adding workflow to ~/cylc-src/, check install fails: TEST_NAME="${TEST_NAME_BASE}-WORKFLOW_NAME-fail-no-src-dir" run_fail "${TEST_NAME}" cylc install "${RND_WORKFLOW_NAME}" # Now add workflow to ~/cylc-src/ and test again RND_WORKFLOW_SOURCE="${PWD}/cylc-src/${RND_WORKFLOW_NAME}" mv "$RND_WORKFLOW_NAME" "${PWD}/cylc-src/" pushd "${RND_WORKFLOW_SOURCE}" || exit 1 TEST_NAME="${TEST_NAME_BASE}-WORKFLOW_NAME-install-ok" run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test cylc install succeeds if suite.rc file in source dir # (also tests installing from absolute path) TEST_NAME="${TEST_NAME_BASE}-suite.rc" make_rnd_workflow rm -f "${RND_WORKFLOW_SOURCE}/flow.cylc" touch "${RND_WORKFLOW_SOURCE}/suite.rc" run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_SOURCE}" --workflow-name="${RND_WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ # Test deprecation message is displayed on installing a suite.rc file MSG=$(python -c 'from cylc.flow.workflow_files import SUITERC_DEPR_MSG; print(SUITERC_DEPR_MSG)') grep_ok "$MSG" "${TEST_NAME}.stderr" purge_rnd_workflow # ----------------------------------------------------------------------------- # Test default path: "cylc install" --no-run-name (flow in $PWD) TEST_NAME="${TEST_NAME_BASE}-pwd-no-run-name" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install --no-run-name contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test "cylc install" flow-name given (flow in $PWD) TEST_NAME="${TEST_NAME_BASE}-flow-name" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}-olaf" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}-olaf/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 rm -rf "${RUN_DIR}/${RND_WORKFLOW_NAME}-olaf" purge_rnd_workflow # ----------------------------------------------------------------------------- # Test "cylc install" flow-name given, no run name (flow in $PWD) TEST_NAME="${TEST_NAME_BASE}-flow-name-no-run-name" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}-olaf" --no-run-name contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}-olaf from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 rm -rf "${RUN_DIR}/${RND_WORKFLOW_NAME}-olaf" purge_rnd_workflow # ----------------------------------------------------------------------------- # Test running cylc install twice increments run dirs correctly TEST_NAME="${TEST_NAME_BASE}-install-twice-1" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ TEST_NAME="${TEST_NAME_BASE}-install-twice-2" run_ok "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run2 from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test running cylc install with multi level name works correctly TEST_NAME="${TEST_NAME_BASE}-install-multi-level" pushd cylc-src || exit 1 make_rnd_workflow SUB_DIR="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" mkdir "${RND_WORKFLOW_NAME}/${SUB_DIR}" mv "${RND_WORKFLOW_NAME}/flow.cylc" "${RND_WORKFLOW_NAME}/${SUB_DIR}" run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_NAME}/${SUB_DIR}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}/${SUB_DIR}/run1 from ${RND_WORKFLOW_SOURCE}/$SUB_DIR __OUT__ TEST_NAME="${TEST_NAME_BASE}-multi-level-from-pwd" pushd "${RND_WORKFLOW_SOURCE}/$SUB_DIR" || exit 1 run_ok "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}/${SUB_DIR}/run2 from ${RND_WORKFLOW_SOURCE}/${SUB_DIR} __OUT__ popd || exit 1 purge_rnd_workflow cylc-flow-8.6.4/tests/functional/cylc-install/02-failures.t0000664000175000017500000002322715202510242023733 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow installation failures . "$(dirname "$0")/test_header" set_test_number 45 create_test_global_config '' ' [install] max depth = 6 ' # Test source directory between runs that are not consistent result in error TEST_NAME="${TEST_NAME_BASE}-forbid-inconsistent-source-dir-between-runs" SOURCE_DIR_1="test-install-${CYLC_TEST_TIME_INIT}/${TEST_NAME_BASE}" WORKFLOW_NAME="cylctb-${CYLC_TEST_TIME_INIT}/${TEST_SOURCE_DIR##*tests/}/${TEST_NAME}" mkdir -p "${PWD}/${SOURCE_DIR_1}" pushd "${SOURCE_DIR_1}" || exit 1 touch flow.cylc run_ok "${TEST_NAME}" cylc install --workflow-name "$WORKFLOW_NAME" popd || exit 1 SOURCE_DIR_2="test-install-${CYLC_TEST_TIME_INIT}2/${TEST_NAME_BASE}" WORKFLOW_NAME="cylctb-${CYLC_TEST_TIME_INIT}/${TEST_SOURCE_DIR##*tests/}/${TEST_NAME}" mkdir -p "${PWD}/${SOURCE_DIR_2}" pushd "${SOURCE_DIR_2}" || exit 1 touch flow.cylc run_fail "${TEST_NAME}" cylc install --workflow-name "$WORKFLOW_NAME" grep_ok "previous installations were from" "${TEST_NAME}.stderr" rm -rf "${PWD:?}/${SOURCE_DIR_1}" "${PWD:?}/${SOURCE_DIR_2}" purge popd || exit # ----------------------------------------------------------------------------- # Test fail no flow.cylc or suite.rc file make_rnd_workflow TEST_NAME="${TEST_NAME_BASE}-no-flow-file" rm -f "${RND_WORKFLOW_SOURCE}/flow.cylc" run_fail "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}" "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: No flow.cylc or suite.rc in ${RND_WORKFLOW_SOURCE} __ERR__ # ----------------------------------------------------------------------------- # Test fail both flow.cylc and suite.rc file make_rnd_workflow TEST_NAME="${TEST_NAME_BASE}-both-suite-and-flow-file" touch "${RND_WORKFLOW_SOURCE}/suite.rc" run_fail "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}" "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: Both flow.cylc and suite.rc files are present in ${RND_WORKFLOW_SOURCE}. \ Please remove one and try again. For more information visit: \ https://cylc.github.io/cylc-doc/stable/html/7-to-8/summary.html#backward-compatibility __ERR__ # Test fail no workflow source dir TEST_NAME="${TEST_NAME_BASE}-nodir" rm -rf "${RND_WORKFLOW_SOURCE}" run_fail "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}" --no-run-name "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: No flow.cylc or suite.rc in ${RND_WORKFLOW_SOURCE} __ERR__ purge_rnd_workflow # ----------------------------------------------------------------------------- # Test cylc install fails when given flow-name that is an absolute path make_rnd_workflow TEST_NAME="${TEST_NAME_BASE}-no-abs-path-flow-name" run_fail "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_SOURCE}" "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: workflow name cannot be an absolute path: ${RND_WORKFLOW_SOURCE} __ERR__ # Test cylc install fails when given forbidden run-name TEST_NAME="${TEST_NAME_BASE}-run-name-forbidden" run_fail "${TEST_NAME}" cylc install --run-name=_cylc-install "${RND_WORKFLOW_SOURCE}" cmp_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: Workflow/run name cannot contain a directory named '_cylc-install' (that filename is reserved) __ERR__ # Test cylc install invalid flow-name TEST_NAME="${TEST_NAME_BASE}-invalid-flow-name" run_fail "${TEST_NAME}" cylc install --workflow-name=".invalid" "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: invalid workflow name '.invalid' - cannot start with: \`.\`, \`-\`, numbers __ERR__ # Test --run-name and --no-run-name options are mutually exclusive TEST_NAME="${TEST_NAME_BASE}--no-run-name-and--run-name-forbidden" pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_fail "${TEST_NAME}" cylc install --run-name="${RND_WORKFLOW_NAME}" --no-run-name cmp_ok "${TEST_NAME}.stderr" <<__ERR__ InputError: options --no-run-name and --run-name are mutually exclusive. __ERR__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test source dir can not contain '_cylc-install, log, share, work' dirs for DIR in 'work' 'share' 'log' '_cylc-install'; do TEST_NAME="${TEST_NAME_BASE}-${DIR}-forbidden-in-source" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 mkdir ${DIR} run_fail "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: ${RND_WORKFLOW_NAME} installation failed - ${DIR} exists in source directory. __ERR__ purge_rnd_workflow popd || exit 1 done # ----------------------------------------------------------------------------- # Test running cylc install twice, first using --run-name, followed by standard run results in error TEST_NAME="${TEST_NAME_BASE}-install-twice-mix-options-1-1st-install" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install --run-name=olaf contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}/olaf from ${RND_WORKFLOW_SOURCE} __OUT__ TEST_NAME="${TEST_NAME_BASE}-install-twice-mix-options-1-2nd-install" run_fail "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: Path: "${RND_WORKFLOW_RUNDIR}" contains an installed workflow. Use --run-name to create a new run. __ERR__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test running cylc install twice, first using standard run, followed by --run-name results in error TEST_NAME="${TEST_NAME_BASE}-install-twice-mix-options-2-1st-install" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ TEST_NAME="${TEST_NAME_BASE}-install-twice-mix-options-2-2nd-install" run_fail "${TEST_NAME}" cylc install --run-name=olaf contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: --run-name option not allowed as '${RND_WORKFLOW_RUNDIR}' contains installed numbered runs. __ERR__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test running cylc install twice, using the same --run-name results in error TEST_NAME="${TEST_NAME_BASE}-install-twice-same-run-name-1st-install" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 run_ok "${TEST_NAME}" cylc install --run-name=olaf contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED ${RND_WORKFLOW_NAME}/olaf from ${RND_WORKFLOW_SOURCE} __OUT__ TEST_NAME="${TEST_NAME_BASE}-install-twice-same-run-name-2nd-install" run_fail "${TEST_NAME}" cylc install --run-name=olaf contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: '${RND_WORKFLOW_RUNDIR}/olaf' already exists __ERR__ popd || exit 1 purge_rnd_workflow # ----------------------------------------------------------------------------- # Test cylc install fails if installation would result in nested run dirs TEST_NAME="${TEST_NAME_BASE}-nested-rundir" make_rnd_workflow mkdir -p "${RND_WORKFLOW_RUNDIR}/.service" run_fail "${TEST_NAME}-install" cylc install "${RND_WORKFLOW_SOURCE}" \ --workflow-name="${RND_WORKFLOW_NAME}/nested" cmp_ok "${TEST_NAME}-install.stderr" <<__ERR__ WorkflowFilesError: Nested run directories not allowed - cannot install workflow in '${RND_WORKFLOW_RUNDIR}/nested/run1' as '${RND_WORKFLOW_RUNDIR}' is already a valid run directory. __ERR__ # Test moving source dir results in error TEST_NAME="${TEST_NAME_BASE}-install-moving-src-dir" make_rnd_workflow run_ok "${TEST_NAME}" cylc install "./${RND_WORKFLOW_NAME}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${PWD}/${RND_WORKFLOW_NAME} __OUT__ rm -rf "${RND_WORKFLOW_SOURCE}" ALT_SOURCE="${TMPDIR}/${USER}/cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)" mkdir -p "${ALT_SOURCE}/${RND_WORKFLOW_NAME}" touch "${ALT_SOURCE}/${RND_WORKFLOW_NAME}/flow.cylc" TEST_NAME="${TEST_NAME_BASE}-install-twice-moving-src-dir-raises-error" run_fail "${TEST_NAME}" cylc install "${ALT_SOURCE}/${RND_WORKFLOW_NAME}" grep_ok "WorkflowFilesError: Symlink broken" "${TEST_NAME}.stderr" rm -rf "${ALT_SOURCE}" purge_rnd_workflow # ----------------------------------------------------------------------------- # --run-name cannot be a path make_rnd_workflow TEST_NAME="${TEST_NAME_BASE}-forbid-cylc-run-dir-install" BASE_NAME="test-install-${CYLC_TEST_TIME_INIT}" mkdir -p "${RUN_DIR}/${BASE_NAME}/${TEST_SOURCE_DIR_BASE}/${TEST_NAME}" && cd "$_" || exit touch flow.cylc run_fail "${TEST_NAME}" cylc install --run-name=foo/bar/baz --workflow-name "$RND_WORKFLOW_NAME" contains_ok "${TEST_NAME}.stderr" <<__ERR__ WorkflowFilesError: Run name cannot be a path. (You used foo/bar/baz) __ERR__ cd "${RUN_DIR}" || exit rm -rf "${BASE_NAME}" purge_rnd_workflow exit cylc-flow-8.6.4/tests/functional/cylc-install/01-symlinks.t0000664000175000017500000001665015202510242023773 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow installation symlinking localhost . "$(dirname "$0")/test_header" if [[ -z ${TMPDIR:-} || -z ${USER:-} || $TMPDIR/$USER == "$HOME" ]]; then skip_all '"TMPDIR" or "USER" not defined or "TMPDIR"/"USER" is "HOME"' fi set_test_number 32 create_test_global_config "" " [install] [[symlink dirs]] [[[localhost]]] run = \$TMPDIR/\$USER/test_cylc_symlink/cylctb_tmp_run_dir share = \$TMPDIR/\$USER/test_cylc_symlink/ log = \$TMPDIR/\$USER/test_cylc_symlink/ log/job = \$TMPDIR/\$USER/test_cylc_symlink/job_log_dir share/cycle = \$TMPDIR/\$USER/test_cylc_symlink/cylctb_tmp_share_dir work = \$TMPDIR/\$USER/test_cylc_symlink/ " # Test "cylc install" ensure symlinks are created TEST_NAME="${TEST_NAME_BASE}-symlinks-created" make_rnd_workflow run_ok "${TEST_NAME}" cylc install --workflow-name="${RND_WORKFLOW_NAME}" "${RND_WORKFLOW_SOURCE}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ WORKFLOW_RUN_DIR="$HOME/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME_BASE}-run-glblcfg" run_ok "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_run_dir/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME_BASE}-share-cycle-glblcfg" run_ok "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/share/cycle")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_share_dir/cylc-run/${RND_WORKFLOW_NAME}/run1/share/cycle" TEST_SYM="${TEST_NAME_BASE}-log-job-glblcfg" run_ok "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/log/job")" \ = "$TMPDIR/${USER}/test_cylc_symlink/job_log_dir/cylc-run/${RND_WORKFLOW_NAME}/run1/log/job" for DIR in 'work' 'share' 'log'; do TEST_SYM="${TEST_NAME_BASE}-${DIR}-glbcfg" run_ok "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/${DIR}")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylc-run/${RND_WORKFLOW_NAME}/run1/${DIR}" done rm -rf "${TMPDIR}/${USER}/test_cylc_symlink/" purge_rnd_workflow # test cli --symlink-dirs overrides the glblcfg SYMDIR=${TMPDIR}/${USER}/test_cylc_cli_symlink/ TEST_NAME="${TEST_NAME_BASE}-cli-opt-install" make_rnd_workflow run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_SOURCE}" \ --workflow-name="${RND_WORKFLOW_NAME}" \ --symlink-dirs="run= ${SYMDIR}cylctb_tmp_run_dir, log=${SYMDIR}, share=${SYMDIR}, \ work = ${SYMDIR}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ WORKFLOW_RUN_DIR="$HOME/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME_BASE}-run-cli" run_ok "$TEST_SYM" test "$(readlink "${WORKFLOW_RUN_DIR}")" \ = "$TMPDIR/${USER}/test_cylc_cli_symlink/cylctb_tmp_run_dir/cylc-run/${RND_WORKFLOW_NAME}/run1" for DIR in 'work' 'share' 'log'; do TEST_SYM="${TEST_NAME_BASE}-${DIR}-cli" run_ok "$TEST_SYM" test "$(readlink "${WORKFLOW_RUN_DIR}/${DIR}")" \ = "${TMPDIR}/${USER}/test_cylc_cli_symlink/cylc-run/${RND_WORKFLOW_NAME}/run1/${DIR}" done INSTALL_LOG="$(find "${WORKFLOW_RUN_DIR}/log/install" -type f -name '*.log')" for DIR in 'work' 'share' 'log'; do grep_ok "${TMPDIR}/${USER}/test_cylc_cli_symlink/cylc-run/${RND_WORKFLOW_NAME}/run1/${DIR}" "${INSTALL_LOG}" done # test cylc play symlinks after cli opts (mapping to different directories) pushd "${WORKFLOW_RUN_DIR}" || exit 1 cat > 'flow.cylc' << __FLOW__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = true __FLOW__ popd || exit 1 run_ok "${TEST_NAME_BASE}-play" cylc play "${RND_WORKFLOW_NAME}/runN" --debug --no-detach # test ensure symlinks, not in cli install are not created from glbl cfg. TEST_SYM="${TEST_NAME_BASE}-share-cycle-cli" run_fail "$TEST_SYM" test "$(readlink "${WORKFLOW_RUN_DIR}/share/cycle")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_share_dir/cylc-run/${RND_WORKFLOW_NAME}/run1/share/cycle" rm -rf "${TMPDIR}/${USER}/test_cylc_cli_symlink/" purge_rnd_workflow # test no symlinks created with --symlink-dirs="" TEST_NAME="${TEST_NAME_BASE}-no-sym-dirs-cli" make_rnd_workflow run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_SOURCE}" \ --workflow-name="${RND_WORKFLOW_NAME}" --symlink-dirs="" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ WORKFLOW_RUN_DIR="$HOME/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME}-run" run_fail "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_run_dir/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME}-share-cycle" run_fail "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/share/cycle")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_share_dir/cylc-run/${RND_WORKFLOW_NAME}/run1/share/cycle" TEST_SYM="${TEST_NAME}-log-job" run_fail "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/log/job")" \ = "$TMPDIR/${USER}/test_cylc_symlink/job_log_dir/cylc-run/${RND_WORKFLOW_NAME}/run1/log/job" for DIR in 'work' 'share' 'log'; do TEST_SYM="${TEST_NAME}-${DIR}" run_fail "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}/${DIR}")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylc-run/${RND_WORKFLOW_NAME}/run1/${DIR}" done pushd "${WORKFLOW_RUN_DIR}" || exit 1 cat > 'flow.cylc' << __FLOW__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = true __FLOW__ popd || exit 1 run_ok "${TEST_NAME_BASE}-play" cylc play "${RND_WORKFLOW_NAME}/runN" --debug --no-detach # test ensure localhost symlink dirs skipped for installed workflows. TEST_SYM="${TEST_NAME_BASE}-installed-workflow-skips-symdirs" run_fail "${TEST_SYM}" test "$(readlink "${WORKFLOW_RUN_DIR}")" \ = "$TMPDIR/${USER}/test_cylc_symlink/cylctb_tmp_run_dir/cylc-run/${RND_WORKFLOW_NAME}/run1" rm -rf "${TMPDIR}/${USER}/test_cylc_cli_symlink/" purge_rnd_workflow # test share and share/cycle same symlinks don't error SYMDIR=${TMPDIR}/${USER}/test_cylc_cli_symlink/ TEST_NAME="${TEST_NAME_BASE}-share-share-cycle-same-dirs" make_rnd_workflow # check install runs without failure run_ok "${TEST_NAME}" cylc install "${RND_WORKFLOW_SOURCE}" \ --workflow-name="${RND_WORKFLOW_NAME}" \ --symlink-dirs="share/cycle=${SYMDIR}, share=${SYMDIR}" contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME/run1 from ${RND_WORKFLOW_SOURCE} __OUT__ WORKFLOW_RUN_DIR="$HOME/cylc-run/${RND_WORKFLOW_NAME}/run1" TEST_SYM="${TEST_NAME_BASE}-share-cli" run_ok "$TEST_SYM" test "$(readlink "${WORKFLOW_RUN_DIR}/share")" \ = "${TMPDIR}/${USER}/test_cylc_cli_symlink/cylc-run/${RND_WORKFLOW_NAME}/run1/share" rm -rf "${TMPDIR}/${USER}/test_cylc_cli_symlink/" purge_rnd_workflow cylc-flow-8.6.4/tests/functional/cylc-install/03-file-transfer.t0000664000175000017500000000667415202510242024672 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test rsync of workflow installation . "$(dirname "$0")/test_header" if ! command -v 'tree' >'/dev/null'; then skip_all '"tree" command not available' fi set_test_number 9 # Need to override any symlink dirs set in global.cylc: create_test_global_config "" " [install] [[symlink dirs]] [[[localhost]]] run = log = work = share = share/cycle = " # Test cylc install copies files to run dir successfully. TEST_NAME="${TEST_NAME_BASE}-basic" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 mkdir .git .svn dir1 dir2 touch .git/file1 .svn/file1 dir1/file1 dir2/file1 file1 file2 run_ok "${TEST_NAME}" cylc install --no-run-name # If rose-cylc plugin is installed add install files to tree. export ROSE_FILES='' tree -a -v -I '*.log|03-file-transfer*' --charset=ascii --noreport "${RND_WORKFLOW_RUNDIR}/" > 'basic-tree.out' cmp_ok 'basic-tree.out' <<__OUT__ ${RND_WORKFLOW_RUNDIR}/ |-- _cylc-install | \`-- source -> ${RND_WORKFLOW_SOURCE} |-- dir1 | \`-- file1 |-- dir2 | \`-- file1 |-- file1 |-- file2 |-- flow.cylc \`-- log \`-- install __OUT__ contains_ok "${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow # Test cylc install copies files to run dir successfully, exluding files from # .cylcignore file. # Should work if we run "cylc install" from source dir or not (see GH #5066) for RUN_IN_SRC_DIR in true false; do TEST_NAME="${TEST_NAME_BASE}-cylcignore-${RUN_IN_SRC_DIR}" make_rnd_workflow pushd "${RND_WORKFLOW_SOURCE}" || exit 1 mkdir .git .svn dir1 dir2 extradir1 extradir2 touch .git/file1 .svn/file1 dir1/file1 dir2/file1 extradir1/file1 extradir2/file1 file1 file2 .cylcignore cat > .cylcignore <<__END__ dir* extradir* file2 __END__ if ${RUN_IN_SRC_DIR}; then run_ok "${TEST_NAME}" cylc install --no-run-name CWD="${PWD}" else DTMP=$(mktemp -d) pushd "${DTMP}" || exit 1 run_ok "${TEST_NAME}" cylc install --no-run-name "${RND_WORKFLOW_SOURCE}" CWD="${PWD}" popd || exit 1 fi OUT="cylc-ignore-tree-${RUN_IN_SRC_DIR}.out" tree -a -v -I '*.log|03-file-transfer*' --charset=ascii --noreport "${RND_WORKFLOW_RUNDIR}/" > "$OUT" cmp_ok "$OUT" <<__OUT__ ${RND_WORKFLOW_RUNDIR}/ |-- _cylc-install | \`-- source -> ${RND_WORKFLOW_SOURCE} |-- file1 |-- flow.cylc \`-- log \`-- install __OUT__ contains_ok "${CWD}/${TEST_NAME}.stdout" <<__OUT__ INSTALLED $RND_WORKFLOW_NAME from ${RND_WORKFLOW_SOURCE} __OUT__ popd || exit 1 purge_rnd_workflow done cylc-flow-8.6.4/tests/functional/cylc-install/07-no-recursive-installations.t0000775000175000017500000000435115202510242027431 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow installation . "$(dirname "$0")/test_header" set_test_number 9 cat > flow.cylc <<__HEREDOC__ [scheduler] allow implicit tasks = true [scheduling] [[graph]] R1 = foo __HEREDOC__ run_ok "$TEST_NAME_BASE" cylc validate "$PWD" TEST_FOLDERS=() MSG="Nested install directories not allowed" TEST_FOLDER=cylctb-$(uuidgen) TEST_FOLDERS+=("$TEST_FOLDER") cylc install "$PWD" --workflow-name "${TEST_FOLDER}/" TEST_NAME="${TEST_NAME_BASE}-child" run_fail "$TEST_NAME" cylc install "$PWD" --workflow-name "${TEST_FOLDER}/child/grandchild" grep_ok "$MSG" "${TEST_NAME}.stderr" TEST_NAME="${TEST_NAME_BASE}-child-no-run-name" run_fail "$TEST_NAME" cylc install "$PWD" --workflow-name "${TEST_FOLDER}/child/grandchild" --no-run-name grep_ok "$MSG" "${TEST_NAME}.stderr" TEST_FOLDER=cylctb-$(uuidgen) TEST_FOLDERS+=("$TEST_FOLDER") cylc install "$PWD" --workflow-name "${TEST_FOLDER}/child/grandchild" TEST_NAME="${TEST_NAME_BASE}-parent" run_fail "$TEST_NAME" cylc install "$PWD" --workflow-name "${TEST_FOLDER}/" grep_ok "$MSG" "${TEST_NAME}.stderr" TEST_NAME="${TEST_NAME_BASE}-parent-no-run-name" run_fail "$TEST_NAME" cylc install "$PWD" --workflow-name "${TEST_FOLDER}/" --no-run-name grep_ok "$MSG" "${TEST_NAME}.stderr" # Cleanup all the test folders added to the array. # shellcheck disable=SC2048 for TEST_FOLDER in ${TEST_FOLDERS[*]}; do rm -fr "${RUN_DIR}/${TEST_FOLDER:-}" done exit cylc-flow-8.6.4/tests/functional/cylc-install/05-check-logs-git.t0000664000175000017500000000412015202510242024713 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test that we only log version control info on workflow source dir # when it is a subdir of a repo. . "$(dirname "$0")/test_header" if ! command -v 'git' > /dev/null; then skip_all 'git not installed' fi # set_test_number 6 set_test_number 3 WORKFLOW="$(date | md5sum | awk '{print $1}')" WORKFLOW_NAME="$(workflow_id "${TEST_NAME_BASE}")" WORKFLOW_RUN_DIR="${RUN_DIR}/${WORKFLOW_NAME}/runN" # Create a workflow in a subdirectory of the test tmpdir mkdir "${WORKFLOW}" cat > "${WORKFLOW}/flow.cylc" <<__HEREDOC__ [scheduler] implicit tasks allowed = True [scheduling] initial cycle point = 1649 [[graph]] R1 = foo __HEREDOC__ # Touch some non-functional files touch "${WORKFLOW}/test_file_in_workflow" touch test_file_outside_workflow # Initialize PWD as a git repo git init . git add . git commit -m "commit 0" # Make changes since commit: echo "Inside workflow" > "${WORKFLOW}/test_file_in_workflow" echo "Outside workflow" > test_file_outside_workflow # Carry out actual test with relpath: run_ok "${TEST_NAME_BASE}-install" \ cylc install "./${WORKFLOW}" --workflow-name "${WORKFLOW_NAME}" DIFF_FILE="${WORKFLOW_RUN_DIR}/log/version/uncommitted.diff" grep_ok "Inside workflow" "$DIFF_FILE" grep_fail "Outside workflow" "$DIFF_FILE" purge cylc-flow-8.6.4/tests/functional/cylc-install/test_header0000777000175000017500000000000015202510242030024 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-list/0000775000175000017500000000000015202510242021014 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-list/00-options0000777000175000017500000000000015202510242024516 2workflow/ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-list/00-options.t0000775000175000017500000001037115202510242023116 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test various uses of the cylc list command . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------ set_test_number 10 #------------------------------------------------------------------------------ install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------ TEST_NAME="${TEST_NAME_BASE}-val" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------ TEST_NAME=${TEST_NAME_BASE}-basic cylc list "${WORKFLOW_NAME}" > list.out cmp_ok list.out << __DONE__ cujo fido manny __DONE__ #------------------------------------------------------------------------------ TEST_NAME=${TEST_NAME_BASE}-opt-a cylc ls -a "${WORKFLOW_NAME}" > list-a.out cmp_ok list-a.out << __DONE__ cujo fido manny not-used __DONE__ #------------------------------------------------------------------------------ TEST_NAME=${TEST_NAME_BASE}-opt-n cylc list -n "${WORKFLOW_NAME}" > list-n.out cmp_ok list-n.out << __DONE__ DOG FICTIONAL MAMMAL POODLE cujo fido manny not-used root __DONE__ #------------------------------------------------------------------------------ TEST_NAME=${TEST_NAME_BASE}-opt-nw cylc ls -nw "${WORKFLOW_NAME}" > list-nw.out cmp_ok list-nw.out << __DONE__ DOG a canid that is known as man's best friend FICTIONAL something made-up MAMMAL a clade of endothermic amniotes POODLE a ridiculous-looking dog owned by idiots cujo a fearsome man-eating poodle fido a large black and white spotted dog manny a large hairy mammoth not-used an unused namespace root __DONE__ #------------------------------------------------------------------------------ TEST_NAME=${TEST_NAME_BASE}-opt-nm cylc list -nm "${WORKFLOW_NAME}" > list-nm.out cmp_ok list-nm.out << __DONE__ DOG DOG MAMMAL root FICTIONAL FICTIONAL root MAMMAL MAMMAL root POODLE POODLE DOG MAMMAL root cujo cujo POODLE DOG MAMMAL FICTIONAL root fido fido DOG MAMMAL root manny manny MAMMAL FICTIONAL root not-used not-used root root root __DONE__ #------------------------------------------------------------------------------ cat > res.out << __DONE__ 20140808T0000Z/cujo 20140808T0000Z/fido 20140808T0000Z/manny 20140809T0000Z/cujo 20140809T0000Z/fido 20140809T0000Z/manny 20140810T0000Z/cujo 20140810T0000Z/fido 20140810T0000Z/manny 20140811T0000Z/cujo 20140811T0000Z/fido 20140811T0000Z/manny 20140812T0000Z/cujo 20140812T0000Z/fido 20140812T0000Z/manny __DONE__ TEST_NAME=${TEST_NAME_BASE}-opt-p1 cylc ls -p 20140808T0000Z,20140812T0000Z "${WORKFLOW_NAME}" > list-p1.out cmp_ok list-p1.out res.out TEST_NAME=${TEST_NAME_BASE}-opt-p2 # default from initial point cylc ls -p ,20140812T0000Z "${WORKFLOW_NAME}" > list-p2.out cmp_ok list-p2.out res.out cat > res2.out << __DONE__ 20140808T0000Z/cujo 20140808T0000Z/fido 20140808T0000Z/manny 20140809T0000Z/cujo 20140809T0000Z/fido 20140809T0000Z/manny 20140810T0000Z/cujo 20140810T0000Z/fido 20140810T0000Z/manny __DONE__ TEST_NAME=${TEST_NAME_BASE}-opt-p3 cylc ls -p 20140808T0000Z, "${WORKFLOW_NAME}" > list-p3.out # default 3 cycle points cmp_ok list-p3.out res2.out TEST_NAME=${TEST_NAME_BASE}-opt-p4 cylc ls -p , "${WORKFLOW_NAME}" > list-p4.out # default 3 cycle points from initial cmp_ok list-p4.out res2.out #------------------------------------------------------------------------------ purge cylc-flow-8.6.4/tests/functional/cylc-list/workflow/0000775000175000017500000000000015202510242022666 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-list/workflow/flow.cylc0000664000175000017500000000173415202510242024516 0ustar alastairalastair[scheduler] UTC mode = True [scheduling] initial cycle point = 20140808T00 [[graph]] P1D = """ fido[-P1D] => fido fido => cujo & manny """ [runtime] [[not-used]] [[[meta]]] title = "an unused namespace" [[MAMMAL]] [[[meta]]] title = "a clade of endothermic amniotes" [[FICTIONAL]] [[[meta]]] title = "something made-up" [[DOG]] inherit = MAMMAL [[[meta]]] title = "a canid that is known as man's best friend" [[POODLE]] inherit = DOG [[[meta]]] title = "a ridiculous-looking dog owned by idiots" [[fido]] inherit = DOG [[[meta]]] title = "a large black and white spotted dog" [[cujo]] inherit = POODLE, FICTIONAL [[[meta]]] title = "a fearsome man-eating poodle" [[manny]] inherit = MAMMAL, FICTIONAL [[[meta]]] title = "a large hairy mammoth" cylc-flow-8.6.4/tests/functional/cylc-list/01-icp.t0000775000175000017500000000250115202510242022173 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test for "cylc list --icp=CYCLE_POINT". . "$(dirname "$0")/test_header" set_test_number 3 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] UTC mode = True [scheduling] [[graph]] R1 = foo => bar [runtime] [[foo, bar]] script = true __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}" cylc list --icp=20200101T0000Z "${WORKFLOW_NAME}" cmp_ok "${TEST_NAME_BASE}.stdout" <<__OUT__ bar foo __OUT__ cmp_ok "${TEST_NAME_BASE}.stderr" <'/dev/null' purge exit cylc-flow-8.6.4/tests/functional/cylc-list/test_header0000777000175000017500000000000015202510242027331 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/0000775000175000017500000000000015202510242022101 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/03-conditional/0000775000175000017500000000000015202510242024624 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/03-conditional/reference.log0000664000175000017500000000026115202510242027264 0ustar alastairalastairInitial point: 1 Final point: 1 1/delay -triggered off [] 1/foo -triggered off [] 1/baz -triggered off ['1/foo'] 1/qux -triggered off ['1/baz'] 1/bar -triggered off ['1/delay'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/03-conditional/flow.cylc0000664000175000017500000000067715202510242026461 0ustar alastairalastair# Check conditional reflow prevention. # foo | bar => baz # baz should not be spawned again if bar runs after baz is gone. [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """foo | bar => baz => qux delay => bar""" [runtime] [[delay]] script = """ # Ensure that bar does not start until baz has gone. cylc__job__poll_grep_workflow_log 'qux.*started' """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/14-trigger-flow-blocker/0000775000175000017500000000000015202510242026352 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/14-trigger-flow-blocker/reference.log0000664000175000017500000000037415202510242031017 0ustar alastairalastairInitial point: 1 Final point: 4 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 2/foo -triggered off [] 2/bar -triggered off ['2/foo'] 3/foo -triggered off [] 3/bar -triggered off ['3/foo'] 4/foo -triggered off [] 4/bar -triggered off ['4/foo'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/14-trigger-flow-blocker/flow.cylc0000664000175000017500000000222615202510242030177 0ustar alastairalastair# This workflow should behave the same as if 3/foo is not force-triggered. # Force-triggering 3/foo to put a running no-flow task in the way of the main flow. # When 2/foo tries to spawn its successor 3/foo, we merge its flow number into # the running 3/foo. Once 3/foo belongs to the flow its successor 4/foo and # child 3/bar must be spawned retroactively. [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 4 runahead limit = P0 [[graph]] P1 = "foo:start => bar" [runtime] [[foo]] script = """ if ((CYLC_TASK_CYCLE_POINT == 1)); then # Force trigger 3/foo while 2/foo is runahead limited. expected="foo, 2, waiting, not-held, not-queued, runahead" diff <(cylc dump -l -t "${CYLC_WORKFLOW_ID}" | grep 'foo, 2') \ <(echo "$expected") cylc trigger --flow=none $CYLC_WORKFLOW_ID//3/foo elif ((CYLC_TASK_CYCLE_POINT == 3)); then # Run until I get merged. cylc__job__poll_grep_workflow_log -E "3/foo.* merged in flow\(s\) 1" fi """ [[bar]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/07-abs-triggers/0000775000175000017500000000000015202510242024716 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/07-abs-triggers/reference.log0000664000175000017500000000040015202510242027351 0ustar alastairalastairInitial point: 1 Final point: 5 2/start -triggered off [] 1/foo -triggered off [] 2/foo -triggered off [] 3/foo -triggered off [] 1/bar -triggered off ['1/foo', '2/start'] 2/bar -triggered off ['2/foo', '2/start'] 3/bar -triggered off ['2/start', '3/foo'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/07-abs-triggers/flow.cylc0000664000175000017500000000122315202510242026537 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT2M [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 5 [[graph]] R1/2 = start P1 = "start[2] & foo => bar" [runtime] [[start]] script = """ # Ensure that 1,2/bar are spawned by 1,2/foo and not by 2/start # (so the scheduler must update their prereqs when 2/start finishes). cylc__job__poll_grep_workflow_log -E "2/bar.* added to the n=0 window" """ [[foo]] [[bar]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-hold-not-spawned.t0000664000175000017500000000170615202510242025674 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test we can hold a task that hasn't spawned yet. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-hold-not-spawned/0000775000175000017500000000000015202510242025503 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/11-hold-not-spawned/reference.log0000664000175000017500000000014115202510242030140 0ustar alastairalastairInitial point: 1 Final point: 1 1/holder -triggered off [] 1/stopper -triggered off ['1/holder'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-hold-not-spawned/flow.cylc0000664000175000017500000000105115202510242027323 0ustar alastairalastair# Test holding a task that hasn't spawned yet. [scheduler] [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = "holder => holdee & stopper" [runtime] [[holder]] script = """ cylc hold "$CYLC_WORKFLOW_ID//1/holdee" """ [[holdee]] script = true [[stopper]] script = """ cylc__job__poll_grep_workflow_log "\[1/holdee.* holding \(as requested earlier\)" -E cylc stop $CYLC_WORKFLOW_ID """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/02-merge/0000775000175000017500000000000015202510242023417 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/02-merge/reference.log0000664000175000017500000000052615202510242026063 0ustar alastairalastairInitial point: 1 Final point: 3 1/foo -triggered off ['0/foo'] 2/foo -triggered off ['1/foo'] 1/bar -triggered off ['1/foo'] 3/foo -triggered off ['2/foo'] 2/bar -triggered off ['2/foo'] 1/foo -triggered off ['0/foo'] 2/foo -triggered off ['1/foo'] 1/bar -triggered off ['1/foo'] 2/bar -triggered off ['2/foo'] 3/bar -triggered off ['3/foo'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/02-merge/flow.cylc0000664000175000017500000000174315202510242025247 0ustar alastairalastair# 3/foo triggers a new flow at 1/foo and waits for it to catch up and merge. # bar checks for the expected flow names at each cycle point. [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 3 [[graph]] P1 = "foo[-P1] => foo => bar" [runtime] [[foo]] script = """ if (( CYLC_TASK_CYCLE_POINT == 3 )); then cylc trigger --flow=new --meta=other "${CYLC_WORKFLOW_ID}//1/foo" cylc__job__poll_grep_workflow_log 'merged in' fi """ [[bar]] script = """ if [[ $CYLC_TASK_JOB == *01 ]]; then # job(01) if (( CYLC_TASK_CYCLE_POINT == 3 )); then test $CYLC_TASK_FLOW_NUMBERS == "1,2" else test $CYLC_TASK_FLOW_NUMBERS == "1" fi else # job(02) test $CYLC_TASK_FLOW_NUMBERS == "2" fi """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/09-set-outputs/0000775000175000017500000000000015202510242024643 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/09-set-outputs/reference.log0000664000175000017500000000034715202510242027310 0ustar alastairalastairInitial point: 1 Final point: 1 1/setter -triggered off [] 1/foo -triggered off [] 1/bar -triggered off [] 1/qux -triggered off ['1/foo'] 1/quw -triggered off ['1/foo'] 1/fux -triggered off ['1/bar'] 1/fuw -triggered off ['1/bar'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/09-set-outputs/flow.cylc0000664000175000017500000000306015202510242026465 0ustar alastairalastair# Test that `cylc set` has the same effect as natural output # completion: i.e. that downstream children are spawned as normal. [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S inactivity timeout = PT30S abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ foo & bar & setter # Task scripting below ensures that foo is still in the pool, but # bar is gone, when its outputs get set - just to make it clear # the target task doesn't have to exist. foo:out1? => qux foo:out2? => quw bar:out1? => fux bar:out2? => fuw """ [runtime] [[foo, bar]] # (Neglecting to complete my outputs naturally). [[[outputs]]] out1 = "file-1 done" out2 = "file-2 done" [[foo]] # Hang about until setter is finished. script = """ cylc__job__poll_grep_workflow_log -E "1/setter.* => succeeded" """ [[bar]] script = true [[setter]] # (To the rescue). script = """ # Set foo outputs while it still exists in the pool. cylc set --flow=2 --output=out1 --output=out2 "${CYLC_WORKFLOW_ID}//1/foo" # Set bar outputs after it is gone from the pool. cylc__job__poll_grep_workflow_log -E "1/bar.* completed" cylc set --flow=2 --output=out1 --output=out2 "${CYLC_WORKFLOW_ID}//1/bar" """ [[qux, quw, fux, fuw]] script = true cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-parents-yes-no.t0000664000175000017500000000216715202510242025404 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # A task with no parents should auto-spawn out to the runahead limit, except # in cycle points where it does have parents. And those cycles, if any, should # not block spawning of subsequent parentless cycles - GitHub #4906. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/01-reflow/0000775000175000017500000000000015202510242023615 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/01-reflow/reference.log0000664000175000017500000000037015202510242026256 0ustar alastairalastairInitial point: 1 Final point: 2 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] 2/foo -triggered off ['1/foo'] 2/triggerer -triggered off ['2/foo'] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/01-reflow/flow.cylc0000664000175000017500000000070515202510242025442 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 runahead limit = P0 [[graph]] R1 = "foo => bar => baz" R1/2/ = "foo[-P1] => foo => triggerer" [runtime] [[triggerer]] script = """ # Cause both 1/bar and 1/baz to run again. cylc trigger --flow=new --meta=cheese "${CYLC_WORKFLOW_ID}//1/bar" """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/15-reflow-incomplete-outputs/0000775000175000017500000000000015202510242027500 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/15-reflow-incomplete-outputs/reference.log0000664000175000017500000000023515202510242032141 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/a -triggered off [] 1/b -triggered off ['1/a'] 1/c -triggered off ['1/b'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/15-reflow-incomplete-outputs/flow.cylc0000664000175000017500000000064315202510242031326 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/b [scheduling] [[graph]] R1 = """ a => b => c """ [runtime] [[b]] script = """ # test $CYLC_TASK_SUBMIT_NUMBER -gt 1 if [[ $CYLC_TASK_SUBMIT_NUMBER -eq 1 ]]; then cylc trigger --flow=new "$CYLC_WORKFLOW_ID//1/a" false fi """ [[a,c]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/08-lost-parents/0000775000175000017500000000000015202510242024761 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/08-lost-parents/reference.log0000664000175000017500000000047015202510242027423 0ustar alastairalastairInitial point: 1 Final point: 6 1/dad -triggered off [] 2/dad -triggered off [] 3/dad -triggered off [] 1/child -triggered off ['1/dad'] 2/child -triggered off ['2/dad'] 3/child -triggered off ['3/dad'] 5/mum -triggered off [] 4/child -triggered off [] 5/child -triggered off ['5/mum'] 6/child -triggered off [] cylc-flow-8.6.4/tests/functional/spawn-on-demand/08-lost-parents/flow.cylc0000664000175000017500000000063015202510242026603 0ustar alastairalastair# A task with parents in some cycles but not others must be spawned on demand # by parents (when it has them) and auto-spawned otherwise. [scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 6 [[graph]] R3//P1 = "dad => child" R1/5/P1 = "mum => child" P1 = "child" [runtime] [[root]] script = true cylc-flow-8.6.4/tests/functional/spawn-on-demand/17-c7backcompat-self-suicide-cycling.t0000664000175000017500000000176315202510242031060 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Cylc 8 back-compat mode. # Test self-induced suicide (cycling workflow, absolute triggers). . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/15-stop-flow-3.t0000664000175000017500000000203015202510242024576 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that stopping the only flow causes the scheduler to shut down cleanly, # even if there is an active-waiting task present. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-c7backcompat-self-suicide.t0000664000175000017500000000171515202510242027426 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Cylc 8 back-compat mode. # Test self-induced suicide. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/14-trigger-flow-blocker.t0000664000175000017500000000201115202510242026531 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check correct behaviour if a no-flow task is manually triggered just ahead of # the main flow. See GitHub #4645 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-parents-yes-no/0000775000175000017500000000000015202510242025211 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/16-parents-yes-no/reference.log0000664000175000017500000000064415202510242027656 0ustar alastairalastairInitial point: 1 Final point: 4 1/failer -triggered off [] in flow 1 1/foo -triggered off [] in flow 1 3/foo -triggered off [] in flow 1 1/bar -triggered off ['1/foo'] in flow 1 3/bar -triggered off ['3/foo'] in flow 1 2/baz -triggered off ['1/failer'] in flow 1 2/foo -triggered off ['2/baz'] in flow 1 4/foo -triggered off [] in flow 1 4/bar -triggered off ['4/foo'] in flow 1 2/bar -triggered off ['2/foo'] in flow 1 cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-parents-yes-no/flow.cylc0000664000175000017500000000211215202510242027030 0ustar alastairalastair# A task with no parents should auto-spawn out to the runahead limit, except # in cycle points where it does have parents. And those cycles, if any, should # not block spawning of subsequent parentless cycles - GitHub #4906. [scheduler] [[events]] inactivity timeout = PT60S abort on inactivity timeout = True expected task failures = 1/failer # Un-stall, to see if foo gets spawned by baz at point 2. # (Note we use to remove failer and trigger baz here, but # now removing a task causes removal of waiting children). stall handlers = cylc set %(workflow)s//1/failer [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 4 runahead limit = P2 # 1, 2, 3 [[graph]] R1 = failer # cause a runahead limit stall R1/2 = "failer[^] => baz => foo" # foo has no parents, so should spawn to the runahead limit... P1 = "foo => bar" # ...except at point 2, where it should be spawned by baz [runtime] [[failer]] script = false [[foo, bar, baz]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/02-merge.t0000664000175000017500000000337315202510242023612 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that flows merge correctly. . "$(dirname "$0")/test_header" install_workflow "${TEST_NAME_BASE}" set_test_number 4 TEST_NAME="${TEST_NAME_BASE}"-validate run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}"-run workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" # check the DB as well sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT name, cycle, flow_nums FROM task_states \ WHERE submit_num is 1 order by cycle" \ > flow-one.db cmp_ok flow-one.db - << __OUT__ foo|1|[1] bar|1|[1] foo|2|[1] bar|2|[1] foo|3|[1] foo|3|[1, 2] bar|3|[1, 2] __OUT__ sqlite3 ~/cylc-run/"${WORKFLOW_NAME}"/log/db \ "SELECT name, cycle, flow_nums FROM task_states \ WHERE submit_num is 2 order by cycle" \ > flow-two.db cmp_ok flow-two.db - << __OUT__ foo|1|[2] bar|1|[2] foo|2|[2] bar|2|[2] __OUT__ purge exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/10-retrigger/0000775000175000017500000000000015202510242024311 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/10-retrigger/reference.log0000664000175000017500000000027515202510242026756 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/oops -triggered off ['1/foo'] 1/triggerer -triggered off ['1/foo'] 1/oops -triggered off ['1/foo'] 1/bar -triggered off ['1/oops'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/10-retrigger/flow.cylc0000664000175000017500000000100415202510242026127 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/oops [scheduling] [[graph]] R1 = """ foo => oops & triggerer oops => bar """ [runtime] [[oops]] script = """ if (( CYLC_TASK_SUBMIT_NUMBER == 1 )); then false else true fi """ [[triggerer]] script = """ cylc__job__poll_grep_workflow_log -E '1/oops/01.* failed' cylc trigger "${CYLC_WORKFLOW_ID}//1/oops" """ [[foo, bar]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/00-no-reflow/0000775000175000017500000000000015202510242024226 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/00-no-reflow/reference.log0000664000175000017500000000043615202510242026672 0ustar alastairalastairInitial point: 1 Final point: 2 1/foo -triggered off ['0/foo'] 1/bar -triggered off ['1/foo'] 2/foo -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] 2/bar -triggered off ['2/foo'] 2/triggerer -triggered off ['2/foo'] 1/bar -triggered off ['1/foo'] 2/baz -triggered off ['2/bar'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/00-no-reflow/flow.cylc0000664000175000017500000000061215202510242026050 0ustar alastairalastair[scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 [[graph]] P1 = "foo[-P1] => foo => bar => baz" R1/2/ = "foo => triggerer" [runtime] [[triggerer]] script = """ # Cause only 1/bar to run again. cylc trigger "${CYLC_WORKFLOW_ID}//1/bar" """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-abs-suicide/0000775000175000017500000000000015202510242024510 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/11-abs-suicide/reference.log0000664000175000017500000000051615202510242027153 0ustar alastairalastairInitial point: 1 Final point: 5 1/a -triggered off [] 1/b -triggered off [] 2/b -triggered off [] 3/b -triggered off [] 4/b -triggered off [] 5/b -triggered off [] 1/x -triggered off ['1/a', '1/b'] 4/x -triggered off ['1/a', '4/b'] 2/x -triggered off ['1/a', '2/b'] 3/x -triggered off ['1/a', '3/b'] 5/x -triggered off ['1/a', '5/b'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-abs-suicide/flow.cylc0000664000175000017500000000153415202510242026336 0ustar alastairalastair# This workflow spawns instances of c (as partially satsified prerequisites) # out to the runahead limit before the suicide trigger gets activated. If the # suicide trigger cleans up all c instances the scheduler can shut down # cleanly. Otherwise it will abort on stall with unsatisfied prerequisites. [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True expected task failures = 1/a [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 5 runahead limit = P4 [[graph]] R1 = "a?" P1 = """ a[^]? => c & !x a[^]:fail? => x & !c b => c & x """ [runtime] [[b,c,x]] [[a]] # Fail after c is spawned out to the runahead limit. script = """ cylc__job__poll_grep_workflow_log "spawned 5/c" false """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/04-branch.t0000664000175000017500000000170115202510242023743 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check branching without suicide triggers. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/06-stop-flow-2.t0000664000175000017500000000173715202510242024612 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that other flows can be stopped without affecting the main flow. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/04-branch/0000775000175000017500000000000015202510242023557 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/04-branch/reference.log0000664000175000017500000000017115202510242026217 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/fish -triggered off ['1/foo'] 1/done -triggered off ['1/fish'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/04-branch/flow.cylc0000664000175000017500000000075415202510242025410 0ustar alastairalastair# Check SOD branching without suicide triggers. # Scheduler should shut down normally even though one branch does not run. [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = """foo:out1? => fish foo:out2? => fowl fish | fowl => done""" [runtime] [[foo]] script = "cylc message 'the quick brown fox'" [[[outputs]]] out1 = "the quick brown fox" out2 = "jumped over the lazy dog" cylc-flow-8.6.4/tests/functional/spawn-on-demand/18-submitted/0000775000175000017500000000000015202510242024327 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/18-submitted/reference.log0000664000175000017500000000056715202510242027000 0ustar alastairalastair7/a -triggered off [] in flow 1 6/a6 -triggered off [] in flow 1 8/a -triggered off [] in flow 1 3/a3 -triggered off [] in flow 1 2/a2 -triggered off [] in flow 1 4/a4 -triggered off [] in flow 1 1/a1 -triggered off [] in flow 1 5/a5 -triggered off [] in flow 1 5/f -triggered off ['5/a5'] in flow 1 8/s -triggered off ['8/a'] in flow 1 6/b -triggered off ['6/a6'] in flow 1 cylc-flow-8.6.4/tests/functional/spawn-on-demand/18-submitted/flow.cylc0000664000175000017500000000351415202510242026155 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] # shut down once the workflow has stalled # abort on stall timeout = True # stall timeout = PT0S stall handlers = cylc stop %(workflow)s expected task failures = 1/a1, 2/a2, 3/a3 [scheduling] initial cycle point = 1 cycling mode = integer runahead limit = P10 [[graph]] # tasks will finish with *in*complete outputs R/1 = """ # a1 should be incomplete (submission is implicitly required) a1? => b """ R/2 = """ # a2 should be incomplete (submission is implicitly required) a2:finished => b """ R/3 = """ # a3 should be incomplete (submission is explicitly required) a3? => b a3:submitted => s """ # tasks will finish with complete outputs R/4 = """ # a4 should be complete (submission is explicitly optional) a4? => b a4:submitted? => s """ R/5 = """ # a5 should be complete (submission is explicitly optional) a5? => b a5:submitted? => s a5:submit-failed? => f # branch should run """ R/6 = """ # a6 should be complete (submission is explicitly optional) a6? => b a6:submit-failed? => f # branch should run """ R/7 = """ # a7 should be complete (submission is explicitly optional) a:submit-failed? => f # branch should run """ R/8 = """ # a8 should be complete (submission is explicitly optional) a:submitted? => s # branch should run """ [runtime] [[a1, a2, a3, a4, a5]] # a task which will always submit-fail platform = broken cylc-flow-8.6.4/tests/functional/spawn-on-demand/18-submitted.t0000664000175000017500000000307715202510242024523 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test the submitted and submit-failed triggers work correctly. # # The :submitted output should be considered required unless explicitly stated # otherwise. # See: # * https://github.com/cylc/cylc-flow/pull/5755 # * https://github.com/cylc/cylc-admin/blob/master/docs/proposal-new-output-syntax.md#output-syntax . "$(dirname "$0")/test_header" set_test_number 5 # define a broken platform which will always result in submission failures create_test_global_config '' ' [platforms] [[broken]] hosts = no-such-host ' install_and_validate reftest_run for number in 1 2 3; do grep_workflow_log_ok \ "${TEST_NAME_BASE}-a${number}" \ "${number}/a${number}.* did not complete the required outputs:" done purge exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/05-stop-flow/0000775000175000017500000000000015202510242024255 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/05-stop-flow/reference.log0000664000175000017500000000012715202510242026716 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/05-stop-flow/flow.cylc0000664000175000017500000000066615202510242026110 0ustar alastairalastair# Check that stopping the only flow causes the workflow to shut down without # spawning more tasks. # Here bar stops the flow, so baz should never run. [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = "foo => bar => baz" [runtime] [[bar]] script = """ cylc stop --flow=1 ${CYLC_WORKFLOW_ID} cylc__job__poll_grep_workflow_log 'Command "stop" actioned' """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/13-trigger-runahead/0000775000175000017500000000000015202510242025552 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/13-trigger-runahead/reference.log0000664000175000017500000000015015202510242030207 0ustar alastairalastairInitial point: 1 Final point: 3 1/foo -triggered off [] 2/foo -triggered off [] 3/foo -triggered off [] cylc-flow-8.6.4/tests/functional/spawn-on-demand/13-trigger-runahead/flow.cylc0000664000175000017500000000127115202510242027376 0ustar alastairalastair# This workflow should behave the same as if foo.1 did not trigger foo.2. [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 3 runahead limit = P0 [[graph]] P1 = foo [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started if ((CYLC_TASK_CYCLE_POINT == 1)); then expected="foo, 1, running, not-held, not-queued, not-runahead foo, 2, waiting, not-held, not-queued, runahead" diff <(cylc dump -l -t "${CYLC_WORKFLOW_ID}") <(echo "$expected") # Force trigger next instance while it is runahead limited. cylc trigger $CYLC_WORKFLOW_ID//2/foo fi """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/09-set-outputs.t0000664000175000017500000000171415202510242025033 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that "cylc set" works like it says on the tin. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-c7backcompat-self-suicide/0000775000175000017500000000000015202510242027235 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/16-c7backcompat-self-suicide/suite.rc0000664000175000017500000000072715202510242030722 0ustar alastairalastair# suite.rc: Cylc 8 back-compat mode. # GitHub cylc-flow #4968: self-induced suicide should not retrigger foo below. [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True expected task failures = 1/foo [scheduling] [[dependencies]] graph = """ foo => bar foo:fail => !foo & !bar foo:fail | bar => baz """ [runtime] [[foo]] script = false [[bar, baz]] cylc-flow-8.6.4/tests/functional/spawn-on-demand/16-c7backcompat-self-suicide/reference.log0000664000175000017500000000011315202510242031671 0ustar alastairalastair1/foo -triggered off [] in flow 1 1/baz -triggered off ['1/foo'] in flow 1 cylc-flow-8.6.4/tests/functional/spawn-on-demand/00-no-reflow.t0000664000175000017500000000172315202510242024416 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that triggering does not start a new flow by default. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/17-c7backcompat-self-suicide-cycling/0000775000175000017500000000000015202510242030664 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/17-c7backcompat-self-suicide-cycling/suite.rc0000664000175000017500000000126115202510242032343 0ustar alastairalastair# suite.rc: Cylc 8 back compat mode. # GitHub cylc-flow #4968: self-induced suicide in the example below should not # cause shutdown after the initial cycle point. [scheduler] [[events]] stall timeout = PT0S abort on stall timeout = True expected task failures = 1/bad, 2/bad [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = 2 [[dependencies]] [[[R1]]] graph = init [[[P1]]] graph = """ init[^] => bad => good bad:fail => !bad & !good """ [runtime] [[init, good]] script = true [[bad]] script = false cylc-flow-8.6.4/tests/functional/spawn-on-demand/17-c7backcompat-self-suicide-cycling/reference.log0000664000175000017500000000016715202510242033331 0ustar alastairalastair1/init -triggered off [] in flow 1 1/bad -triggered off ['1/init'] in flow 1 2/bad -triggered off ['1/init'] in flow 1 cylc-flow-8.6.4/tests/functional/spawn-on-demand/08-lost-parents.t0000664000175000017500000000172015202510242025146 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check a task gets auto-spawned after losing its parents. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/07-abs-triggers.t0000664000175000017500000000270515202510242025107 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that absolute triggers get satisfied even if spawned by other tasks, # and that they are remembered and used after a restart. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play \ "${WORKFLOW_NAME}" \ --reference-test \ --no-detach \ --stopcp=3 \ --debug # Restart will hang if abs triggers not remembered. workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play "${WORKFLOW_NAME}" --no-detach purge exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/10-retrigger.t0000664000175000017500000000174515202510242024505 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that an incomplete failed task can be retriggered to carry on the flow. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/19-submitted-compat.t0000664000175000017500000000354215202510242026002 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test the submitted and submit-failed triggers work correctly in back-compat # mode. See https://github.com/cylc/cylc-flow/issues/5771 . "$(dirname "$0")/test_header" set_test_number 4 init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = """ a b """ [runtime] [[a]] # should complete [[b]] # should not complete platform = broken __FLOW__ mv "$WORKFLOW_RUN_DIR/flow.cylc" "$WORKFLOW_RUN_DIR/suite.rc" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --no-detach grep_workflow_log_ok \ "${TEST_NAME_BASE}-back-compat" \ 'Backward compatibility mode ON' grep_workflow_log_ok \ "${TEST_NAME_BASE}-a-complete" \ '\[1/a/01:running\] => succeeded' grep_workflow_log_ok \ "${TEST_NAME_BASE}-b-incomplete" \ '1/b did not complete the required outputs:\n.*\n.*submitted.*\n.*succeeded\n' \ -Pizoq purge cylc-flow-8.6.4/tests/functional/spawn-on-demand/test_header0000777000175000017500000000000015202510242030416 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/15-stop-flow-3/0000775000175000017500000000000015202510242024416 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/15-stop-flow-3/reference.log0000664000175000017500000000006615202510242027061 0ustar alastairalastairInitial point: 1 Final point: 1 1/c -triggered off [] cylc-flow-8.6.4/tests/functional/spawn-on-demand/15-stop-flow-3/flow.cylc0000664000175000017500000000136015202510242026241 0ustar alastairalastair# This should shut down cleanly without running "never" or "never_again". # The scheduler must remove flow_num 1 from task "c" to prevent spawning of # "never_again" AND remove the n=0 active-waiting task "never" from the pool # because it hasn't become active yet (otherwise the inactivity timer will # abort the run because the xtrigger never succeeds). [scheduler] allow implicit tasks = True [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[xtriggers]] x = xrandom(0, 10) # never succeeds [[graph]] R1 = """ @x => never c => never_again """ [runtime] [[c]] script = "cylc stop --flow=1 $CYLC_WORKFLOW_ID" cylc-flow-8.6.4/tests/functional/spawn-on-demand/06-stop-flow-2/0000775000175000017500000000000015202510242024415 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/06-stop-flow-2/reference.log0000664000175000017500000000031415202510242027054 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] 1/qux -triggered off ['1/baz'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/06-stop-flow-2/flow.cylc0000664000175000017500000000154615202510242026246 0ustar alastairalastair# Check that a specified flow can be stopped without affecting the main flow. # Here baz triggers a new flow then waits for the second baz to finish. # Meanwhile the second bar stops its own flow. So order events should be: # 1. foo => bar => baz (flow a) # 2. foo => bar (flow b) # 3. qux (flow a) [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = "foo => bar => baz => qux" [runtime] [[bar]] script = """ if (( CYLC_TASK_SUBMIT_NUMBER == 2 )); then cylc stop --flow=1 ${CYLC_WORKFLOW_ID} cylc__job__poll_grep_workflow_log 'Command "stop" actioned' fi """ [[baz]] script = """ if (( CYLC_TASK_SUBMIT_NUMBER == 1 )); then cylc trigger --flow=new --meta=other "${CYLC_WORKFLOW_ID}//1/foo" cylc__job__poll_grep_workflow_log -E "1/bar/02\(flows=2\):running.* => succeeded" fi """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/01-reflow.t0000664000175000017500000000171415202510242024005 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that triggering with --flow starts a new flow. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/12-set-outputs-cont-flow/0000775000175000017500000000000015202510242026543 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/spawn-on-demand/12-set-outputs-cont-flow/reference.log0000664000175000017500000000023015202510242031177 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/setter -triggered off ['1/foo'] 1/bar -triggered off ['1/foo'] 1/baz -triggered off ['1/bar'] cylc-flow-8.6.4/tests/functional/spawn-on-demand/12-set-outputs-cont-flow/flow.cylc0000664000175000017500000000122015202510242030361 0ustar alastairalastair# Test that `cylc set` continues the active flow by default # Task "setter" should cause bar to run, then subsequently baz. [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S inactivity timeout = PT30S abort on inactivity timeout = True expected task failures = 1/foo [scheduling] [[graph]] R1 = """ foo:fail? => setter foo? => bar => baz """ [runtime] [[foo]] script = false [[bar, baz]] script = true [[setter]] script = """ cylc set --output=succeeded "${CYLC_WORKFLOW_ID}//1/foo" """ cylc-flow-8.6.4/tests/functional/spawn-on-demand/15-reflow-incomplete-outputs.t0000664000175000017500000000201115202510242027657 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check correct behaviour if a no-flow task is manually triggered just ahead of # the main flow. See GitHub #4645 . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/03-conditional.t0000664000175000017500000000167415202510242025021 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check conditional reflow prevention. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/12-set-outputs-cont-flow.t0000664000175000017500000000171215202510242026731 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that "cylc set" continues a flow by default. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/11-abs-suicide.t0000775000175000017500000000206115202510242024677 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that absolute suicide triggers clean up unsatisfied prerequisites. # See explanatory comments in the workflow config. . "$(dirname "$0")/test_header" set_test_number 2 export REFTEST_OPTS='--debug' reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/13-trigger-runahead.t0000664000175000017500000000201015202510242025730 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check correct behaviour if a parentless task is manually triggered whilst # runahead-limited. See GitHub #4619. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/spawn-on-demand/05-stop-flow.t0000664000175000017500000000173515202510242024450 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Check that stopping the only flow causes the scheduler to shut down. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/clock-trigger-inexact/0000775000175000017500000000000015202510242023276 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-trigger-inexact/00-big-offset.t0000664000175000017500000000311315202510242025723 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clock triggers (xtrigger and old-style) with a large inexact offset. . "$(dirname "$0")/test_header" skip_all 'TODO: fix test https://github.com/cylc/cylc-flow/issues/4633' set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" cylc cat-log "${WORKFLOW_NAME}" > log START_HOUR=$(grep 'Workflow:' log | cut -c 1-13) START_MINU=$(grep 'Workflow:' log | cut -c 15-16) TRIGG_MINU=$(( 10#${START_MINU} + 1)) [[ $START_MINU == 0* ]] && TRIGG_MINU=0${TRIGG_MINU} for NAME in foo bar baz; do grep_ok "${START_HOUR}:${TRIGG_MINU}.* INFO - \[.*/${NAME} .*\] => waiting$" log done purge cylc-flow-8.6.4/tests/functional/clock-trigger-inexact/test_header0000777000175000017500000000000015202510242031613 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-trigger-inexact/00-big-offset/0000775000175000017500000000000015202510242025540 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/clock-trigger-inexact/00-big-offset/flow.cylc0000664000175000017500000000215115202510242027362 0ustar alastairalastair#!Jinja2 # Test clock-trigger offset involving inexact intervals (months and years), # which requires adding the offset to the cycle point before conversion to # absolute seconds. Initial cycle point is next minute minus P1Y7M, with # opposite clock-offset to get real time triggering once per minute. {% set OFFSET = "P1Y7M" %} [scheduler] [[events]] # May take up to 60 secs to finish, allow some extra time. inactivity timeout = PT80S abort on inactivity timeout = True [scheduling] initial cycle point = next(T--00) - {{OFFSET}} # next minute - P1Y7M final cycle point = +P0Y [[xtriggers]] clock1 = wall_clock({{OFFSET}}) # xtrigger with arg clock2 = wall_clock(offset={{OFFSET}}) # xtrigger with kwarg [[special tasks]] clock-trigger = baz({{OFFSET}}) # old-style clock-triggered task [[graph]] PT1M = """ # These should all trigger at once, at the first minute boundary # after start-up @clock1 => foo @clock2 => bar baz """ [runtime] [[foo, bar, baz]] cylc-flow-8.6.4/tests/functional/cylc-kill/0000775000175000017500000000000015202510242020774 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/00-multi-hosts-compat.t0000775000175000017500000000344515202510242025160 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test kill multiple jobs on localhost and a remote host export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" RUN_DIR="$RUN_DIR/${WORKFLOW_NAME}" LOG="${RUN_DIR}/log/scheduler/log" sed -n 's/^.*\(cylc jobs-kill\)/\1/p' "${LOG}" | sort -u >'edited-workflow-log' sort >'edited-workflow-log-ref' <<__LOG__ cylc jobs-kill --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/remote-1/01 1/remote-2/01 cylc jobs-kill --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/local-1/01 1/local-2/01 1/local-3/01 __LOG__ cmp_ok 'edited-workflow-log' 'edited-workflow-log-ref' purge exit cylc-flow-8.6.4/tests/functional/cylc-kill/04-handlers/0000775000175000017500000000000015202510242023015 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/04-handlers/reference.log0000664000175000017500000000023615202510242025457 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/killer -triggered off ['1/a', '1/b'] 1/end -triggered off ['1/a', '1/b', '1/c'] cylc-flow-8.6.4/tests/functional/cylc-kill/04-handlers/flow.cylc0000664000175000017500000000177115202510242024646 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] expected task failures = 1/a, 1/b, 1/c stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = """ a:started => killer a:failed => end b:submitted? => killer b:submit-failed? => end c:submit-failed? => end c:submitted? => nope """ [runtime] [[a, b, c]] [[[events]]] failed handlers = echo %(id)s submission failed handlers = echo %(id)s [[a]] script = sleep 40 [[b]] platform = old_street [[c]] platform = $(sleep 40; echo localhost) [[killer]] script = """ cylc kill "$CYLC_WORKFLOW_ID//1/a" "$CYLC_WORKFLOW_ID//1/b" cylc__job__poll_grep_workflow_log -E '1\/c.* => preparing' cylc kill "$CYLC_WORKFLOW_ID//1/c" """ [[end]] script = cylc stop "$CYLC_WORKFLOW_ID" --now --now cylc-flow-8.6.4/tests/functional/cylc-kill/00-multi-hosts-compat/0000775000175000017500000000000015202510242024762 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/00-multi-hosts-compat/reference.log0000664000175000017500000000053015202510242027421 0ustar alastairalastairInitial point: 1 Final point: 1 1/remote-1 -triggered off [] 1/local-1 -triggered off ['1/remote-1', '1/remote-2'] 1/local-2 -triggered off ['1/remote-1', '1/remote-2'] 1/remote-2 -triggered off [] 1/local-3 -triggered off ['1/remote-1', '1/remote-2'] 1/killer -triggered off ['1/local-1', '1/local-2', '1/local-3', '1/remote-1', '1/remote-2'] cylc-flow-8.6.4/tests/functional/cylc-kill/00-multi-hosts-compat/flow.cylc0000664000175000017500000000145615202510242026613 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [[events]] expected task failures = 1/local-1, 1/local-2, 1/local-3, 1/remote-1, 1/remote-2 [scheduling] [[graph]] R1 = """ # wait for the remote tasks to start before triggering the # local ones in order to factor out remote-init time remote-1:start & remote-2:start => local-1 & local-2 & local-3 KILLABLE:start-all => killer """ [runtime] [[KILLABLE]] script = sleep 60 [[local-1, local-2, local-3]] inherit = KILLABLE [[remote-1, remote-2]] inherit = KILLABLE platform = {{CYLC_TEST_PLATFORM}} [[killer]] script = """ cylc kill "${CYLC_WORKFLOW_ID}//1/KILLABLE" cylc stop "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/cylc-kill/02-submitted/0000775000175000017500000000000015202510242023213 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/02-submitted/reference.log0000664000175000017500000000042015202510242025650 0ustar alastairalastairInitial point: 1 Final point: 1 1/killable-1 -triggered off [] 1/killable-2 -triggered off [] 1/killable-3 -triggered off [] 1/killer -triggered off ['1/killable-1', '1/killable-2', '1/killable-3'] 1/stopper -triggered off ['1/killable-1', '1/killable-2', '1/killable-3'] cylc-flow-8.6.4/tests/functional/cylc-kill/02-submitted/flow.cylc0000664000175000017500000000200715202510242025035 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [[events]] expected task failures = 1/killable-1, 1/killable-2, 1/killable-3 [scheduling] [[graph]] R1 = """ KILLABLE:submit-all? => killer KILLABLE:submit-fail-all? => stopper """ [runtime] [[KILLABLE]] init-script=""" echo "CYLC_JOB_PID=$$" >>"$0.status" sleep 60 """ script=true [[killable-1, killable-2, killable-3]] inherit=KILLABLE [[killer]] script=""" cylc__job__wait_cylc_message_started cylc__job__poll_grep_workflow_log -F '1/killable-1 -triggered' cylc__job__poll_grep_workflow_log -F '1/killable-2 -triggered' cylc__job__poll_grep_workflow_log -F '1/killable-3 -triggered' # (Avoid killing myself if my started message hasn't arrived yet:) cylc kill "${CYLC_WORKFLOW_ID}//*/killable*:submitted" """ [[stopper]] script=cylc stop "${CYLC_WORKFLOW_ID}" cylc-flow-8.6.4/tests/functional/cylc-kill/01-multi-hosts/0000775000175000017500000000000015202510242023502 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/01-multi-hosts/reference.log0000664000175000017500000000053015202510242026141 0ustar alastairalastairInitial point: 1 Final point: 1 1/remote-1 -triggered off [] 1/local-1 -triggered off ['1/remote-1', '1/remote-2'] 1/local-2 -triggered off ['1/remote-1', '1/remote-2'] 1/remote-2 -triggered off [] 1/local-3 -triggered off ['1/remote-1', '1/remote-2'] 1/killer -triggered off ['1/local-1', '1/local-2', '1/local-3', '1/remote-1', '1/remote-2'] cylc-flow-8.6.4/tests/functional/cylc-kill/01-multi-hosts/flow.cylc0000664000175000017500000000145715202510242025334 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [[events]] expected task failures = 1/local-1, 1/local-2, 1/local-3, 1/remote-1, 1/remote-2 [scheduling] [[graph]] R1 = """ # wait for the remote tasks to start before triggering the # local ones in order to factor out remote-init time remote-1:start & remote-2:start => local-1 & local-2 & local-3 KILLABLE:start-all => killer """ [runtime] [[KILLABLE]] script = sleep 60 [[local-1, local-2, local-3]] inherit = KILLABLE [[remote-1, remote-2]] inherit = KILLABLE platform = {{ CYLC_TEST_PLATFORM }} [[killer]] script = """ cylc kill "${CYLC_WORKFLOW_ID}//1/KILLABLE" cylc stop "${CYLC_WORKFLOW_ID}" """ cylc-flow-8.6.4/tests/functional/cylc-kill/01-multi-hosts.t0000775000175000017500000000344615202510242023701 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test kill multiple jobs on localhost and a remote host export REQUIRE_PLATFORM='loc:remote comms:tcp' . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" \ -s "CYLC_TEST_PLATFORM='${CYLC_TEST_PLATFORM}'" RUN_DIR="$RUN_DIR/${WORKFLOW_NAME}" LOG="${RUN_DIR}/log/scheduler/log" sed -n 's/^.*\(cylc jobs-kill\)/\1/p' "${LOG}" | sort -u >'edited-workflow-log' sort >'edited-workflow-log-ref' <<__LOG__ cylc jobs-kill --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/remote-1/01 1/remote-2/01 cylc jobs-kill --debug -- '\$HOME/cylc-run/${WORKFLOW_NAME}/log/job' 1/local-1/01 1/local-2/01 1/local-3/01 __LOG__ cmp_ok 'edited-workflow-log' 'edited-workflow-log-ref' purge exit cylc-flow-8.6.4/tests/functional/cylc-kill/04-handlers.t0000664000175000017500000000314315202510242023203 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test event handlers when killing running/submitted/preparing tasks. # Any downstream tasks that depend on the `:submit-fail`/`:fail` outputs # SHOULD run. # Handlers for the `submission failed`/`failed` events SHOULD run. export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 5 # Create platform that ensures job will be in submitted state for long enough create_test_global_config '' " [platforms] [[old_street]] job runner = at job runner command template = at now + 5 minutes hosts = localhost install target = localhost " install_and_validate reftest_run grep_workflow_log_ok "grep-a" "[(('event-handler-00', 'failed'), 1) out] 1/a" -F for task in b c; do grep_workflow_log_ok "grep-${task}" \ "[(('event-handler-00', 'submission failed'), 1) out] 1/${task}" -F done purge cylc-flow-8.6.4/tests/functional/cylc-kill/test_header0000777000175000017500000000000015202510242027311 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/05-retries.t0000664000175000017500000000245515202510242023066 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test retries when killing running/submitted/preparing tasks. # As killing tasks puts them in the held state, retries should NOT go ahead. export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 2 # Create platform that ensures job will be in submitted state for long enough create_test_global_config '' " [platforms] [[old_street]] job runner = at job runner command template = at now + 5 minutes hosts = localhost install target = localhost " install_and_validate reftest_run purge cylc-flow-8.6.4/tests/functional/cylc-kill/02-submitted.t0000775000175000017500000000166115202510242023407 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test kill running jobs only . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/functional/cylc-kill/05-retries/0000775000175000017500000000000015202510242022673 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-kill/05-retries/reference.log0000664000175000017500000000022515202510242025333 0ustar alastairalastairInitial point: 1 Final point: 1 1/a -triggered off [] 1/b -triggered off [] 1/killer -triggered off ['1/a', '1/b'] 1/end -triggered off ['1/killer'] cylc-flow-8.6.4/tests/functional/cylc-kill/05-retries/flow.cylc0000664000175000017500000000214115202510242024514 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] expected task failures = 1/a, 1/b, 1/c stall timeout = PT0S abort on stall timeout = True [scheduling] [[graph]] R1 = """ a:started & b:submitted? => killer => end # Should not get these outputs: a:failed? | b:submit-failed? | c:submit-failed? => nope """ [runtime] [[a]] script = sleep 40 execution retry delays = PT0S [[b]] platform = old_street submission retry delays = PT0S [[c]] platform = $(sleep 40; echo localhost) submission retry delays = PT0S [[killer]] script = """ cylc kill "$CYLC_WORKFLOW_ID//1/a" "$CYLC_WORKFLOW_ID//1/b" cylc__job__poll_grep_workflow_log -E '1\/c.* => preparing' cylc kill "$CYLC_WORKFLOW_ID//1/c" """ [[end]] script = """ for task in a b c; do cylc__job__poll_grep_workflow_log -E "1\/${task}.* retrying" done cylc stop "$CYLC_WORKFLOW_ID" --now --now """ cylc-flow-8.6.4/tests/functional/cylc-show/0000775000175000017500000000000015202510242021021 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-show/05-complex.t0000664000175000017500000000676215202510242023112 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc show for a basic task. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_SHOW_OUTPUT_PATH="$PWD/${TEST_NAME_BASE}-show.stdout" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate \ --set="TEST_OUTPUT_PATH='$TEST_SHOW_OUTPUT_PATH'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" run_ok "${TEST_NAME}" cylc play \ --no-detach --set="TEST_OUTPUT_PATH='$TEST_SHOW_OUTPUT_PATH'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-show" contains_ok "${TEST_SHOW_OUTPUT_PATH}" << '__OUT__' title: (not given) description: (not given) URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 1 & 2 & (3 | (4 & 5)) & 0 ✓ 0 = 19991231T0000Z/f succeeded ✓ 1 = 20000101T0000Z/a succeeded ✓ 2 = 20000101T0000Z/b succeeded ✓ 3 = 20000101T0000Z/c succeeded ✓ 4 = 20000101T0000Z/d succeeded ✓ 5 = 20000101T0000Z/e succeeded outputs: ('⨯': not completed) ⨯ 20000101T0000Z/f expired ✓ 20000101T0000Z/f submitted ⨯ 20000101T0000Z/f submit-failed ✓ 20000101T0000Z/f started ⨯ 20000101T0000Z/f succeeded ⨯ 20000101T0000Z/f failed output completion: incomplete ⨯ ┆ succeeded 19991231T0000Z/f succeeded 20000101T0000Z/a succeeded 20000101T0000Z/b succeeded 20000101T0000Z/c succeeded 20000101T0000Z/d succeeded 20000101T0000Z/e succeeded title: (not given) description: (not given) URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 1 & 2 & (3 | (4 & 5)) & 0 ✓ 0 = 20000101T0000Z/f succeeded ✓ 1 = 20000102T0000Z/a succeeded ✓ 2 = 20000102T0000Z/b succeeded ✓ 3 = 20000102T0000Z/c succeeded ✓ 4 = 20000102T0000Z/d succeeded ✓ 5 = 20000102T0000Z/e succeeded outputs: ('⨯': not completed) ⨯ 20000102T0000Z/f expired ✓ 20000102T0000Z/f submitted ⨯ 20000102T0000Z/f submit-failed ✓ 20000102T0000Z/f started ⨯ 20000102T0000Z/f succeeded ⨯ 20000102T0000Z/f failed output completion: incomplete ⨯ ┆ succeeded 20000101T0000Z/f succeeded 20000102T0000Z/a succeeded 20000102T0000Z/b succeeded 20000102T0000Z/c succeeded 20000102T0000Z/d succeeded 20000102T0000Z/e succeeded __OUT__ #------------------------------------------------------------------------------- purge cylc-flow-8.6.4/tests/functional/cylc-show/05-complex/0000775000175000017500000000000015202510242022712 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-show/05-complex/flow.cylc0000664000175000017500000000107415202510242024537 0ustar alastairalastair#!Jinja2 [scheduler] UTC mode = True [scheduling] initial cycle point = 2000 final cycle point = +P1D runahead limit = P1 [[graph]] T00 = a & b & (c | (d & e)) & f[-P1D] => f [runtime] [[root]] script = true [[a, b, c, d, e]] [[f]] script = """ # show myself. sleep 4 cylc show "${CYLC_WORKFLOW_ID}//${CYLC_TASK_CYCLE_POINT}/f" >>{{ TEST_OUTPUT_PATH }} cylc show --list-prereqs "${CYLC_WORKFLOW_ID}//${CYLC_TASK_CYCLE_POINT}/f" >>{{ TEST_OUTPUT_PATH }} """ cylc-flow-8.6.4/tests/functional/cylc-show/06-past-present-future.t0000664000175000017500000000410315202510242025364 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc-show prerequisites for past, future, and present tasks. . "$(dirname "$0")/test_header" set_test_number 6 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" # First run: task c shuts the scheduler down then fails. TEST_NAME="${TEST_NAME_BASE}-run-1" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" # Restart: task kick-c triggers off of c's failure, and re-triggers c. # Then, c runs `cylc show` on tasks b, c, and d. TEST_NAME="${TEST_NAME_BASE}-run-2" workflow_run_ok "${TEST_NAME}" cylc play --no-detach --debug "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-show.past" contains_ok "$WORKFLOW_RUN_DIR/show-b.txt" <<__END__ state: succeeded prerequisites: (n/a for past tasks) __END__ # Note trigger command satisfies off-flow prerequisites. TEST_NAME="${TEST_NAME_BASE}-show.present" contains_ok "${WORKFLOW_RUN_DIR}/show-c.txt" <<__END__ state: running prerequisites: ('⨯': not satisfied) ✓ 1/b succeeded __END__ TEST_NAME="${TEST_NAME_BASE}-show.future" contains_ok "${WORKFLOW_RUN_DIR}/show-d.txt" <<__END__ state: waiting prerequisites: ('⨯': not satisfied) ⨯ 1/c succeeded __END__ purge cylc-flow-8.6.4/tests/functional/cylc-show/test_header0000777000175000017500000000000015202510242027336 2../lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-show/06-past-present-future/0000775000175000017500000000000015202510242025201 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-show/06-past-present-future/flow.cylc0000664000175000017500000000166715202510242027036 0ustar alastairalastair[scheduler] allow implicit tasks = True [[events]] inactivity timeout = PT1M abort on inactivity timeout = True [scheduling] [[graph]] R1 = """ a => b => c? => d c:fail? => kick-c """ [runtime] # First run: c stops the scheduler then fails. # On restart, kick-c retriggers c to run 'cylc show'. [[kick-c]] script = cylc trigger "$CYLC_WORKFLOW_ID//1/c" [[c]] script = """ if ((CYLC_TASK_SUBMIT_NUMBER == 1)); then cylc stop --now --max-polls=10 --interval=1 $CYLC_WORKFLOW_ID false else # Allow time for c submission => running sleep 2 cylc show "$CYLC_WORKFLOW_ID//1/b" >> $CYLC_WORKFLOW_RUN_DIR/show-b.txt cylc show "$CYLC_WORKFLOW_ID//1/c" >> $CYLC_WORKFLOW_RUN_DIR/show-c.txt cylc show "$CYLC_WORKFLOW_ID//1/d" >> $CYLC_WORKFLOW_RUN_DIR/show-d.txt fi """ cylc-flow-8.6.4/tests/functional/cylc-show/clock-triggered-non-utc-mode/0000775000175000017500000000000015202510242026371 5ustar alastairalastaircylc-flow-8.6.4/tests/functional/cylc-show/clock-triggered-non-utc-mode/reference-untz.log0000664000175000017500000000056315202510242032034 0ustar alastairalastairRun mode: live Initial point: 20140808T0900$TZ_OFFSET_BASIC Final point: 20140808T0900$TZ_OFFSET_BASIC Cold Start 20140808T0900$TZ_OFFSET_BASIC 20140808T0900$TZ_OFFSET_BASIC/woo -triggered off [] 20140808T0900$TZ_OFFSET_BASIC/foo -triggered off ['20140808T0900$TZ_OFFSET_BASIC/woo'] 20140808T0900$TZ_OFFSET_BASIC/show -triggered off ['20140808T0900$TZ_OFFSET_BASIC/woo'] cylc-flow-8.6.4/tests/functional/cylc-show/clock-triggered-non-utc-mode/flow.cylc0000664000175000017500000000074615202510242030223 0ustar alastairalastair#!jinja2 [scheduler] cycle point time zone = {{ TZ_OFFSET_BASIC }} [scheduling] initial cycle point = 20140808T09 final cycle point = 20140808T09 [[special tasks]] clock-trigger = foo(PT5M) [[graph]] PT1H = "woo => foo & show" [runtime] [[woo]] script = true [[foo]] script = sleep 10 [[show]] script = """ sleep 4 cylc show "$CYLC_WORKFLOW_ID//20140808T0900{{ TZ_OFFSET_BASIC }}/foo" >{{ TEST_SHOW_OUTPUT_PATH }} """ cylc-flow-8.6.4/tests/k0000777000175000017500000000000015202510242020312 2flakyfunctional/ustar alastairalastaircylc-flow-8.6.4/tests/u0000777000175000017500000000000015202510242016112 2unit/ustar alastairalastaircylc-flow-8.6.4/tests/f0000777000175000017500000000000015202510242017256 2functional/ustar alastairalastaircylc-flow-8.6.4/tests/unit/0000775000175000017500000000000015202510242015726 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/test_prerequisite.py0000664000175000017500000002217015202510242022062 0ustar alastairalastair# 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 . from functools import partial from typing import Optional import pytest from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.loader import ISO8601_CYCLING_TYPE, get_point from cylc.flow.exceptions import TriggerExpressionError from cylc.flow.id import Tokens, detokenise from cylc.flow.prerequisite import Prerequisite, SatisfiedState from cylc.flow.run_modes import RunMode detok = partial(detokenise, selectors=True, relative=True) @pytest.fixture def prereq(set_cycling_type): set_cycling_type(ISO8601_CYCLING_TYPE, "Z") prereq = Prerequisite(get_point('2000')) prereq[(1999, 'a', 'succeeded')] = True prereq[(2000, 'b', 'succeeded')] = False prereq[(2000, 'c', 'succeeded')] = False prereq[(2001, 'd', 'custom')] = False return prereq def test_satisfied(prereq: Prerequisite): assert prereq._satisfied == { # the pre-initial dependency should be marked as satisfied ('1999', 'a', 'succeeded'): 'satisfied naturally', # all others should not ('2000', 'b', 'succeeded'): False, ('2000', 'c', 'succeeded'): False, ('2001', 'd', 'custom'): False, } # No cached satisfaction state yet: assert prereq._cached_satisfied is None # Calling self.is_satisfied() should cache the result: assert not prereq.is_satisfied() assert prereq._cached_satisfied is False # mark two prerequisites as satisfied prereq.satisfy_me([ Tokens('2000/b:succeeded', relative=True), Tokens('2000/c:succeeded', relative=True), ]) assert prereq._satisfied == { # the pre-initial dependency should be marked as satisfied ('1999', 'a', 'succeeded'): 'satisfied naturally', # the two newly-satisfied dependency should be satisfied ('2000', 'b', 'succeeded'): 'satisfied naturally', ('2000', 'c', 'succeeded'): 'satisfied naturally', # the remaining dependency should not ('2001', 'd', 'custom'): False, } # Should have reset cached satisfaction state: assert prereq._cached_satisfied is None assert not prereq.is_satisfied() # mark all prereqs as satisfied prereq.set_satisfied() assert prereq._satisfied == { # the pre-initial dependency should be marked as satisfied ('1999', 'a', 'succeeded'): 'satisfied naturally', # the two newly-satisfied dependency should be satisfied ('2000', 'b', 'succeeded'): 'satisfied naturally', ('2000', 'c', 'succeeded'): 'satisfied naturally', # the remaining dependency should be marked as forse-satisfied ('2001', 'd', 'custom'): 'force satisfied', } # Should have set cached satisfaction state as must be true now: assert prereq._cached_satisfied is True assert prereq.is_satisfied() def test_getitem_setitem(prereq: Prerequisite): msg = ('2000', 'b', 'succeeded') # __getitem__: assert prereq[msg] is False # __setitem__: prereq[msg] = True assert prereq[msg] == 'satisfied naturally' prereq[msg] = 'force satisfied' assert prereq[msg] == 'force satisfied' # coercion of cycle point assert prereq[(2000, 'b', 'succeeded')] == 'force satisfied' assert prereq[(get_point('2000'), 'b', 'succeeded')] == 'force satisfied' def test_iter(prereq: Prerequisite): assert list(prereq) == [ ('1999', 'a', 'succeeded'), ('2000', 'b', 'succeeded'), ('2000', 'c', 'succeeded'), ('2001', 'd', 'custom'), ] assert [p.task for p in prereq] == ['a', 'b', 'c', 'd'] def test_items(prereq: Prerequisite): assert list(prereq.items()) == [ (('1999', 'a', 'succeeded'), 'satisfied naturally'), (('2000', 'b', 'succeeded'), False), (('2000', 'c', 'succeeded'), False), (('2001', 'd', 'custom'), False), ] def test_set_conditional_expr(prereq: Prerequisite): assert not prereq.is_satisfied() prereq.set_conditional_expr('1999/a succeeded | 2000/b succeeded') assert prereq.is_satisfied() def test_iter_target_point_strings(prereq): assert set(prereq.iter_target_point_strings()) == { '1999', '2000', '2001', } def test_get_target_points(prereq): assert set(prereq.get_target_points()) == { get_point('1999'), get_point('2000'), get_point('2001'), } @pytest.fixture def satisfied_states_prereq(): """Fixture for testing the full range of possible satisfied states.""" prereq = Prerequisite(IntegerPoint('2')) prereq[('1', 'a', 'x')] = True prereq[('1', 'b', 'x')] = False prereq[('1', 'c', 'x')] = 'satisfied from database' prereq[('1', 'd', 'x')] = 'force satisfied' prereq[('1', 'e', 'x')] = 'satisfied by skip mode' return prereq def test_unset_naturally_satisfied(satisfied_states_prereq: Prerequisite): satisfied_states_prereq[('1', 'a', 'y')] = True satisfied_states_prereq[('1', 'a', 'z')] = 'force satisfied' for id_, expected in [ ('1/a', True), ('1/b', False), ('1/c', True), ('1/d', False), ('1/e', True), ]: assert ( satisfied_states_prereq.unset_naturally_satisfied(id_) == expected ) assert satisfied_states_prereq._satisfied == { ('1', 'a', 'x'): False, ('1', 'a', 'y'): False, ('1', 'a', 'z'): 'force satisfied', ('1', 'b', 'x'): False, ('1', 'c', 'x'): False, ('1', 'd', 'x'): 'force satisfied', ('1', 'e', 'x'): False, } def test_set_satisfied(satisfied_states_prereq: Prerequisite): satisfied_states_prereq.set_satisfied() assert satisfied_states_prereq._satisfied == { ('1', 'a', 'x'): 'satisfied naturally', ('1', 'b', 'x'): 'force satisfied', ('1', 'c', 'x'): 'satisfied from database', ('1', 'd', 'x'): 'force satisfied', ('1', 'e', 'x'): 'satisfied by skip mode', } def test_satisfy_me(): prereq = Prerequisite(IntegerPoint('2')) for task_name in ('a', 'b', 'c'): prereq[('1', task_name, 'x')] = False assert not prereq.is_satisfied() assert prereq._cached_satisfied is False prereq.satisfy_me( [Tokens('//1/a:x'), Tokens('//1/d:x'), Tokens('//1/c:y')], ) assert prereq._satisfied == { ('1', 'a', 'x'): 'satisfied naturally', ('1', 'b', 'x'): False, ('1', 'c', 'x'): False, } # should have reset cached satisfaction state assert prereq._cached_satisfied is None prereq.satisfy_me( [Tokens('//1/a:x'), Tokens('//1/b:x')], forced=True, ) assert prereq._satisfied == { # 1/a:x unaffected as already satisfied ('1', 'a', 'x'): 'satisfied naturally', ('1', 'b', 'x'): 'force satisfied', ('1', 'c', 'x'): False, } @pytest.mark.parametrize('forced, mode, expected', [ (False, None, 'satisfied naturally'), (True, None, 'force satisfied'), (True, RunMode.SKIP, 'force satisfied'), (False, RunMode.SKIP, 'satisfied by skip mode'), ]) def test_satisfy_me__override_false( forced: bool, mode: Optional[RunMode], expected: SatisfiedState, ): """Test satisfying an unsatisfied prereq with different states.""" prereq = Prerequisite(IntegerPoint('2')) prereq[('1', 'a', 'x')] = False prereq.satisfy_me([Tokens('//1/a:x')], forced=forced, mode=mode) assert prereq[('1', 'a', 'x')] == expected @pytest.mark.parametrize('mode', [None, RunMode.SKIP]) @pytest.mark.parametrize('forced', [True, False]) @pytest.mark.parametrize('existing', [ 'satisfied from database', 'force satisfied', 'satisfied naturally', ]) def test_satisfy_me__override_truthy( existing: SatisfiedState, forced: bool, mode: Optional[RunMode], ): """Test that satisfying an already-satisfied prereq doesn't change it.""" prereq = Prerequisite(IntegerPoint('2')) prereq[('1', 'a', 'x')] = existing prereq.satisfy_me([Tokens('//1/a:x')], forced=forced, mode=mode) assert prereq[('1', 'a', 'x')] == existing @pytest.mark.parametrize( 'expr, err', ( ('int("df")', 'invalid literal'), ('7 +', 'invalid syntax'), )) def test__eval_satisfied_raises(expr, err, monkeypatch): monkeypatch.setattr( 'cylc.flow.prerequisite.Prerequisite.get_raw_conditional_expression', lambda _: expr ) prereq = Prerequisite(IntegerPoint('1')) prereq.conditional_expression = expr with pytest.raises(TriggerExpressionError, match=err): prereq._eval_satisfied() cylc-flow-8.6.4/tests/unit/main_loop/0000775000175000017500000000000015202510242017703 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/main_loop/test_log_memory.py0000664000175000017500000000537715202510242023501 0ustar alastairalastair# 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 . from pathlib import Path from unittest.mock import Mock import pytest try: from cylc.flow.main_loop.log_memory import ( _compute_sizes, _transpose, _dump, _plot ) except ModuleNotFoundError as exc: if exc.name == 'pympler': pytest.skip( 'pympler required for these tests', allow_module_level=True ) else: raise def test_compute_sizes(): """Test the interface for the calculation of instance attribute sizes.""" keys = { 'a': [], 'b': 42, 'c': 'beef wellington' } test_object = Mock(**keys) # no fields should be larger than 10kb sizes = _compute_sizes(test_object, 10000) sizes.pop('total') assert sizes == {} # all fields should be larger than 0kb ret = _compute_sizes(test_object, 0) assert { key for key, value in ret.items() # filter out mock fields if not key.startswith('_') and key not in ('method_calls', 'total') } == set(keys) @pytest.fixture() def test_data(): return [ (5, {'a': 1, 'b': 2, 'c': 3}), (6, {'a': 2, 'c': 4}), (7, {'a': 5, 'c': 2}) ] def test_transpose(test_data): """Test transposing the data from bin to series orientated.""" assert _transpose(test_data) == ( { # the keys are sorted by their last entry 'a': [1, 2, 5], 'c': [3, 4, 2], 'b': [2, -1, -1] # missing values become -1 }, [0, 1, 2] ) def test_dump(test_data, tmp_path): """Ensure the data is serialiseable.""" _dump(test_data, tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_memory.json') ] def test_plot(test_data, tmp_path): """Ensure the plotting mechanism doesn't raise errors.""" fields, times = _transpose(test_data) _plot(fields, times, tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_memory.pdf') ] cylc-flow-8.6.4/tests/unit/main_loop/test_main_loop.py0000664000175000017500000001650515202510242023300 0ustar alastairalastair# 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 . import asyncio from collections import deque import logging import pytest from cylc.flow import CYLC_LOG from cylc.flow.exceptions import CylcError from cylc.flow.main_loop import ( CoroTypes, MainLoopPluginException, _wrapper, get_runners, load, ) from cylc.flow.main_loop.health_check import health_check as hc_during def test_load_plugins_blank(): """Test that log_plugins works when no plugins are requested.""" conf = { 'plugins': [] } assert load(conf) == { 'config': conf, 'state': {}, 'timings': {} } def test_load_plugins(): """Test the loading of a built-in plugin.""" conf = { 'plugins': ['health check'], 'health check': { 'interval': 1234 } } assert load(conf) == { CoroTypes.Periodic: { ('health check', 'health_check'): hc_during }, 'state': { 'health check': { } }, 'config': conf, 'timings': { ('health check', 'health_check'): deque([], maxlen=1) } } def test_wrapper_calls_function(): """Ensure the wrapper calls coroutines.""" flag = False async def test_coro(arg1, arg2): nonlocal flag assert arg1 == 'arg1' assert arg2 == 'arg2' flag = True coro = _wrapper( test_coro, 'arg1', 'arg2' ) asyncio.run(coro) assert flag def test_wrapper_logging(caplog): """Ensure the wrapper logs each coroutine call.""" async def test_coro(*_): pass coro = _wrapper( test_coro, None, None ) with caplog.at_level(logging.DEBUG, logger=CYLC_LOG): asyncio.run(coro) assert len(caplog.record_tuples) == 2 ( (run_log, run_level, run_msg), (end_log, end_level, end_msg) ) = caplog.record_tuples # we should have two messages, one sent before and one after # the function assert 'run' in run_msg assert 'end' in end_msg # both should contain the name of the function assert 'test_coro' in run_msg assert 'test_coro' in end_msg # and should be sent to the cylc logger at the debug level assert run_log == end_log == CYLC_LOG assert run_level == end_level == logging.DEBUG def test_wrapper_catches_exceptions(caplog): """Ensure the wrapper catches Exception instances and logs them.""" async def test_coro(*_): raise Exception('foo') coro = _wrapper( test_coro, None, None ) with caplog.at_level(logging.DEBUG, logger=CYLC_LOG): asyncio.run(coro) assert len(caplog.record_tuples) == 4 run, error, traceback, completed = caplog.record_tuples assert 'run' in run[2] assert error[1] == logging.ERROR assert traceback[1] == logging.ERROR assert 'foo' in traceback[2] assert completed[1] == logging.DEBUG def test_wrapper_passes_cylc_error(): """Ensure the wrapper does not catch CylcError instances.""" async def test_coro(*_): raise CylcError('foo') coro = _wrapper( test_coro, None, None ) with pytest.raises(MainLoopPluginException): asyncio.run(coro) @pytest.fixture def basic_plugins(): calls = [] def capture(*args): calls.append(args) plugins = { 'config': { 'periodic plugin': { 'interval': 10 } }, 'timings': { ('periodic plugin', 'periodic_coro'): [], ('startup plugin', 'startup_coro'): [], }, 'state': { 'periodic plugin': { 'a': 1 }, 'startup plugin': { 'b': 2 } }, CoroTypes.Periodic: { ('periodic plugin', 'periodic_coro'): capture }, CoroTypes.StartUp: { ('startup plugin', 'startup_coro'): capture } } return (plugins, calls, capture) def test_get_runners_startup(basic_plugins): """IT should return runners for startup functions.""" plugins, calls, capture = basic_plugins runners = get_runners( plugins, CoroTypes.StartUp, 'scheduler object' ) assert len(runners) == 1 asyncio.run(runners[0]) assert calls == [('scheduler object', {'b': 2})] def test_get_runners_periodic(basic_plugins): """It should return runners for periodic functions.""" plugins, calls, capture = basic_plugins runners = get_runners( plugins, CoroTypes.Periodic, 'scheduler object' ) assert len(runners) == 1 asyncio.run(runners[0]) assert calls == [('scheduler object', {'a': 1})] def test_get_runners_periodic_debounce(basic_plugins): """It should run periodic functions based on the configured interval.""" plugins, calls, capture = basic_plugins # we should start with a blank timings object assert len(plugins['timings'][('periodic plugin', 'periodic_coro')]) == 0 runners = get_runners( plugins, CoroTypes.Periodic, 'scheduler object' ) assert len(runners) == 1 asyncio.run(runners[0]) assert calls == [('scheduler object', {'a': 1})] # the timings object should now contain the previous run assert len(plugins['timings'][('periodic plugin', 'periodic_coro')]) == 1 # the next run should be skipped because of the interval runners = get_runners( plugins, CoroTypes.Periodic, 'scheduler object' ) assert len(runners) == 0 # if we remove the interval the next run will not get skipped plugins['config']['periodic plugin']['interval'] = 0 runners = get_runners( plugins, CoroTypes.Periodic, 'scheduler object' ) assert len(runners) == 1 assert calls[-1] == ('scheduler object', {'a': 1}) # Clean up coroutines we didn't run for coro in runners: coro.close() def test_state(basic_plugins): """It should pass the same state object with each function call. * Run the same plugin function twice. * Ensure that the state object recieved by each call is the same object. """ plugins, calls, capture = basic_plugins runners = get_runners( plugins, CoroTypes.StartUp, 'scheduler object' ) assert len(runners) == 1 asyncio.run(*runners) assert len(calls) == 1 runners = get_runners( plugins, CoroTypes.StartUp, 'scheduler object' ) assert len(runners) == 1 asyncio.run(*runners) assert len(calls) == 2 (_, state1), (_, state2) = calls assert id(state1) == id(state2) cylc-flow-8.6.4/tests/unit/main_loop/test_log_db.py0000664000175000017500000000254215202510242022545 0ustar alastairalastair# 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 . from textwrap import dedent from cylc.flow.main_loop.log_db import _format def test_format(): """It should indent, fix case and strip comments in SQL statements.""" assert _format(''' select a, b, c, d, e, f, g from table_1 left join table_2 where a = 1 and b = 2 and c = 3 # whatever ''') == dedent(''' SELECT a, b, c, d, e, f, g FROM table_1 LEFT JOIN table_2 WHERE a = 1 AND b = 2 AND c = 3 '''[1:]) cylc-flow-8.6.4/tests/unit/main_loop/test_health_check.py0000664000175000017500000000412415202510242023717 0ustar alastairalastair# 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 . from unittest.mock import Mock import pytest from cylc.flow.exceptions import CylcError from cylc.flow.main_loop.health_check import ( _check_workflow_run_dir, _check_contact_file ) def test_check_workflow_run_dir(): """Ensure a missing workflow run dir raises an CylcError.""" sched = Mock(workflow_run_dir='/a/b/c/d/e') with pytest.raises(CylcError): _check_workflow_run_dir(sched) def test_check_contact_file_data(monkeypatch): """Ensure differing contact file data raises CylcError.""" contact_data = { 'a': 'beef', 'b': 2 } sched = Mock( workflow='foo', contact_data=dict(contact_data) ) monkeypatch.setattr( 'cylc.flow.main_loop.health_check.workflow_files.load_contact_file', lambda x: dict(contact_data) ) # pass _check_contact_file(sched) # fail contact_data['a'] = 'wellington' with pytest.raises(CylcError): _check_contact_file(sched) def test_check_contact_file_io(monkeypatch): """Ensure IOError retrieving the contact file raises CylcError.""" sched = Mock(workflow='foo') def whoopsie(*_): raise IOError('') monkeypatch.setattr( 'cylc.flow.main_loop.health_check.workflow_files.load_contact_file', whoopsie ) with pytest.raises(CylcError): _check_contact_file(sched) cylc-flow-8.6.4/tests/unit/main_loop/test_auto_restart.py0000664000175000017500000002300115202510242024024 0ustar alastairalastair# 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 . import logging from unittest.mock import Mock import pytest from cylc.flow import CYLC_LOG from cylc.flow.exceptions import ( CylcConfigError, CylcError, HostSelectException ) from cylc.flow.main_loop.auto_restart import ( _can_auto_restart, _set_auto_restart, _should_auto_restart, auto_restart, ) from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.scheduler import Scheduler from cylc.flow.workflow_status import ( AutoRestartMode, StopMode ) def test_can_auto_restart_pass(monkeypatch, caplog): """Test can_auto_restart for successful host selection.""" def select_workflow_host(**_): return ('localhost', 'localhost') monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart.select_workflow_host', select_workflow_host ) assert _can_auto_restart() assert caplog.record_tuples == [] def test_can_auto_restart_fail(monkeypatch, caplog): """Test can_auto_restart for unsuccessful host selection.""" def select_workflow_host(**_): raise HostSelectException({}) monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart.select_workflow_host', select_workflow_host ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert not _can_auto_restart() [(_, level, msg)] = caplog.record_tuples assert level == logging.CRITICAL assert 'No alternative host to restart workflow on' in msg def test_can_auto_restart_fail_horribly( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ): """Test can_auto_restart for really unsuccessful host selection.""" def select_workflow_host(**_): raise Exception('Unexpected error in host selection') monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart.select_workflow_host', select_workflow_host ) with caplog.at_level(level=logging.ERROR, logger=CYLC_LOG): assert not _can_auto_restart() assert 'Error in host selection' in caplog.text assert "Traceback (most recent call last):" in caplog.text @pytest.mark.parametrize( 'host, stop_mode, condemned_hosts,' ' auto_restart_time, should_auto_restart', [ ( # no reason to restart, no reason not to 'localhost', None, [], None, False ), ( # should restart but already stopping 'localhost', StopMode.AUTO, ['localhost'], None, False ), ( # stop restart but already auto-restarting 'localhost', StopMode.AUTO, ['localhost'], 12345, False ), ( # should restart 'localhost', None, ['localhost'], None, AutoRestartMode.RESTART_NORMAL ), ( # should force stop 'localhost', None, ['localhost!'], None, AutoRestartMode.FORCE_STOP ) ] ) def test_should_auto_restart( host, stop_mode, condemned_hosts, auto_restart_time, should_auto_restart, monkeypatch ): """Ensure the workflow only auto-restarts when appropriate.""" # factor out networking and FQDNs for testing purposes monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart.get_fqdn_by_host', lambda x: x ) # mock a scheduler object scheduler = Mock( host=host, stop_mode=stop_mode, auto_restart_time=auto_restart_time ) # mock a workflow configuration object cfg = Mock() cfg.get = lambda x: condemned_hosts # test assert _should_auto_restart(scheduler, cfg) == should_auto_restart def test_set_auto_restart_already_stopping(caplog): """Ensure restart isn't attempted if already stopping.""" scheduler = Mock( stop_mode=StopMode.AUTO ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert _set_auto_restart(scheduler) assert caplog.record_tuples == [] def test_set_auto_restart_force_oveeride(caplog): """Ensure scheduled restart is cancelled for a force stop.""" scheduler = Mock( stop_mode=None, auto_restart_time=1234 ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert _set_auto_restart( scheduler, mode=AutoRestartMode.FORCE_STOP, ) assert len(caplog.record_tuples) == 2 [ (*_, msg1), (*_, msg2) ] = caplog.record_tuples assert 'This workflow will be shutdown' in msg1 assert 'Scheduled automatic restart canceled' in msg2 def test_set_auto_restart_already_restarting(caplog): """Ensure restart isn't re-scheduled.""" scheduler = Mock( stop_mode=None, auto_restart_time=1234 ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert _set_auto_restart(scheduler) assert caplog.record_tuples == [] def test_set_auto_restart_no_detach(caplog: pytest.LogCaptureFixture): """Ensure raises a CylcError (or subclass) if running in no-detach mode.""" scheduler = Mock( spec=Scheduler, stop_mode=None, auto_restart_time=None, options=Mock(no_detach=True) ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): with pytest.raises(CylcError): _set_auto_restart(scheduler) assert caplog.record_tuples == [] def test_set_auto_restart_unable_to_restart(monkeypatch): """Ensure returns False if workflow is unable to restart""" called = False def workflow_select_fail(**_): nonlocal called called = True # prevent this becoming a placebo return False monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart._can_auto_restart', workflow_select_fail ) scheduler = Mock( stop_mode=None, auto_restart_time=None, options=Mock(no_detach=False) ) assert not _set_auto_restart( scheduler ) assert called def test_set_auto_restart_with_delay(monkeypatch, caplog): """Ensure workflows wait for a period before auto-restarting.""" called = False def workflow_select_pass(**_): nonlocal called called = True # prevent this becoming a placebo return True monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart._can_auto_restart', workflow_select_pass ) monkeypatch.setattr( # remove the random element of the restart delay 'cylc.flow.main_loop.auto_restart.random', lambda: 1 ) scheduler = Mock( stop_mode=None, auto_restart_time=None, options=Mock(no_detach=False) ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert _set_auto_restart( scheduler, restart_delay=1 ) [(*_, msg1), (*_, msg2)] = caplog.record_tuples assert 'will automatically restart' in msg1 assert 'will restart in 1s' in msg2 assert called def test_set_auto_restart_without_delay(monkeypatch, caplog): """Ensure workflows auto-restart when no delay is provided.""" called = False def workflow_select_pass(**_): nonlocal called called = True # prevent this becoming a placebo return True monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart._can_auto_restart', workflow_select_pass ) scheduler = Mock( stop_mode=None, auto_restart_time=None, options=Mock(no_detach=False) ) with caplog.at_level(level=logging.DEBUG, logger=CYLC_LOG): assert _set_auto_restart( scheduler ) [(*_, msg)] = caplog.record_tuples assert 'will automatically restart' in msg assert called @pytest.mark.parametrize('exc_class', [ParsecError, CylcConfigError]) async def test_log_config_error(caplog, log_filter, monkeypatch, exc_class): """It should log errors in the global config. When errors are present in the global config they should be caught and logged nicely rather than left to spill over as traceback in the log. """ # make the global config raise an error def global_config_load_error(*args, **kwargs): raise exc_class('something even more bizarrely inexplicable') monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart.glbl_cfg', global_config_load_error, ) # call the auto_restart plugin, the error should be caught caplog.clear() assert await auto_restart(None, None) is False # the error should have been logged assert len(caplog.messages) == 1 assert 'an error in the global config' in caplog.messages[0] assert 'something even more bizarrely inexplicable' in caplog.messages[0] cylc-flow-8.6.4/tests/unit/main_loop/test_log_main_loop.py0000664000175000017500000000354715202510242024143 0ustar alastairalastair# 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 . from collections import deque from pathlib import Path import pytest from cylc.flow.main_loop.log_main_loop import ( _normalise, _dump, _plot ) @pytest.fixture() def test_data(): return { # (plugin_name, coro_name): deque([(start_time, duration), ...]) ('foo', 'bar'): deque([(2, 1), (3, 2), (4, 3)]), ('baz', 'pub'): deque([(1, 4), (2, 5), (3, 6)]), } def test_normalise(test_data): """Ensure we correctly normalise the timings against the earliest time.""" assert _normalise(test_data) == { 'foo': [(1, 1), (2, 2), (3, 3)], 'baz': [(0, 4), (1, 5), (2, 6)], } def test_dump(test_data, tmp_path): """Ensure the data is serialisable.""" assert _dump(_normalise(test_data), tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_main_loop.json') ] def test_plot(test_data, tmp_path): """Ensure the plotting mechanism doesn't raise errors.""" assert _plot(_normalise(test_data), tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_main_loop.pdf') ] cylc-flow-8.6.4/tests/unit/main_loop/test_log_data_store.py0000664000175000017500000000452315202510242024306 0ustar alastairalastair# 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 . from pathlib import Path import pytest try: from cylc.flow.main_loop.log_data_store import ( _dump, _plot, _iter_data_store ) except ModuleNotFoundError as exc: if exc.name == 'pympler': pytest.skip( 'pympler required for these tests', allow_module_level=True ) else: raise @pytest.fixture() def test_data(): return { 'times': [1, 2, 3], 'objects': { 'foo': [4, 5, 6] }, 'size': { 'foo': [7, 8, 9] } } def test_dump(test_data, tmp_path): """Ensure the data is serialiseable.""" assert _dump(test_data, tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_data_store.json') ] def test_plot_no_data(tmp_path): """Ensure plotting skips if data insufficient.""" assert not _plot({'times': [1]}, tmp_path) def test_plot(test_data, tmp_path): """Ensure the plotting mechanism doesn't raise errors.""" pytest.importorskip('matplotlib', reason='requires matplotlib') assert _plot(test_data, tmp_path) assert list(tmp_path.iterdir()) == [ Path(tmp_path, 'cylc.flow.main_loop.log_data_store.pdf') ] def test_iter_data_store(): class DataStore: tracker = {'this': 'that'} data = {'x': {'a': 1, 'workflow': 2, 'c': 3}} ds = DataStore() assert ( list(_iter_data_store(ds)) ) == [ ('data_store_mgr (total)', ds), ('tracker', {'this': 'that'}), ('data.a', 1), ('data.workflow', [2]), ('data.c', 3) ] cylc-flow-8.6.4/tests/unit/test_data_store_mgr.py0000664000175000017500000000356215202510242022337 0ustar alastairalastair# 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 . from copy import deepcopy from time import time from cylc.flow.data_store_mgr import ( task_mean_elapsed_time, apply_delta, WORKFLOW, DELTAS_MAP, ALL_DELTAS, DATA_TEMPLATE ) def int_id(): return '20130808T00/foo/03' class FakeTDef: elapsed_times = (0.0, 10.0) def test_task_mean_elapsed_time(): tdef = FakeTDef() result = task_mean_elapsed_time(tdef) assert result == 5 assert isinstance(result, int) def test_apply_delta(): """Test delta application. Some functionality is not used at the Scheduler, so is not covered by integration testing. """ w_id = 'workflow_id' delta = DELTAS_MAP[ALL_DELTAS]() delta.workflow.time = time() flow = delta.workflow.updated flow.id = 'workflow_id' flow.stamp = f'{w_id}@{delta.workflow.time}' delta.workflow.pruned = w_id data = deepcopy(DATA_TEMPLATE) assert data[WORKFLOW].id != w_id assert data[WORKFLOW].pruned is False for field, sub_delta in delta.ListFields(): apply_delta(field.name, sub_delta, data) assert data[WORKFLOW].id == w_id assert data[WORKFLOW].pruned is True cylc-flow-8.6.4/tests/unit/__init__.py0000664000175000017500000000135715202510242020045 0ustar alastairalastair# 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 . cylc-flow-8.6.4/tests/unit/test_rundb.py0000664000175000017500000001731015202510242020453 0ustar alastairalastair# 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 . import contextlib from pathlib import Path import sqlite3 from types import SimpleNamespace from typing import ( List, Optional, Tuple, ) import unittest from unittest import mock import sys import pytest from cylc.flow.exceptions import PlatformLookupError from cylc.flow.flow_mgr import FlowNums from cylc.flow.rundb import CylcWorkflowDAO from cylc.flow.util import serialise_set GLOBAL_CONFIG = """ [platforms] [[desktop[0-9]{2}|laptop[0-9]{2}]] # hosts = platform name (default) # Note: "desktop01" and "desktop02" are both valid and distinct # platforms [[sugar]] hosts = localhost job runner = slurm [[hpc]] hosts = hpcl1, hpcl2 retrieve job logs = True job runner = pbs [[hpcl1-bg]] hosts = hpcl1 retrieve job logs = True job runner = background [[hpcl2-bg]] hosts = hpcl2 retrieve job logs = True job runner = background """ class TestRunDb(unittest.TestCase): def setUp(self): self.dao = CylcWorkflowDAO(':memory:') self.mocked_connection = mock.Mock() self.dao.connect = mock.MagicMock(return_value=self.mocked_connection) get_select_task_job = [ ["cycle", "name", "NN"], ["cycle", "name", None], ["cycle", "name", "02"], ] def test_select_task_job(self): """Test the rundb CylcWorkflowDAO select_task_job method""" columns = self.dao.tables[CylcWorkflowDAO.TABLE_TASK_JOBS].columns[3:] expected_values = [[2 for _ in columns]] self.mocked_connection.execute.return_value = expected_values # parameterized test for cycle, name, submit_num in self.get_select_task_job: returned_values = self.dao.select_task_job(cycle, name, submit_num) for column in columns: self.assertEqual(2, returned_values[column.name]) def test_select_task_job_sqlite_error(self): """Test when the rundb CylcWorkflowDAO select_task_job method raises a SQLite exception, the method returns None""" self.mocked_connection.execute.side_effect = sqlite3.DatabaseError r = self.dao.select_task_job("it'll", "raise", "an error!") self.assertIsNone(r) @contextlib.contextmanager def create_temp_db(): """Create and tidy a temporary database for testing purposes.""" conn = sqlite3.connect(':memory:') yield conn conn.close() # doesn't raise error on re-invocation def test_operational_error(tmp_path, caplog): """Test logging on operational error.""" # create a db object db_file = tmp_path / 'db' with CylcWorkflowDAO(db_file) as dao: # stage some stuff dao.add_delete_item(CylcWorkflowDAO.TABLE_TASK_JOBS) dao.add_insert_item(CylcWorkflowDAO.TABLE_TASK_JOBS, ['pub']) dao.add_update_item( CylcWorkflowDAO.TABLE_TASK_JOBS, ({'pub': None}, {}) ) # connect the to DB dao.connect() # then delete the file - this will result in an OperationalError db_file.unlink() # execute & commit the staged items with pytest.raises(sqlite3.OperationalError): dao.execute_queued_items() # ensure that the failed transaction is logged for debug purposes assert len(caplog.messages) == 1 message = caplog.messages[0] assert 'An error occurred when writing to the database' in message assert 'DELETE FROM task_jobs' in message assert 'INSERT OR REPLACE INTO task_jobs' in message assert 'UPDATE task_jobs' in message assert 'The error was: no such table: task_jobs' in message if sys.version_info.major == 3 and sys.version_info.minor >= 11: assert 'SQLite error code: 1' in message assert 'SQLite error name: SQLITE_ERROR' in message else: assert 'SQLite error code: Not available' in message assert 'SQLite error name: Not available' in message def test_table_creation(tmp_path: Path): """Test tables are NOT created by default.""" db_file = tmp_path / 'db' stmt = "SELECT name FROM sqlite_master WHERE type='table'" with CylcWorkflowDAO(db_file) as dao: tables = list(dao.connect().execute(stmt)) assert tables == [] with CylcWorkflowDAO(db_file, create_tables=True) as dao: tables = [i[0] for i in dao.connect().execute(stmt)] assert CylcWorkflowDAO.TABLE_WORKFLOW_PARAMS in tables def test_context_manager_exit( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ): """Test connection is closed even if an exception occurs somewhere.""" db_file = tmp_path / 'db' mock_close = mock.Mock() with monkeypatch.context() as mp: mp.setattr(CylcWorkflowDAO, 'close', mock_close) with CylcWorkflowDAO(db_file) as dao, pytest.raises(RuntimeError): mock_close.assert_not_called() raise RuntimeError('mock err') mock_close.assert_called_once() # Close connection for real: dao.close() def test_select_task_pool_for_restart_if_not_platforms(tmp_path): """Returns error if platform error or errors raised by callback. """ # Setup a fake callback function which returns the fake "platform_name": def callback(index, row): return ''.join(row) db_file = tmp_path / 'db' dao = CylcWorkflowDAO(db_file, create_tables=True) # Fiddle the connect method to return a list of fake "platform_names": dao.connect = lambda: SimpleNamespace(execute=lambda _: ['foo', 'bar']) # Assert that an error is raised and that it mentions both fake platforms: with pytest.raises( PlatformLookupError, match='not defined.*\n.*foo.*\n.*bar' ): dao.select_task_pool_for_restart(callback) @pytest.mark.parametrize( 'values, expected', [ pytest.param( [ ({1, 2}, '2021-01-01T00:00:00'), ({3, 4}, '2021-01-01T00:00:02'), ({5, 6}, '2021-01-01T00:00:01'), ], {3, 4}, id="basic" ), pytest.param( [ ({2}, '2021-01-01T00:00:00'), (set(), '2021-01-01T00:00:01'), (set(), '2021-01-01T00:00:02'), ], {2}, id="ignore flow=none" ), pytest.param( [ (set(), '2021-01-01T00:00:01'), (set(), '2021-01-01T00:00:02'), ], None, id="all flow=none" ), ], ) def test_select_latest_flow_nums( values: List[Tuple[FlowNums, str]], expected: Optional[FlowNums] ): with CylcWorkflowDAO(':memory:') as dao: conn = dao.connect() conn.execute( "CREATE TABLE task_states (flow_nums TEXT, time_created TEXT)" ) for (fnums, timestamp) in values: conn.execute( 'INSERT INTO task_states VALUES (?, ?)', (serialise_set(fnums), timestamp) ) conn.commit() assert dao.select_latest_flow_nums() == expected cylc-flow-8.6.4/tests/unit/test_db_compat.py0000664000175000017500000001634415202510242021277 0ustar alastairalastair# 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 . """Compatibility tests for handling old workflow databases.""" from functools import partial from unittest.mock import Mock import pytest import sqlite3 from cylc.flow.exceptions import CylcError, ServiceFileError from cylc.flow.task_pool import TaskPool from cylc.flow.workflow_db_mgr import ( CylcWorkflowDAO, WorkflowDatabaseManager, ) from cylc.flow.dbstatecheck import CylcWorkflowDBChecker @pytest.fixture def _setup_db(tmp_path): """Fixture to create old DB.""" def _inner(stmts, db_file_name='sql.db'): db_file = tmp_path / db_file_name db_file.parent.mkdir(parents=True, exist_ok=True) # Note: cannot use CylcWorkflowDAO here as creating outdated DB conn = sqlite3.connect(str(db_file)) conn.execute(( r'CREATE TABLE task_states(name TEXT, cycle TEXT, flow_nums TEXT,' r' time_created TEXT, time_updated TEXT, submit_num INTEGER,' r' status TEXT, flow_wait INTEGER, is_manual_submit INTEGER,' r' PRIMARY KEY(name, cycle, flow_nums));') ) conn.execute(( r'CREATE TABLE task_jobs(cycle TEXT, name TEXT,' r' submit_num INTEGER, is_manual_submit INTEGER,' r' try_num INTEGER, time_submit TEXT, time_submit_exit TEXT,' r' submit_status INTEGER, time_run TEXT, time_run_exit TEXT,' r' run_signal TEXT, run_status INTEGER, platform_name TEXT,' r' job_runner_name TEXT, job_id TEXT,' r' PRIMARY KEY(cycle, name, submit_num));' )) for stmt, values in stmts: conn.execute(stmt, values) conn.execute(( r"INSERT INTO task_jobs VALUES" r" ('10090101T0000Z', 'foo', 1, 0, 1, '2022-12-05T14:46:06Z'," r" '2022-12-05T14:46:07Z', 0, '2022-12-05T14:46:10Z'," r" '2022-12-05T14:46:39Z', '', 0, 'localhost', 'background'," r" 4377)" )) conn.commit() conn.close() return db_file return _inner def test_upgrade_pre_810_fails_on_multiple_flows(_setup_db): stmts = [( r'INSERT INTO task_states VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', ( 'foo', '10050101T0000Z', '[1, 3]', '2022-12-05T14:46:33Z', '2022-12-05T14:46:40Z', 1, 'succeeded', 0, 0, ), )] db_file_name = _setup_db(stmts) with CylcWorkflowDAO(db_file_name) as dao, pytest.raises( CylcError, match='^Cannot .* 8.0.x to 8.1.0 .* used.$' ): WorkflowDatabaseManager.upgrade_pre_810(dao) def test_upgrade_pre_810_pass_on_single_flow(_setup_db): stmts = [( r'INSERT INTO task_states VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', ( 'foo', '10050101T0000Z', '[1]', '2022-12-05T14:46:33Z', '2022-12-05T14:46:40Z', 1, 'succeeded', 0, 0, ), )] db_file_name = _setup_db(stmts) with CylcWorkflowDAO(db_file_name) as dao: WorkflowDatabaseManager.upgrade_pre_810(dao) result = dao.connect().execute( 'SELECT DISTINCT flow_nums FROM task_jobs;' ).fetchall()[0][0] assert result == '[1]' def test_check_workflow_db_compat(_setup_db, capsys): """method can pick private or public db to check. """ # Create public and private databases with different cylc versions: create = r'CREATE TABLE workflow_params(key TEXT, value TEXT)' insert = r'INSERT INTO workflow_params VALUES (?, ?)' pri_path = _setup_db( [ (create, tuple()), (insert, ('cylc_version', '7.99.99')), ], db_file_name='private/db', ) pub_path = _setup_db( [ (create, tuple()), (insert, ('cylc_version', '7.99.98')), ], db_file_name='public/db' ) with pytest.raises(ServiceFileError, match='99.98'): WorkflowDatabaseManager.check_db_compatibility(pub_path) with pytest.raises(ServiceFileError, match='99.99'): WorkflowDatabaseManager.check_db_compatibility(pri_path) def test_cylc_7_db_wflow_params_table(_setup_db): """Test back-compat needed by workflow state xtrigger for Cylc 7 DBs.""" ptformat = "CCYY" create = r'CREATE TABLE suite_params(key TEXT, value TEXT)' insert = r'INSERT INTO suite_params VALUES (?, ?)' db_file_name = _setup_db([ (create, tuple()), (insert, ('cycle_point_format', ptformat)), ]) with CylcWorkflowDBChecker('foo', 'bar', db_path=db_file_name) as checker: with pytest.raises( sqlite3.OperationalError, match="no such table: workflow_params" ): checker._get_db_point_format() assert checker.db_point_fmt == ptformat def test_pre_830_task_action_timers(_setup_db): """Test back compat for task_action_timers table. Before 8.3.0, TaskEventMailContext had an extra field "ctx_type" at index 1. TaskPool.load_db_task_action_timers() should be able to discard this field. """ stmts = [ ( r''' CREATE TABLE task_action_timers( cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key) ); ''', tuple(), ), ( r'INSERT INTO task_action_timers VALUES (?, ?, ?, ?, ?, ?, ?, ?)', ( '1', 'foo', '[["event-mail", "failed"], 9]', ( '["TaskEventMailContext", ["event-mail", "event-mail",' ' "notifications@fbc.gov", "jfaden"]]' ), '[0.0]', 1, '0.0', '1709229449.61275', ) ), ( r'INSERT INTO task_action_timers VALUES (?, ?, ?, ?, ?, ?, ?, ?)', ( '1', 'foo', '["try_timers", "execution-retry"]', None, '[94608000.0]', 1, None, None, ), ), ] db_file = _setup_db(stmts) mock_pool = Mock() load_db_task_action_timers = partial( TaskPool.load_db_task_action_timers, mock_pool ) with CylcWorkflowDAO(db_file, create_tables=True) as dao: dao.select_task_action_timers(load_db_task_action_timers) cylc-flow-8.6.4/tests/unit/test_hostuserutil.py0000664000175000017500000000435615202510242022121 0ustar alastairalastair# 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 . import os import re import pytest from cylc.flow.hostuserutil import ( get_fqdn_by_host, get_host, get_user, get_user_home, is_remote_host, is_remote_user ) def test_is_remote_user_on_current_user(): """is_remote_user with current user.""" assert not is_remote_user(None) assert not is_remote_user(os.getenv('USER')) def test_is_remote_host_on_localhost(monkeypatch): """is_remote_host with localhost.""" assert not is_remote_host(None) assert not is_remote_host('localhost') assert not is_remote_host('localhost4.localhost42') assert not is_remote_host(os.getenv('HOSTNAME')) assert not is_remote_host(get_host()) def test_get_fqdn_by_host_on_bad_host(): """get_fqdn_by_host bad host. Warning: This test can fail due to ISP/network configuration (for example ISP may reroute failed DNS to custom search page) e.g: https://www.virginmedia.com/help/advanced-network-error-search """ bad_host = 'nosuchhost.nosuchdomain.org' with pytest.raises(IOError) as exc: get_fqdn_by_host(bad_host) assert re.match( r"(\[Errno -2\] Name or service|" r"\[Errno 8\] nodename nor servname provided, or)" r" not known: '{}'".format(bad_host), str(exc.value) ) assert exc.value.filename == bad_host def test_get_user(): """get_user.""" assert os.getenv('USER') == get_user() def test_get_user_home(): """get_user_home.""" assert os.getenv('HOME') == get_user_home() cylc-flow-8.6.4/tests/unit/test_psutil.py0000664000175000017500000000314015202510242020655 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import json import os from psutil import Process from cylc.flow.scripts.psutil import _psutil def test_psutil_basic(): """It should return measurements.""" # obtain memory reading ret = _psutil('[["virtual_memory"]]') # we asked for one thing so we should get one response assert len(ret) == 1 # the result should be dict-like and serialise to json mem = ret[0] dict(mem) json.dumps(mem) for key in ('total', 'available', 'used', 'free'): # it should have multiple fields assert key in mem # all of which should be integers assert isinstance(mem[key], int) def test_psutil_object(): """It should call object methods.""" # obtain the commandline for this test process ret = _psutil(f'[["Process.cmdline", {os.getpid()}]]') assert ret[0] == Process().cmdline() cylc-flow-8.6.4/tests/unit/test_task_state_prop.py0000664000175000017500000000270415202510242022544 0ustar alastairalastair# 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 . import pytest from cylc.flow.task_state import ( TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED ) from cylc.flow.task_state_prop import extract_group_state def test_extract_group_state_childless(): assert extract_group_state(child_states=[]) is None @pytest.mark.parametrize("child_states, is_stopped, expected", [ ( [TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED], False, TASK_STATUS_SUBMIT_FAILED ), ( ["Who?", TASK_STATUS_FAILED], False, TASK_STATUS_FAILED ) ]) def test_extract_group_state_order(child_states, is_stopped, expected): assert extract_group_state( child_states=child_states, is_stopped=is_stopped ) == expected cylc-flow-8.6.4/tests/unit/test_timer.py0000664000175000017500000000333315202510242020461 0ustar alastairalastair# 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 . import logging from time import sleep import pytest from cylc.flow.timer import Timer def test_Timer(caplog: pytest.LogCaptureFixture): """Test the Timer class.""" caplog.set_level(logging.INFO) timer = Timer("bob timeout", 1.0) # timer attributes assert timer.name == "bob timer" assert timer.interval == "PT1S" # start timer timer.reset() assert caplog.records[-1].message == "PT1S bob timer starts NOW" # check timeout sleep(2) assert timer.timed_out() assert caplog.records[-1].message == "bob timer timed out after PT1S" # stop should do nothing after timeout caplog.clear() timer.stop() assert not caplog.records # start timer again, then stop it timer.reset() assert caplog.records[-1].message == "PT1S bob timer starts NOW" timer.stop() assert caplog.records[-1].message == "bob timer stopped" # another stop should do nothing caplog.clear() timer.stop() assert not caplog.records cylc-flow-8.6.4/tests/unit/test_task_outputs.py0000664000175000017500000002605315202510242022112 0ustar alastairalastair# 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 . from types import SimpleNamespace from typing import ( Optional, Set, ) import pytest from cylc.flow.task_outputs import ( TASK_OUTPUT_EXPIRED, TASK_OUTPUT_FAILED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, TASK_OUTPUTS, TaskOutputs, get_completion_expression, get_trigger_completion_variable_maps, ) from cylc.flow.util import sstrip def tdef(required, optional, completion=None): """Stub a task definition. Args: required: Collection of required outputs. optional: Collection of optional outputs. completion: User defined execution completion expression. """ return SimpleNamespace( rtconfig={ 'completion': completion, }, outputs={ output: ( output, ( # output is required: True if output in required # output is optional: else False if output in optional # output is ambiguous (i.e. not referenced in graph): else None ) ) for output in set(TASK_OUTPUTS) | set(required) | set(optional) }, ) def test_completion_implicit(): """It should generate a completion expression when none is provided. The outputs should be considered "complete" according to the logic in proposal point 5: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ # one required output - succeeded outputs = TaskOutputs(tdef([TASK_OUTPUT_SUCCEEDED], [])) # the completion expression should only contain the one required output assert outputs._completion_expression == 'succeeded' # the outputs should be incomplete - it hasn't run yet assert outputs.is_complete() is False # set the submit-failed output outputs.set_message_complete(TASK_OUTPUT_SUBMIT_FAILED) # the outputs should be incomplete - submited-failed is a "final" output assert outputs.is_complete() is False # set the submitted and succeeded outputs outputs.set_message_complete(TASK_OUTPUT_SUBMITTED) outputs.set_message_complete(TASK_OUTPUT_SUCCEEDED) # the outputs should be complete - it has run an succeedd assert outputs.is_complete() is True # set the expired output outputs.set_message_complete(TASK_OUTPUT_EXPIRED) # the outputs should still be complete - it has run and succeeded assert outputs.is_complete() is True def test_completion_explicit(): """It should use the provided completion expression. The outputs should be considered "complete" according to the logic in proposal point 5: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ outputs = TaskOutputs(tdef( # no required outputs [], # four optional outputs [ TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, 'x', 'y', ], # one pair must be satisfied for the outputs to be complete completion='(succeeded and x) or (failed and y)', )) # the outputs should be incomplete - it hasn't run yet assert outputs.is_complete() is False # set the succeeded and failed outputs outputs.set_message_complete(TASK_OUTPUT_SUCCEEDED) outputs.set_message_complete(TASK_OUTPUT_FAILED) # the task should be incomplete - it has executed but the completion # expression is not satisfied assert outputs.is_complete() is False # satisfy the (failed and y) pair outputs.set_message_complete('y') assert outputs.is_complete() is True # satisfy the (succeeded and x) pair outputs._completed['y'] = False outputs.set_message_complete('x') assert outputs.is_complete() is True @pytest.mark.parametrize( 'required, optional, expression', [ pytest.param( {TASK_OUTPUT_SUCCEEDED}, [], 'succeeded', id='0', ), pytest.param( {TASK_OUTPUT_SUCCEEDED, 'x'}, [], '(succeeded and x)', id='1', ), pytest.param( [], {TASK_OUTPUT_SUCCEEDED}, 'succeeded or failed', id='2', ), pytest.param( {TASK_OUTPUT_SUCCEEDED}, {TASK_OUTPUT_EXPIRED}, 'succeeded or expired', id='3', ), pytest.param( [], {TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_EXPIRED}, 'succeeded or failed or expired', id='4', ), pytest.param( {TASK_OUTPUT_SUCCEEDED}, {TASK_OUTPUT_EXPIRED, TASK_OUTPUT_SUBMITTED}, 'succeeded or submit_failed or expired', id='5', ), pytest.param( {TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_SUBMITTED}, {TASK_OUTPUT_EXPIRED}, '(submitted and succeeded) or expired', id='6', ), pytest.param( [], {TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_SUBMIT_FAILED}, 'succeeded or failed or submit_failed', id='7', ), pytest.param( {'x'}, { TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_EXPIRED, }, '(x and succeeded) or failed or submit_failed or expired', id='8', ), ], ) def test_get_completion_expression_implicit(required, optional, expression): """It should generate a completion expression if none is provided.""" assert get_completion_expression(tdef(required, optional)) == expression def test_get_completion_expression_explicit(): """If a completion expression is used, it should be used unmodified.""" assert get_completion_expression(tdef( {'x', 'y'}, {TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, TASK_OUTPUT_EXPIRED}, '((failed and x) or (succeeded and y)) or expired' )) == '((failed and x) or (succeeded and y)) or expired' def test_format_completion_status(): outputs = TaskOutputs( tdef( {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, {TASK_OUTPUT_EXPIRED}, ) ) assert outputs.format_completion_status( indent=2, gutter=2 ) == ' ' + sstrip( ''' ┆ ( ⨯ ┆ succeeded ⨯ ┆ and x ⨯ ┆ and y ┆ ) ⨯ ┆ or expired ''' ) outputs.set_message_complete('succeeded') outputs.set_message_complete('x') assert outputs.format_completion_status( indent=2, gutter=2 ) == ' ' + sstrip( ''' ┆ ( ✓ ┆ succeeded ✓ ┆ and x ⨯ ┆ and y ┆ ) ⨯ ┆ or expired ''' ) @pytest.mark.parametrize( 'required, optional, expected_required, expected_expression', [ # this task has three required outputs and one optional output pytest.param( {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, {'z'}, {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, None, id="3-required-1-optional", ), # this task does not have any required outputs (besides the implicitly # required submitted/started outputs) # Note: validation should prevent this at the config level pytest.param( {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, {TASK_OUTPUT_FAILED}, # task may fail set(), None, id="no-required-outputs", ), # the preconditions expiry/submitted are excluded from this logic when # defined as optional: pytest.param( {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, {TASK_OUTPUT_EXPIRED}, # task may expire {TASK_OUTPUT_SUCCEEDED, 'x', 'y'}, '(succeeded and x and y) or expired', id="expiry-submitted", ), # NOTE: a required output might not be required! # If success is optional, then apparently-required outputs are made # implicitly optional. See # https://github.com/cylc/cylc-flow/pull/6505#issuecomment-2517781523 pytest.param( {'x'}, {TASK_OUTPUT_SUCCEEDED}, set(), '(x and succeeded) or failed', id="implicit-optional", ), pytest.param( set(), {'x', TASK_OUTPUT_SUCCEEDED}, set(), 'succeeded or failed', id="all-optional", ), ] ) def test_iter_required_outputs( required: Set[str], optional: Set[str], expected_required: Set[str], expected_expression: Optional[str], ): """It should yield required outputs only.""" outputs = TaskOutputs(tdef(required, optional)) if expected_expression: assert outputs._completion_expression == expected_expression assert set(outputs.iter_required_messages()) == expected_required def test_iter_required_outputs__disable(): # Get all outputs required for success path (excluding failure, what # is still required): outputs = TaskOutputs( tdef( {}, {'a', 'succeeded', 'b', 'y', 'failed', 'x'}, '(x and y and failed) or (a and b and succeeded)' ) ) assert set(outputs.iter_required_messages()) == set() # Disabling succeeded leaves us with failure required outputs: assert set( outputs.iter_required_messages(disable=TASK_OUTPUT_SUCCEEDED) ) == { TASK_OUTPUT_FAILED, 'x', 'y', } # Disabling failed leaves us with succeeded required outputs: assert set(outputs.iter_required_messages(disable=TASK_OUTPUT_FAILED)) == { TASK_OUTPUT_SUCCEEDED, 'a', 'b', } # Disabling an abitrary output leaves us with required outputs # from another branch: assert set(outputs.iter_required_messages(disable='a')) == { TASK_OUTPUT_FAILED, 'x', 'y', } def test_get_trigger_completion_variable_maps(): """It should return a bi-map of triggers to compvars.""" t2c, c2t = get_trigger_completion_variable_maps(('a', 'b-b', 'c-c-c')) assert t2c == {'a': 'a', 'b-b': 'b_b', 'c-c-c': 'c_c_c'} assert c2t == {'a': 'a', 'b_b': 'b-b', 'c_c_c': 'c-c-c'} cylc-flow-8.6.4/tests/unit/test_host_select.py0000664000175000017500000001610615202510242021657 0ustar alastairalastair# 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 . """Test the cylc.flow.host_select module. NOTE: these are functional tests, for unit tests see the docstrings in the host_select module. """ import logging from secrets import token_hex import socket import pytest from cylc.flow import CYLC_LOG from cylc.flow.exceptions import HostSelectException from cylc.flow.host_select import ( _get_metrics, select_host, select_workflow_host, ) from cylc.flow.hostuserutil import get_fqdn_by_host from cylc.flow.parsec.exceptions import ListValueError localhost, localhost_aliases, _ = socket.gethostbyname_ex('localhost') localhost_fqdn = get_fqdn_by_host(localhost) # NOTE: ensure that all localhost aliases are actually aliases of localhost, # it would appear that this is not always the case # on Travis-CI on of the aliases has a different fqdn from the fqdn # of the host it is an alias of localhost_aliases = [ alias for alias in localhost_aliases if get_fqdn_by_host(alias) == localhost_fqdn ] def test_localhost(): """Basic test with one host to choose from.""" assert select_host([localhost]) == ( localhost, localhost_fqdn ) def test_unique(): """Basic test choosing from multiple forms of localhost""" name, fqdn = select_host( localhost_aliases + [localhost] ) assert name in localhost_aliases + [localhost] assert fqdn == localhost_fqdn def test_filter(): """Test that hosts are filtered out if specified.""" message = 'Localhost not allowed' with pytest.raises(HostSelectException) as excinfo: select_host( [localhost], blacklist=[localhost], blacklist_name=message ) assert message in str(excinfo.value) def test_filter_invalid_blacklist(log_filter): """Test that invalid blacklist doesn't bring down the program.""" result = select_host( [localhost], blacklist=[f'non_exist_{token_hex(4)}'] ) assert result[0] == localhost assert log_filter(logging.WARNING, "Could not resolve blacklisted host") def test_rankings(): """Positive test that rankings are evaluated. (doesn't prove anything by itself hence test_unreasonable_rankings) """ assert select_host( [localhost], ranking_string=''' # if this test fails due to race conditions # then you have bigger issues than a test failure virtual_memory().available > 1 getloadavg()[0] < 500 cpu_count() > 1 disk_usage('/').free > 1 ''' ) == (localhost, localhost_fqdn) def test_unreasonable_rankings(): """Negative test that rankings are evaluated. (doesn't prove anything by itself hence test_rankings) """ with pytest.raises(HostSelectException) as excinfo: select_host( [localhost], ranking_string=''' # if this test fails due to race conditions # then you are very lucky virtual_memory().available > 123456789123456789 getloadavg()[0] < 1 cpu_count() > 512 disk_usage('/').free > 123456789123456789 ''' ) assert ( 'virtual_memory().available > 123456789123456789: False' ) in str(excinfo.value) def test_metric_command_failure(): """If the psutil command (or SSH) fails ensure the host is excluded.""" with pytest.raises(HostSelectException) as excinfo: select_host( [localhost], ranking_string=''' # elephant is not a psutil attribute # so will cause the command to fail elephant ''' ) # we should expect the special (2) returncode # (i.e. the command ran fine but there was something wrong with the # provided expression) assert excinfo.value.data[localhost_fqdn]['returncode'] == 2 def test_workflow_host_select(mock_glbl_cfg): """Run the workflow_host_select mechanism.""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available= {localhost} ''' ) assert select_workflow_host() == (localhost, localhost_fqdn) def test_workflow_host_select_default(mock_glbl_cfg): """Ensure "localhost" is provided as a default host.""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', ''' [scheduler] [[run hosts]] available = ''' ) hostname, host_fqdn = select_workflow_host() assert hostname in localhost_aliases + [localhost] assert host_fqdn == localhost_fqdn # NOTE: on Travis-CI the fqdn of `localhost` is `localhost` @pytest.mark.skipif( localhost == localhost_fqdn, reason='Cannot condemn a host unless is has a safe unique fqdn.' ) def test_workflow_host_select_condemned(mock_glbl_cfg): """Ensure condemned hosts are filtered out.""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {localhost} condemned = {localhost_fqdn} ''' ) with pytest.raises(HostSelectException) as excinfo: select_workflow_host() assert 'blacklisted' in str(excinfo.value) assert 'condemned host' in str(excinfo.value) def test_condemned_host_ambiguous(mock_glbl_cfg): """Test the [scheduler]condemend host coercer Not actually host_select code but related functionality. """ with pytest.raises(ListValueError) as excinfo: mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {localhost} condemned = {localhost} ''' ) assert 'ambiguous host' in excinfo.value.msg def test_get_metrics_no_hosts_error(caplog): """It should handle SSH errors. If a host is not contactable then it should be shipped. """ caplog.set_level(logging.WARN, CYLC_LOG) data = {} host_stats = _get_metrics(['not-a-host'], None, data) # a warning should be logged assert len(caplog.records) == 1 # no data for the host should be returned assert not host_stats # the return code should be recorded assert data == {'not-a-host': {'returncode': 255}} cylc-flow-8.6.4/tests/unit/test_subprocpool.py0000664000175000017500000003273315202510242021716 0ustar alastairalastair# 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 . from pathlib import Path from types import SimpleNamespace from tempfile import ( NamedTemporaryFile, SpooledTemporaryFile, TemporaryFile, TemporaryDirectory, ) import pytest from cylc.flow import LOG from cylc.flow.id import Tokens from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.task_events_mgr import TaskJobLogsRetrieveContext from cylc.flow.subprocctx import SubProcContext from cylc.flow.subprocpool import ( SubProcPool, _XTRIG_FUNC_CACHE, get_xtrig_func, ) from cylc.flow.task_outputs import ( TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, TASK_OUTPUT_EXPIRED, ) from cylc.flow.task_proxy import TaskProxy def test_get_temporary_file(): """Test SubProcPool.get_temporary_file.""" assert isinstance(SubProcPool.get_temporary_file(), SpooledTemporaryFile) def test_run_command_returns_0(): """Test basic usage, command returns 0""" ctx = SubProcContext('truth', ['true']) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == '' assert ctx.ret_code == 0 def test_run_command_returns_1(): """Test basic usage, command returns 1""" ctx = SubProcContext('lies', ['false']) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == '' assert ctx.ret_code == 1 def test_run_command_writes_to_out(): """Test basic usage, command writes to STDOUT""" ctx = SubProcContext('parrot', ['echo', 'pirate', 'urrrr']) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'pirate urrrr\n' assert ctx.ret_code == 0 def test_run_command_writes_to_err(): """Test basic usage, command writes to STDERR""" ctx = SubProcContext( 'parrot2', ['bash', '--noprofile', '--norc', '-c', 'echo pirate errrr >&2'] ) SubProcPool.run_command(ctx) assert 'pirate errrr\n' assert ctx.out == '' assert ctx.ret_code == 0 def test_run_command_with_stdin_from_str(): """Test STDIN from string""" ctx = SubProcContext('meow', ['cat'], stdin_str='catches mice.\n') SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'catches mice.\n' assert ctx.ret_code == 0 def test_run_command_with_stdin_from_unicode(): """Test STDIN from string with Unicode""" ctx = SubProcContext('meow', ['cat'], stdin_str='喵\n') SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == '喵\n' assert ctx.ret_code == 0 def test_run_command_with_stdin_from_handle(): """Test STDIN from a single opened file handle""" handle = TemporaryFile() handle.write('catches mice.\n'.encode('UTF-8')) handle.seek(0) ctx = SubProcContext('meow', ['cat'], stdin_files=[handle]) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'catches mice.\n' assert ctx.ret_code == 0 handle.close() def test_run_command_with_stdin_from_path(): """Test STDIN from a single file path""" handle = NamedTemporaryFile() handle.write('catches mice.\n'.encode('UTF-8')) handle.seek(0) ctx = SubProcContext('meow', ['cat'], stdin_files=[handle.name]) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'catches mice.\n' assert ctx.ret_code == 0 handle.close() def test_run_command_with_stdin_from_handles(): """Test STDIN from multiple file handles""" handles = [] for txt in ['catches mice.\n', 'eat fish.\n']: handle = TemporaryFile() handle.write(txt.encode('UTF-8')) handle.seek(0) handles.append(handle) ctx = SubProcContext('meow', ['cat'], stdin_files=handles) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'catches mice.\neat fish.\n' assert ctx.ret_code == 0 for handle in handles: handle.close() def test_run_command_with_stdin_from_paths(): """Test STDIN from multiple file paths""" handles = [] for txt in ['catches mice.\n', 'eat fish.\n']: handle = NamedTemporaryFile() handle.write(txt.encode('UTF-8')) handle.seek(0) handles.append(handle) ctx = SubProcContext( 'meow', ['cat'], stdin_files=[handle.name for handle in handles] ) SubProcPool.run_command(ctx) assert ctx.err == '' assert ctx.out == 'catches mice.\neat fish.\n' assert ctx.ret_code == 0 for handle in handles: handle.close() def test_xfunction(): """Test xtrigger function import.""" with TemporaryDirectory() as temp_dir: python_dir = Path(temp_dir, "lib", "python") python_dir.mkdir(parents=True) the_answer_file = python_dir / "the_answer.py" with the_answer_file.open(mode="w") as f: f.write("""the_answer = lambda: 42""") f.flush() f_name = "the_answer" fn = get_xtrig_func(f_name, f_name, temp_dir) result = fn() assert 42 == result def test_xfunction_cache(): """Test xtrigger function import cache.""" with TemporaryDirectory() as temp_dir: python_dir = Path(temp_dir, "lib", "python") python_dir.mkdir(parents=True) amandita_file = python_dir / "amandita.py" with amandita_file.open(mode="w") as f: f.write("""choco = lambda: 'chocolate'""") f.flush() m_name = "amandita" # module f_name = "choco" # function fn = get_xtrig_func(m_name, f_name, temp_dir) result = fn() assert 'chocolate' == result # is in the cache assert (m_name, f_name) in _XTRIG_FUNC_CACHE # returned from cache assert fn, get_xtrig_func(m_name, f_name == temp_dir) def test_xfunction_import_error(): """Test for error on importing a xtrigger function. To prevent the test eventually failing if the test function is added and successfully imported, we use an invalid module name as per Python spec. """ with TemporaryDirectory() as temp_dir, pytest.raises(ModuleNotFoundError): get_xtrig_func("invalid-module-name", "func-name", temp_dir) def test_xfunction_attribute_error(): """Test for error on looking for an attribute in a xtrigger script.""" with TemporaryDirectory() as temp_dir: python_dir = Path(temp_dir, "lib", "python") python_dir.mkdir(parents=True) the_answer_file = python_dir / "the_sword.py" with the_answer_file.open(mode="w") as f: f.write("""the_droid = lambda: 'excalibur'""") f.flush() f_name = "the_sword" with pytest.raises(AttributeError): get_xtrig_func(f_name, f_name, temp_dir) @pytest.fixture def mock_ctx(): def inner_(ret_code=None, host=None, cmd_key=None, cmd=None): """Provide a SimpleNamespace which looks like a ctx object.""" inputs = locals() defaults = { 'ret_code': 255, 'host': 'mouse', 'cmd_key': 'my-command', 'cmd': ['bistromathic', 'take-off'], } for key in inputs: if inputs[key] is None: inputs[key] = defaults[key] ctx = SimpleNamespace( cmd=inputs['cmd'], timestamp=None, ret_code=inputs['ret_code'], host=inputs['host'], cmd_key=inputs['cmd_key'], ) return ctx yield inner_ def _test_callback(ctx, foo=''): """Very Simple test callback function""" LOG.error(f'callback called.{foo}') def _test_callback_255(ctx, foo=''): """Very Simple test callback function""" LOG.error(f'255 callback called.{foo}') @pytest.mark.parametrize( 'expect, ret_code, cmd_key', [ pytest.param('callback called', 0, 'ssh something', id="return 0"), pytest.param('callback called', 1, 'ssh something', id="return 1"), pytest.param( 'platform: None - Could not connect to mouse.', 255, 'ssh', id="return 255", ), pytest.param( 'platform: localhost - Could not connect to mouse.', 255, TaskJobLogsRetrieveContext(['ssh', 'something'], None, None), id="return 255 (log-ret)", ), ], ) def test__run_command_exit(caplog, mock_ctx, expect, ret_code, cmd_key): """It runs a callback""" ctx = mock_ctx(ret_code=ret_code, cmd_key=cmd_key, cmd=['ssh']) SubProcPool._run_command_exit( ctx, callback=_test_callback, callback_255=_test_callback_255 ) assert expect in caplog.records[0].msg if ret_code == 255: assert '255 callback called.' in caplog.records[1].msg def test__run_command_exit_no_255_callback(caplog, mock_ctx): """It runs the vanilla callback if no 255 callback provided""" SubProcPool._run_command_exit(mock_ctx(), callback=_test_callback) assert 'callback called' in caplog.records[0].msg def test__run_command_exit_no_gettable_platform(caplog, mock_ctx): """It logs being unable to select a platform""" ret_ctx = TaskJobLogsRetrieveContext( platform_name='rhenas', max_size=256, key='rhenas' ) ctx = mock_ctx(cmd_key=ret_ctx, cmd=['ssh'], ret_code=255) SubProcPool._run_command_exit(ctx, callback=_test_callback) assert 'platform: rhenas' in caplog.records[0].msg def test__run_command_exit_no_255_args(caplog, mock_ctx): """It runs the 255 callback with the args of the callback if no callback 255 args provided. """ SubProcPool._run_command_exit( mock_ctx(cmd=['ssh', 'Zaphod']), callback=_test_callback, callback_args=['Zaphod'], callback_255=_test_callback_255, ) assert '255' in caplog.records[1].msg def test__run_command_exit_add_to_badhosts(mock_ctx): """It updates the list of badhosts""" badhosts = {'foo', 'bar'} SubProcPool._run_command_exit( mock_ctx(cmd=['ssh']), bad_hosts=badhosts, callback=print, callback_args=['Welcome to Magrathea'], ) assert badhosts == {'foo', 'bar', 'mouse'} def test__run_command_exit_add_to_badhosts_log(caplog, mock_ctx): """It gets platform name from the callback args.""" badhosts = {'foo', 'bar'} SubProcPool._run_command_exit( mock_ctx(cmd=['ssh']), bad_hosts=badhosts, callback=lambda x, t: print(str(x)), callback_args=[ TaskProxy( Tokens('~u/w//c/t/2'), SimpleNamespace( name='t', dependencies={}, sequential='', external_triggers=[], xtrig_labels={}, expiration_offset=None, outputs={ TASK_OUTPUT_SUBMITTED: [None, None], TASK_OUTPUT_SUBMIT_FAILED: [None, None], TASK_OUTPUT_SUCCEEDED: [None, None], TASK_OUTPUT_FAILED: [None, None], TASK_OUTPUT_EXPIRED: [None, None], }, graph_children={}, rtconfig={'platform': 'foo'}, ), ISO8601Point('1990'), ) ], ) assert 'platform: foo' in caplog.records[0].message assert badhosts == {'foo', 'bar', 'mouse'} def test__run_command_exit_rsync_fails(mock_ctx): """It updates the list of badhosts""" badhosts = {'foo', 'bar'} ctx = mock_ctx(cmd=['rsync'], ret_code=42, cmd_key='file-install') SubProcPool._run_command_exit( ctx=ctx, bad_hosts=badhosts, callback=print, callback_args=[ { 'name': 'Magrathea', 'ssh command': 'ssh', 'rsync command': 'rsync command', }, 'Welcome to Magrathea', ], ) assert badhosts == {'foo', 'bar', 'mouse'} @pytest.mark.parametrize( 'expect, ctx_kwargs', [ (True, {'cmd': ['ssh'], 'ret_code': 255}), (False, {'cmd': ['foo'], 'ret_code': 255}), (False, {'cmd': ['ssh'], 'ret_code': 42}), ], ) def test_ssh_255_fail(mock_ctx, expect, ctx_kwargs): """It knows when a ctx has failed""" output = SubProcPool.ssh_255_fail(mock_ctx(**ctx_kwargs)) assert output == expect @pytest.mark.parametrize( 'expect, ctx_kwargs', [ (True, {'cmd': ['rsync'], 'ret_code': 99, 'host': 'not_local'}), (True, {'cmd': ['rsync'], 'ret_code': 255, 'host': 'not_local'}), (False, {'cmd': ['make it-so'], 'ret_code': 255, 'host': 'not_local'}), (False, {'cmd': ['rsync'], 'ret_code': 125, 'host': 'localhost'}), ], ) def test_rsync_255_fail(mock_ctx, expect, ctx_kwargs): """It knows when a ctx has failed""" output = SubProcPool.rsync_255_fail( mock_ctx(**ctx_kwargs), {'ssh command': 'ssh', 'rsync command': 'rsync command'}, ) assert output == expect cylc-flow-8.6.4/tests/unit/test_exceptions.py0000664000175000017500000000462715202510242021531 0ustar alastairalastair# 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 . from textwrap import dedent import pytest from cylc.flow.exceptions import ( CylcError, HostSelectException, ) def test_cylc_error_str(): error = CylcError("abcd") assert str(error) == "abcd" def test_host_select_exception(): """Test exception used for host selection failures. * Could not connect to hosts (e.g. SSH failure). * Commands failed (e.g. configuration error). * Could not obtain metrics (e.g. host ranking expression error). * No available hosts (e.g. no hosts met ranking thresholds). """ # it should format the selection results nicely exc = HostSelectException( { 'host-1': {'returncode': 1}, 'host-2': {'returncode': 1}, }, ranking='virtual_memory().available > 1', ) assert str(exc) == dedent(''' Could not select host from: host-1: returncode: 1 host-2: returncode: 1 ''').strip() @pytest.mark.parametrize( 'ret_code, expect', [ # it should give a useful hint for exit code "2" # (error in the selection ranking expression) (2, 'This is likely an error in the ranking expression'), # it should give a useful hint for the exit code "255" # (ssh error) (255, 'Cylc could not establish SSH connection to the run hosts.') ] ) def test_host_select_exception_returncodes(ret_code, expect): assert expect in str( HostSelectException( { 'host-1': {'returncode': ret_code}, 'host-2': {'returncode': ret_code}, }, ranking='virtual_memory().available > 1', ) ) cylc-flow-8.6.4/tests/unit/test_config.py0000664000175000017500000015653415202510242020622 0ustar alastairalastair# 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 . from contextlib import suppress import logging from optparse import Values import os from pathlib import Path from textwrap import dedent from types import SimpleNamespace from typing import ( Any, Callable, Dict, List, Optional, Tuple, Type, ) from unittest.mock import Mock import pytest from cylc.flow import ( CYLC_LOG, flags, ) from cylc.flow.config import WorkflowConfig from cylc.flow.cycling import loader from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.cycling.loader import ( INTEGER_CYCLING_TYPE, ISO8601_CYCLING_TYPE, ) from cylc.flow.exceptions import ( GraphParseError, InputError, PointParsingError, WorkflowConfigError, XtriggerConfigError, ) from cylc.flow.parsec.exceptions import ( Jinja2Error, ) from cylc.flow.scheduler_cli import RunOptions from cylc.flow.scripts.validate import ValidateOptions from cylc.flow.task_outputs import ( TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.wallclock import ( get_utc_mode, set_utc_mode, ) from cylc.flow.workflow_files import WorkflowFiles Fixture = Any param = pytest.param class TestWorkflowConfig: """Test class for the Cylc WorkflowConfig object.""" def test_xfunction_imports( self, mock_glbl_cfg: 'Fixture', tmp_path: 'Path'): """Test for a workflow configuration with valid xtriggers""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''' ) python_dir = tmp_path / "lib" / "python" python_dir.mkdir(parents=True) name_a_tree_file = python_dir / "name_a_tree.py" # NB: we are not returning a lambda, instead we have a scalar name_a_tree_file.write_text("""name_a_tree = lambda: 'jacaranda'""") flow_file = tmp_path / WorkflowFiles.FLOW_FILE flow_config = """ [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] tree = name_a_tree() [[graph]] R1 = '@tree => qux' """ flow_file.write_text(flow_config) workflow_config = WorkflowConfig( workflow="name_a_tree", fpath=flow_file, options=SimpleNamespace() ) assert 'tree' in workflow_config.xtrigger_collator.functx_map def test_xfunction_import_error(self, mock_glbl_cfg, tmp_path): """Test for error when a xtrigger function cannot be imported.""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''' ) python_dir = tmp_path / "lib" / "python" python_dir.mkdir(parents=True) caiman_file = python_dir / "caiman.py" # NB: we are not returning a lambda, instead we have a scalar caiman_file.write_text("""caiman = lambda: True""") flow_file = tmp_path / WorkflowFiles.FLOW_FILE flow_config = """ [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] oopsie = piranha() [[graph]] R1 = '@oopsie => qux' """ flow_file.write_text(flow_config) with pytest.raises(XtriggerConfigError) as excinfo: WorkflowConfig( workflow="caiman_workflow", fpath=flow_file, options=SimpleNamespace() ) assert "No module named 'piranha'" in str(excinfo.value) def test_xfunction_attribute_error(self, mock_glbl_cfg, tmp_path): """Test for error when a xtrigger function cannot be imported.""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''' ) python_dir = tmp_path / "lib" / "python" python_dir.mkdir(parents=True) capybara_file = python_dir / "capybara.py" # NB: we are not returning a lambda, instead we have a scalar capybara_file.write_text("""toucan = lambda: True""") flow_file = tmp_path / WorkflowFiles.FLOW_FILE flow_config = """ [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] oopsie = capybara() [[graph]] R1 = '@oopsie => qux' """ flow_file.write_text(flow_config) with pytest.raises(XtriggerConfigError) as excinfo: WorkflowConfig(workflow="capybara_workflow", fpath=flow_file, options=SimpleNamespace()) assert "module 'capybara' has no attribute 'capybara'" in str( excinfo.value) def test_xfunction_not_callable(self, mock_glbl_cfg, tmp_path): """Test for error when a xtrigger function is not callable.""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''' ) python_dir = tmp_path / "lib" / "python" python_dir.mkdir(parents=True) not_callable_file = python_dir / "not_callable.py" # NB: we are not returning a lambda, instead we have a scalar not_callable_file.write_text("""not_callable = 42""") flow_file = tmp_path / WorkflowFiles.FLOW_FILE flow_config = """ [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] oopsie = not_callable() [[graph]] R1 = '@oopsie => qux' """ flow_file.write_text(flow_config) with pytest.raises(XtriggerConfigError) as excinfo: WorkflowConfig( workflow="workflow_with_not_callable", fpath=flow_file, options=SimpleNamespace() ) assert "callable" in str(excinfo.value) @pytest.mark.parametrize( 'fam_txt', [pytest.param('"SOMEFAM"', id="double quoted"), pytest.param('\'SOMEFAM\'', id="single quoted"), pytest.param('SOMEFAM', id="unquoted")] ) def test_family_inheritance_and_quotes( fam_txt: str, mock_glbl_cfg: Callable, tmp_flow_config: Callable ) -> None: """Test that inheritance does not ignore items, if not all quoted. For example: inherit = 'MAINFAM', SOMEFAM inherit = 'BIGFAM', SOMEFAM See bug #2700 for more/ """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''' ) id_ = 'test' file_path = tmp_flow_config(id_, f''' [scheduler] allow implicit tasks = True [task parameters] major = 1..5 minor = 10..20 [scheduling] [[graph]] R1 = """hello => MAINFAM hello => SOMEFAM""" [runtime] [[root]] script = true [[MAINFAM]] [[SOMEFAM]] [[ goodbye_0 ]] inherit = 'MAINFAM', {fam_txt} ''') config = WorkflowConfig( id_, file_path, template_vars={}, options=Values() ) assert ('goodbye_0_major1_minor10' in config.runtime['descendants']['MAINFAM_major1_minor10']) assert ('goodbye_0_major1_minor10' in config.runtime['descendants']['SOMEFAM']) @pytest.mark.parametrize( ('cycling_type', 'scheduling_cfg', 'expected_icp', 'expected_err'), [ pytest.param( ISO8601_CYCLING_TYPE, {'initial cycle point': None}, None, (WorkflowConfigError, "requires an initial cycle point"), id="Lack of icp" ), pytest.param( INTEGER_CYCLING_TYPE, {'initial cycle point': None}, '1', None, id="Default icp for integer cycling type" ), pytest.param( INTEGER_CYCLING_TYPE, {'initial cycle point': "now"}, None, (PointParsingError, "invalid literal for int()"), id="Non-integer ICP for integer cycling type" ), pytest.param( INTEGER_CYCLING_TYPE, {'initial cycle point': "20500808T0000Z"}, None, (PointParsingError, "invalid literal for int()"), id="More non-integer ICP for integer cycling type" ), pytest.param( ISO8601_CYCLING_TYPE, {'initial cycle point': "1"}, None, (PointParsingError, "Invalid ISO 8601 date representation"), id="Non-ISO8601 ICP for ISO8601 cycling type" ), pytest.param( ISO8601_CYCLING_TYPE, {'initial cycle point': 'now'}, '20050102T0615+0530', None, id="ICP = now" ), pytest.param( ISO8601_CYCLING_TYPE, {'initial cycle point': 'previous(T00)'}, '20050102T0000+0530', None, id="ICP = prev" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'initial cycle point constraints': ['T00', 'T12'] }, '20130101T0000+0530', None, id="Constraints" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2021-01-20', 'initial cycle point constraints': ['--01-19', '--01-21'] }, None, (WorkflowConfigError, "does not meet the constraints"), id="Violated constraints" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': 'a', }, None, (WorkflowConfigError, 'Invalid ISO 8601 date representation: a'), id="invalid" ), ] ) def test_process_icp( cycling_type: str, scheduling_cfg: Dict[str, Any], expected_icp: Optional[str], expected_err: Optional[Tuple[Type[Exception], str]], monkeypatch: pytest.MonkeyPatch, set_cycling_type: 'Fixture' ) -> None: """Test WorkflowConfig.process_initial_cycle_point(). "now" is assumed to be 2005-01-02T06:15+0530 Params: cycling_type: Workflow cycling type. scheduling_cfg: 'scheduling' section of workflow config. expected_icp: The expected icp value that gets set. expected_err: Exception class expected to be raised plus the message. """ set_cycling_type(cycling_type, time_zone="+0530") mocked_config = SimpleNamespace( cycling_type=cycling_type, options=SimpleNamespace(icp=None), cfg={ 'scheduling': { 'initial cycle point constraints': [], **scheduling_cfg }, }, ) monkeypatch.setattr('cylc.flow.config.get_current_time_string', lambda: '20050102T0615+0530') if expected_err: err, msg = expected_err with pytest.raises(err) as exc: WorkflowConfig.process_initial_cycle_point(mocked_config) assert msg in str(exc.value) else: WorkflowConfig.process_initial_cycle_point(mocked_config) assert mocked_config.cfg[ 'scheduling']['initial cycle point'] == expected_icp assert str(mocked_config.initial_point) == expected_icp @pytest.mark.parametrize( 'cycling_type, options, expected, expected_err', [ ( ISO8601_CYCLING_TYPE, SimpleNamespace(startcp='20210120T1700+0530'), '20210120T1700+0530', None, ), ( ISO8601_CYCLING_TYPE, SimpleNamespace(startcp='now'), '20050102T0615+0530', None, ), ( ISO8601_CYCLING_TYPE, SimpleNamespace(startcp='previous(T00)'), '20050102T0000+0530', None, ), ( ISO8601_CYCLING_TYPE, SimpleNamespace(startcp=None), '18990501T0000+0530', None, ), ( ISO8601_CYCLING_TYPE, SimpleNamespace( starttask=['20090802T0615+0530/foo', '20090802T0515+0530/bar'] ), '20090802T0515+0530', None, ), ( ISO8601_CYCLING_TYPE, SimpleNamespace( startcp='20210120T1700+0530', starttask=['20090802T0615+0530/foo'], ), None, ( InputError, "--start-cycle-point and --start-task are mutually exclusive", ), ), ( INTEGER_CYCLING_TYPE, SimpleNamespace(startcp='10'), '10', None, ) ], ) def test_process_startcp( cycling_type: str, options: SimpleNamespace, expected: str, expected_err: Optional[Tuple[Type[Exception], str]], monkeypatch: pytest.MonkeyPatch, set_cycling_type: 'Fixture' ) -> None: """Test WorkflowConfig.process_start_cycle_point(). An icp of 1899-05-01T00+0530 is assumed, and "now" is assumed to be 2005-01-02T06:15+0530 Params: options: SimpleNamespace with startcp and starttask attributes. expected: The expected startcp value that gets set. expected_err: Expected exception. """ set_cycling_type(cycling_type, time_zone='+0530') mocked_config = Mock( spec=WorkflowConfig, cycling_type=cycling_type, initial_point='18990501T0000+0530', options=options, ) monkeypatch.setattr('cylc.flow.config.get_current_time_string', lambda: '20050102T0615+0530') if expected_err is not None: err, msg = expected_err with pytest.raises(err) as exc: WorkflowConfig.process_start_cycle_point(mocked_config) assert msg in str(exc.value) else: WorkflowConfig.process_start_cycle_point(mocked_config) assert str(mocked_config.start_point) == expected @pytest.mark.parametrize( 'cycling_type, scheduling_cfg, options_fcp, expected_fcp, expected_err', [ pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2021', 'final cycle point': None, }, None, None, None, id="No fcp" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2021', 'final cycle point': '', }, None, None, None, id="Empty fcp in cfg" # This test is needed because fcp is treated as string by parsec, # unlike other cycle point settings (allows for e.g. '+P1Y') ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2016', 'final cycle point': '2021', }, None, '20210101T0000+0530', None, id="fcp in cfg" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2016', 'final cycle point': '2021', }, '2019', '20190101T0000+0530', None, id="Overriden by cli option" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2017-02-11', 'final cycle point': '+P4D', }, None, '20170215T0000+0530', None, id="Relative fcp" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2017-02-11', 'final cycle point': '+P4D+PT3H-PT2H', }, None, '20170215T0100+0530', None, id="Relative fcp chained" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2017-02-11', 'final cycle point': '---04', }, None, '20170215T0000+0530', None, id="Relative truncated fcp", marks=pytest.mark.xfail # https://github.com/metomi/isodatetime/issues/80 ), pytest.param( INTEGER_CYCLING_TYPE, { 'initial cycle point': '1', 'final cycle point': '4', }, None, '4', None, id="Integer cycling" ), pytest.param( INTEGER_CYCLING_TYPE, { 'initial cycle point': '1', 'final cycle point': '+P2', }, None, '3', None, id="Relative fcp, integer cycling" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'final cycle point': '2009', }, None, None, (WorkflowConfigError, "initial cycle point:20130101T0000+0530 is after the " "final cycle point"), id="fcp before icp" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'final cycle point': '-PT1S', }, None, None, (WorkflowConfigError, "initial cycle point:20130101T0000+0530 is after the " "final cycle point"), id="Negative relative fcp" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'final cycle point': '2021', 'final cycle point constraints': ['T00', 'T12'] }, None, '20210101T0000+0530', None, id="Constraints" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'final cycle point': '2021-01-19', 'final cycle point constraints': ['--01-19', '--01-21'] }, '2021-01-20', None, (WorkflowConfigError, "does not meet the constraints"), id="Violated constraints" ), pytest.param( ISO8601_CYCLING_TYPE, { 'initial cycle point': '2013', 'final cycle point': '2021', }, 'reload', '20210101T0000+0530', None, id="--fcp=reload" ), ] ) def test_process_fcp( cycling_type: str, scheduling_cfg: dict, options_fcp: Optional[str], expected_fcp: Optional[str], expected_err: Optional[Tuple[Type[Exception], str]], set_cycling_type: 'Fixture' ) -> None: """Test WorkflowConfig.process_final_cycle_point(). Params: cycling_type: Workflow cycling type. scheduling_cfg: 'scheduling' section of workflow config. options_fcp: The fcp set by cli option. expected_fcp: The expected fcp value that gets set. expected_err: Exception class expected to be raised plus the message. """ set_cycling_type(cycling_type, time_zone='+0530') mocked_config = SimpleNamespace( cycling_type=cycling_type, cfg={ 'scheduling': { 'final cycle point constraints': [], **scheduling_cfg, }, }, initial_point=loader.get_point( scheduling_cfg['initial cycle point'] ).standardise(), final_point=None, options=SimpleNamespace(fcp=options_fcp), ) if expected_err: err, msg = expected_err with pytest.raises(err) as exc: WorkflowConfig.process_final_cycle_point(mocked_config) assert msg in str(exc.value) else: WorkflowConfig.process_final_cycle_point(mocked_config) assert mocked_config.cfg[ 'scheduling']['final cycle point'] == expected_fcp assert str(mocked_config.final_point) == str(expected_fcp) @pytest.mark.parametrize( ('cfg_stopcp', 'options_stopcp', 'expected_value', 'expected_options_value', 'expected_warning'), [ pytest.param( None, None, None, None, None, id="no-stopcp" ), pytest.param( '1993', None, '1993', None, None, id="stopcp" ), pytest.param( '1993', '1066', '1066', '1066', None, id="stop-cp-and-cli-option" ), pytest.param( '1993', 'reload', '1993', None, None, id="stop-cp-and-cli-reload-option" ), pytest.param( '3000', None, None, None, "will have no effect as it is after the final cycle point", id="stopcp-beyond-fcp" ), pytest.param( '+P12Y -P2Y', None, '2000', None, None, id="stopcp-relative-to-icp" ), ] ) def test_process_stop_cycle_point( cfg_stopcp: Optional[str], options_stopcp: Optional[str], expected_value: Optional[str], expected_options_value: Optional[str], expected_warning: Optional[str], set_cycling_type: Callable, caplog: pytest.LogCaptureFixture ): """Test WorkflowConfig.process_stop_cycle_point(). Params: cfg_stopcp: [scheduling]stop after cycle point options_stopcp: The stopcp from cli option / database. expected_value: The expected stopcp value that gets set. expected_options_value: The expected options.stopcp that gets set. expected_warning: Expected warning message, if any. """ set_cycling_type(ISO8601_CYCLING_TYPE, dump_format='CCYY') caplog.set_level(logging.WARNING, CYLC_LOG) fcp = loader.get_point('2012').standardise() mock_config = SimpleNamespace( cfg={ 'scheduling': { 'stop after cycle point': cfg_stopcp } }, initial_point=ISO8601Point('1990'), final_point=fcp, stop_point=None, options=RunOptions(stopcp=options_stopcp), ) WorkflowConfig.process_stop_cycle_point(mock_config) assert str(mock_config.stop_point) == str(expected_value) assert mock_config.cfg['scheduling']['stop after cycle point'] == ( expected_value ) assert mock_config.options.stopcp == expected_options_value if expected_warning: assert expected_warning in caplog.text else: assert not caplog.record_tuples @pytest.mark.parametrize( 'cfg_fcp, cfg_stopcp, opts, warning_expected', [ pytest.param( '2005', '2017', {}, True, id="cfg stopcp > fcp bad" ), pytest.param( '2017', '2017', {}, False, id="cfg stopcp == fcp ok" ), pytest.param( '', '', {'fcp': '2005', 'stopcp': '2017'}, True, id="options stopcp > fcp bad" ), pytest.param( '', '', {'fcp': '2017', 'stopcp': '2017'}, False, id="options stopcp == fcp ok" ), pytest.param( '2017', '2005', {'stopcp': '2022'}, True, id="options stopcp > cfg fcp bad" ), pytest.param( '2017', '2005', {'stopcp': '2022'}, True, id="options stopcp > cfg fcp bad" ), pytest.param( '2022', '2017', {'fcp': '2005'}, True, id="cfg stopcp > options fcp bad" ), pytest.param( '', '2022', {}, False, id="no fcp" ), ] ) def test_stopcp_after_fcp( cfg_fcp: str, cfg_stopcp: str, opts: Dict[str, str], warning_expected: bool, tmp_flow_config: Callable, caplog: pytest.LogCaptureFixture, ): """Test that setting a stop after cycle point that is beyond the final cycle point is handled correctly.""" caplog.set_level(logging.WARNING, CYLC_LOG) id_ = 'cassini' flow_file: 'Path' = tmp_flow_config(id_, f""" [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1997 final cycle point = {cfg_fcp} stop after cycle point = {cfg_stopcp} [[graph]] P1Y = huygens """) cfg = WorkflowConfig(id_, flow_file, options=RunOptions(**opts)) msg = "will have no effect as it is after the final cycle point" if warning_expected: assert msg in caplog.text assert cfg.stop_point is None else: assert msg not in caplog.text if cfg_stopcp or opts.get('stopcp'): assert cfg.stop_point @pytest.mark.parametrize( 'scheduling_cfg, scheduling_expected, expected_err', [ pytest.param( { 'graph': {} }, None, (WorkflowConfigError, "No workflow dependency graph defined"), id="Empty graph" ), pytest.param( { 'graph': {'R1': 'foo'} }, { 'cycling mode': 'integer', 'initial cycle point': '1', 'final cycle point': '1', 'graph': {'R1': 'foo'} }, None, id="Pure acyclic graph" ), pytest.param( { 'cycling mode': "", 'graph': {'R1': 'foo'} }, { 'cycling mode': "", 'graph': {'R1': 'foo'} }, None, id="Pure acyclic graph but datetime cycling" ), pytest.param( { 'graph': {'R1': 'foo', 'R2': 'bar'} }, { 'graph': {'R1': 'foo', 'R2': 'bar'} }, None, id="Acyclic graph with >1 recurrence" ), ] ) def test_prelim_process_graph( scheduling_cfg: Dict[str, Any], scheduling_expected: Optional[Dict[str, Any]], expected_err: Optional[Tuple[Type[Exception], str]]): """Test WorkflowConfig.prelim_process_graph(). Params: scheduling_cfg: 'scheduling' section of workflow config. scheduling_expected: The expected scheduling section after preliminary processing. expected_err: Exception class expected to be raised plus the message. """ mock_config = SimpleNamespace(cfg={ 'scheduling': scheduling_cfg }) if expected_err: err, msg = expected_err with pytest.raises(err) as exc: WorkflowConfig.prelim_process_graph(mock_config) assert msg in str(exc.value) else: WorkflowConfig.prelim_process_graph(mock_config) assert mock_config.cfg['scheduling'] == scheduling_expected def test_utc_mode(caplog, mock_glbl_cfg): """Test that UTC mode is handled correctly.""" caplog.set_level(logging.WARNING, CYLC_LOG) def _test(utc_mode, expected, expected_warnings=0): mock_glbl_cfg( 'cylc.flow.config.glbl_cfg', f''' [scheduler] UTC mode = {utc_mode['glbl']} ''' ) mock_config = SimpleNamespace( cfg={ 'scheduler': { 'UTC mode': utc_mode['workflow'] } }, options=SimpleNamespace(utc_mode=utc_mode['stored']), ) WorkflowConfig.process_utc_mode(mock_config) assert mock_config.cfg['scheduler']['UTC mode'] is expected assert get_utc_mode() is expected assert len(caplog.record_tuples) == expected_warnings caplog.clear() tests = [ { 'utc_mode': {'glbl': True, 'workflow': None, 'stored': None}, 'expected': True }, { 'utc_mode': {'glbl': True, 'workflow': False, 'stored': None}, 'expected': False }, { # On restart 'utc_mode': {'glbl': False, 'workflow': None, 'stored': True}, 'expected': True }, { # Changed config value between restarts 'utc_mode': {'glbl': False, 'workflow': False, 'stored': True}, 'expected': True, 'expected_warnings': 1 } ] for case in tests: _test(**case) def test_cycle_point_tz(caplog, monkeypatch): """Test that `[scheduler]cycle point time zone` is handled correctly.""" caplog.set_level(logging.WARNING, CYLC_LOG) local_tz = '-0230' monkeypatch.setattr( 'cylc.flow.config.get_local_time_zone_format', lambda: local_tz ) def _test(cp_tz, utc_mode, expected, expected_warnings=0): set_utc_mode(utc_mode) mock_config = SimpleNamespace( cfg={ 'scheduler': { 'cycle point time zone': cp_tz['workflow'], }, }, options=SimpleNamespace(cycle_point_tz=cp_tz['stored']), ) WorkflowConfig.process_cycle_point_tz(mock_config) assert mock_config.cfg['scheduler'][ 'cycle point time zone'] == expected assert len(caplog.record_tuples) == expected_warnings caplog.clear() tests = [ { 'cp_tz': {'workflow': None, 'stored': None}, 'utc_mode': True, 'expected': 'Z' }, { 'cp_tz': {'workflow': None, 'stored': None}, 'utc_mode': False, 'expected': 'Z' }, { 'cp_tz': {'workflow': '+0530', 'stored': None}, 'utc_mode': True, 'expected': '+0530' }, { # On restart 'cp_tz': {'workflow': None, 'stored': '+0530'}, 'utc_mode': True, 'expected': '+0530', 'expected_warnings': 1 }, { # Changed config value between restarts 'cp_tz': {'workflow': '+0530', 'stored': '-0030'}, 'utc_mode': True, 'expected': '-0030', 'expected_warnings': 1 }, { 'cp_tz': {'workflow': 'Z', 'stored': 'Z'}, 'utc_mode': False, 'expected': 'Z' } ] for case in tests: _test(**case) def test_rsync_includes_will_not_accept_sub_directories(tmp_flow_config): id_ = 'rsynctest' flow_file = tmp_flow_config(id_, """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/subdir2/, file1, file2 """) with pytest.raises(WorkflowConfigError) as exc: WorkflowConfig( workflow=id_, fpath=flow_file, options=Values() ) assert "Directories can only be from the top level" in str(exc.value) @pytest.mark.parametrize( 'cylc_var, expected_err', [ ["CYLC_WORKFLOW_NAME", None], ["CYLC_BEEF_WELLINGTON", (Jinja2Error, "is undefined")], ] ) def test_jinja2_cylc_vars(tmp_flow_config, cylc_var, expected_err): """Defined CYLC_ variables should be available to Jinja2 during parsing. This test is not located in the jinja2_support unit test module because CYLC_ variables are only defined during workflow config parsing. """ reg = 'nodule' flow_file = tmp_flow_config(reg, """#!Jinja2 # {{""" + cylc_var + """}} [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo """) if expected_err is None: WorkflowConfig(workflow=reg, fpath=flow_file, options=Values()) else: with pytest.raises(expected_err[0]) as exc: WorkflowConfig(workflow=reg, fpath=flow_file, options=Values()) assert expected_err[1] in str(exc) def test_valid_rsync_includes_returns_correct_list(tmp_flow_config): """Test that the rsync includes in the correct """ id_ = 'rsynctest' flow_file = tmp_flow_config(id_, """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/, file1, file2 allow implicit tasks = True """) config = WorkflowConfig( workflow=id_, fpath=flow_file, options=Values() ) rsync_includes = WorkflowConfig.get_validated_rsync_includes(config) assert rsync_includes == ['dir/', 'dir2/', 'file1', 'file2'] @pytest.mark.parametrize( 'cycling_type, runahead_limit, valid', [ (INTEGER_CYCLING_TYPE, 'P14', True), (ISO8601_CYCLING_TYPE, 'P14', True), (ISO8601_CYCLING_TYPE, 'PT12H', True), (ISO8601_CYCLING_TYPE, 'P7D', True), (ISO8601_CYCLING_TYPE, 'P2W', True), (ISO8601_CYCLING_TYPE, '4', True), (INTEGER_CYCLING_TYPE, 'PT12H', False), (INTEGER_CYCLING_TYPE, 'P7D', False), (INTEGER_CYCLING_TYPE, '4', False), (ISO8601_CYCLING_TYPE, '', False), (ISO8601_CYCLING_TYPE, 'asdf', False) ] ) def test_process_runahead_limit( cycling_type: str, runahead_limit: str, valid: bool, set_cycling_type: Callable ) -> None: set_cycling_type(cycling_type) mock_config = SimpleNamespace(cycling_type=cycling_type) mock_config.cfg = { 'scheduling': { 'runahead limit': runahead_limit } } if valid: WorkflowConfig.process_runahead_limit(mock_config) else: with pytest.raises(WorkflowConfigError) as exc: WorkflowConfig.process_runahead_limit(mock_config) assert "bad runahead limit" in str(exc.value).lower() @pytest.mark.parametrize( 'opt', [None, 'check_circular'] ) def test_check_circular(opt, monkeypatch, caplog, tmp_flow_config): """Test WorkflowConfig._check_circular().""" # ----- Setup ----- caplog.set_level(logging.INFO, CYLC_LOG) options = SimpleNamespace(is_validate=True) if opt: setattr(options, opt, True) id_ = 'circular' flow_file = tmp_flow_config(id_, """ [scheduling] cycling mode = integer [[graph]] R1 = "a => b => c => d => e => a" [runtime] [[a, b, c, d, e]] script = True """) def WorkflowConfig__assert_err_raised(): with pytest.raises(WorkflowConfigError) as exc: WorkflowConfig(workflow=id_, fpath=flow_file, options=options) assert "circular edges detected" in str(exc.value) # ----- The actual test ----- WorkflowConfig__assert_err_raised() # Now artificially lower the limit and re-test: monkeypatch.setattr( 'cylc.flow.config.WorkflowConfig.CHECK_CIRCULAR_LIMIT', 4) if opt != 'check_circular': # Will no longer raise WorkflowConfig(workflow='circular', fpath=flow_file, options=options) msg = "will not check graph for circular dependencies" assert msg in caplog.text else: WorkflowConfig__assert_err_raised() @pytest.mark.parametrize( 'graph', (('foo:x => bar'), ('foo:x')) ) def test_undefined_custom_output(graph: str, tmp_flow_config: Callable): """Test error on undefined custom output referenced in graph.""" id_ = 'custom_out1' flow_file = tmp_flow_config(id_, f""" [scheduling] [[graph]] R1 = "{graph}" [runtime] [[foo, bar]] """) with pytest.raises(WorkflowConfigError) as cm: WorkflowConfig(workflow=id_, fpath=flow_file, options=Values()) assert "Undefined custom output" in str(cm.value) def test_invalid_custom_output_msg(tmp_flow_config: Callable): """Test invalid output message (colon not allowed).""" id_ = 'invalid_output' flow_file = tmp_flow_config(id_, """ [scheduling] [[graph]] R1 = "foo:x => bar" [runtime] [[bar]] [[foo]] [[[outputs]]] x = "the quick: brown fox" """) with pytest.raises(WorkflowConfigError) as cm: WorkflowConfig( workflow=id_, fpath=flow_file, options=Values()) assert ( 'Invalid task message "[runtime][foo][outputs]x = ' 'the quick: brown fox"' ) in str(cm.value) def test_c7_back_compat_optional_outputs(tmp_flow_config, monkeypatch): """Test optional and required outputs Cylc 7 back compat mode. Success outputs should be required, others optional. Tested here because success is set to required after graph parsing, in taskdef processing. """ monkeypatch.setattr('cylc.flow.flags.cylc7_back_compat', True) id_ = 'custom_out2' flow_file = tmp_flow_config(id_, ''' [scheduling] [[graph]] R1 = """ foo:x => bar foo:fail = oops foo => spoo """ [runtime] [[bar, oops, spoo]] [[foo]] [[[outputs]]] x = x ''') cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) for taskdef in cfg.taskdefs.values(): for output, (_, required) in taskdef.outputs.items(): if output in [TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED]: assert required else: assert not required @pytest.mark.parametrize( 'graph', [ "foo:x => bar", "foo:start => bar", "foo:submit => bar", ] ) def test_implicit_success_required(tmp_flow_config, graph): """Check foo:succeed is required if success/fail not used in the graph.""" id_ = 'blargh' flow_file = tmp_flow_config(id_, f""" [scheduling] [[graph]] R1 = {graph} [runtime] [[bar]] [[foo]] [[[outputs]]] x = "the quick brown fox" """) cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) assert cfg.taskdefs['foo'].outputs[TASK_OUTPUT_SUCCEEDED][1] @pytest.mark.parametrize( 'allow_implicit_tasks', [ pytest.param(True, id="allow implicit tasks = True"), pytest.param(None, id="allow implicit tasks not set"), pytest.param(False, id="allow implicit tasks = False") ] ) @pytest.mark.parametrize( 'cylc7_compat, rose_suite_conf, expected_exc, extra_msg_expected', [ pytest.param( False, False, WorkflowConfigError, True, id="Default" ), pytest.param( False, True, WorkflowConfigError, True, id="rose-suite.conf present" ), pytest.param( True, False, None, False, id="Cylc 7 back-compat" ), pytest.param( True, True, WorkflowConfigError, False, id="Cylc 7 back-compat, rose-suite.conf present" ), ] ) def test_implicit_tasks( allow_implicit_tasks: Optional[bool], cylc7_compat: bool, rose_suite_conf: bool, expected_exc: Optional[Type[Exception]], extra_msg_expected: bool, caplog: pytest.LogCaptureFixture, log_filter: Callable, monkeypatch: pytest.MonkeyPatch, tmp_flow_config: Callable ): """Test that the prescence of implicit tasks in the config is handled correctly. Params: allow_implicit_tasks: Value of "[scheduler]allow implicit tasks". cylc7_compat: Whether Cylc 7 backwards compatibility is turned on. rose_suite_conf: Whether a rose-suite.conf file is present in run dir. expected_exc: Exception expected to be raised only when "[scheduler]allow implicit tasks" is not set. extra_msg_expected: If True, there should be the note on how to allow implicit tasks in the err msg. """ # Setup id_ = 'rincewind' allow_implicit_tasks_text = ( f'allow implicit tasks = {allow_implicit_tasks}' if allow_implicit_tasks is not None else '' ) flow_file: 'Path' = tmp_flow_config( id_, dedent(f""" [scheduler] {allow_implicit_tasks_text} [scheduling] [[graph]] R1 = foo """) ) monkeypatch.setattr('cylc.flow.flags.cylc7_back_compat', cylc7_compat) if rose_suite_conf: (flow_file.parent / 'rose-suite.conf').touch() caplog.set_level(logging.DEBUG, CYLC_LOG) if allow_implicit_tasks is True: expected_exc = None elif allow_implicit_tasks is False: expected_exc = WorkflowConfigError extra_msg_expected &= (allow_implicit_tasks is None) # Test args: dict = {'workflow': id_, 'fpath': flow_file, 'options': None} expected_msg = r"implicit tasks detected.*" if expected_exc: with pytest.raises(expected_exc, match=expected_msg) as excinfo: WorkflowConfig(**args) assert ( "To allow implicit tasks" in str(excinfo.value) ) is extra_msg_expected else: WorkflowConfig(**args) @pytest.mark.parametrize('workflow_meta', [True, False]) @pytest.mark.parametrize('url_type', ['good', 'bad', 'ugly', 'broken']) def test_process_urls(log_filter, workflow_meta, url_type): if url_type == 'good': # valid cylc 8 syntax url = '%(workflow)s' elif url_type == 'bad': # no variable called "foo" url = '%(foo)s' elif url_type == 'broken': # invalid syntax (missing the trailing "s") url = '%(suite_name)' elif url_type == 'ugly': # valid cylc 7 syntax url = '%(suite_name)s' config = SimpleNamespace() config.workflow = 'my-workflow' if workflow_meta: config.cfg = { 'meta': {'URL': url}, 'runtime': {} } else: config.cfg = { 'meta': {'URL': ''}, 'runtime': {'foo': {'meta': {'URL': url}}}, } if url_type == 'good': WorkflowConfig.process_metadata_urls(config) elif url_type in {'bad', 'broken'}: with pytest.raises(InputError): WorkflowConfig.process_metadata_urls(config) elif url_type == 'ugly': WorkflowConfig.process_metadata_urls(config) assert log_filter( contains='Detected deprecated template variables', ) @pytest.mark.parametrize('opts', [ValidateOptions(), RunOptions()]) @pytest.mark.parametrize( 'recurrence, should_warn', [ # Format 3: ('P0Y', True), ('R//P0Y', True), ('R2//P0Y', True), ('R1//P0Y', False), # Format 4: ('R/P0M', True), ('R1/P0M', False), # Format 1: ('R/2002-09-01/2002-09-01', True), ('R1/2002-09-01/2002-09-01', False), ('R/2002-08-31/2002-09-02', False), ] ) def test_zero_interval( recurrence: str, should_warn: bool, opts: Values, tmp_flow_config: Callable, log_filter: Callable, ): """Test that a zero-duration recurrence with >1 repetition gets an appropriate warning.""" id_ = 'ordinary' flow_file: 'Path' = tmp_flow_config(id_, f""" [scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2002-08-30 final cycle point = 2002-09-14 [[graph]] {recurrence} = slidescape36 """) WorkflowConfig(id_, flow_file, options=opts) logged = log_filter( level=logging.WARNING, contains="Cannot have more than 1 repetition for zero-duration" ) if should_warn: assert logged else: assert not logged @pytest.mark.parametrize( 'icp, fcp_expr, expected_fcp', [ ('2021-02-28', '+P1M+P1D', '2021-03-29'), ('2019-02-28', '+P1D+P1M', '2019-04-01'), ('2008-07-01', '+P1M-P1D', '2008-07-31'), ('2004-07-01', '-P1D+P1M', '2004-07-30'), ('1992-02-29', '+P1Y+P1M', '1993-03-28'), ('1988-02-29', '+P1M+P1Y', '1989-03-29'), ('1910-08-14', '+P2D-PT6H', '1910-08-15T18:00'), ('1850-04-10', '+P1M-P1D+PT1H', '1850-05-09T01:00'), ('1066-10-14', '+PT1H+PT1M', '1066-10-14T01:01'), ] ) def test_chain_expr( icp: str, fcp_expr: str, expected_fcp: str, tmp_flow_config: Callable, ): """Test a "chain expression" final cycle point offset. Note the order matters when "nominal" units (years, months) are used. """ id_ = 'osgiliath' flow_file: 'Path' = tmp_flow_config(id_, f""" [scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = {icp} final cycle point = {fcp_expr} [[graph]] P1D = faramir """) cfg = WorkflowConfig(id_, flow_file, options=ValidateOptions()) assert cfg.final_point == ISO8601Point(expected_fcp).standardise() @pytest.mark.parametrize( 'runtime_cfg', ( pytest.param( {'foo': {'remote': {'host': 'bar'}}}, id='no-owners' ), pytest.param( {'foo': {'remote': {'owner': 'tim'}}}, id='one-owner' ), pytest.param( { 'foo': {'remote': {'owner': 'tim'}}, 'bar': {'remote': {'owner': 'oliver'}}, 'baz': {'remote': {'owner': 'ronnie'}}, }, id='3-owners' ), pytest.param( { 'foo': {'remote': {'owner': 'tim'}}, 'bar': {'remote': {'owner': 'oliver'}}, 'baz': {'remote': {'owner': 'ronnie'}}, 'qux': {'remote': {'owner': 'tim'}}, 'aleph': {'remote': {'owner': 'oliver'}}, 'bet': {'remote': {'owner': 'ronnie'}}, }, id='6-owners' ), ) ) def test_check_for_owner(runtime_cfg): """check_for_owner raises a list of [runtime][task][remote]owner set.""" if 'owner' in str(runtime_cfg): with pytest.raises(WorkflowConfigError) as exc: WorkflowConfig.check_for_owner(runtime_cfg) # Assert is the correct error message: assert exc.match('owner\" is obsolete') # Assert error message has right number of lines: else: # Assert function doesn't raise if no owner set: assert WorkflowConfig.check_for_owner(runtime_cfg) is None @pytest.fixture(scope='module') def awe_config(mod_tmp_flow_config: Callable) -> WorkflowConfig: """Return a workflow config object.""" id_ = 'awe' flow_file = mod_tmp_flow_config(id_, ''' [scheduling] cycling mode = integer [[graph]] P1 = ordinary & sterling R1/2 = fra_mauro [runtime] [[USA, MOON]] [[ordinary, sterling]] inherit = USA [[fra_mauro]] inherit = MOON ''') return WorkflowConfig( workflow=id_, fpath=flow_file, options=ValidateOptions() ) @pytest.mark.parametrize( 'name, expected', [ pytest.param( 'ordinary', ['ordinary'], id="task name" ), pytest.param( 'USA', ['ordinary', 'sterling'], id="family name" ), pytest.param( 'fra*', ['fra_mauro'], id="glob task name" ), pytest.param( 'U*', ['ordinary', 'sterling'], id="glob family name" ), pytest.param( '*', ['ordinary', 'sterling', 'fra_mauro'], id="glob everything" ), pytest.param( 'butte', [], id="no match" ), ] ) def test_find_taskdefs( name: str, expected: List[str], awe_config: WorkflowConfig ): assert sorted( t.name for t in awe_config.find_taskdefs(name) ) == sorted(expected) def test__warn_if_queues_have_implicit_tasks(caplog): """It Warns that queues imply tasks undefined in runtime. """ config = { 'scheduling': {'queues': { 'q1': {'members': ['foo']}, 'q2': {'members': ['bar', 'baz']} }}, 'runtime': {} } taskdefs = {} max_warning_lines = 2 WorkflowConfig._warn_if_queues_have_implicit_tasks( config, taskdefs, max_warning_lines) result = caplog.records[0].message assert "'foo' in queue 'q1'" in result assert "'bar' in queue 'q2'" in result assert "'baz'" not in result assert f"showing first {max_warning_lines}" in result @pytest.mark.parametrize( 'installed, run_dir, cylc_vars', [ pytest.param( False, # not installed (parsing a source dir) None, # no run directory passed to config object by scheduler { 'CYLC_WORKFLOW_NAME': True, # expected environment variables 'CYLC_WORKFLOW_ID': False, 'CYLC_WORKFLOW_RUN_DIR': False, 'CYLC_WORKFLOW_WORK_DIR': False, 'CYLC_WORKFLOW_SHARE_DIR': False, 'CYLC_WORKFLOW_LOG_DIR': False, }, id="source-dir" ), pytest.param( True, None, { 'CYLC_WORKFLOW_NAME': True, 'CYLC_WORKFLOW_ID': True, 'CYLC_WORKFLOW_RUN_DIR': True, 'CYLC_WORKFLOW_WORK_DIR': False, 'CYLC_WORKFLOW_SHARE_DIR': False, 'CYLC_WORKFLOW_LOG_DIR': False, }, id="run-dir" ), pytest.param( True, "/some/path", { 'CYLC_WORKFLOW_NAME': True, 'CYLC_WORKFLOW_ID': True, 'CYLC_WORKFLOW_RUN_DIR': True, 'CYLC_WORKFLOW_WORK_DIR': True, 'CYLC_WORKFLOW_SHARE_DIR': True, 'CYLC_WORKFLOW_LOG_DIR': True, }, id="run-dir-from-scheduler" ), ] ) def test_cylc_env_at_parsing( tmp_path: 'Path', monkeypatch: pytest.MonkeyPatch, installed, run_dir, cylc_vars ): """Check that CYLC_ environment vars exported during config file parsing are appropriate to the workflow context (source, installed, or running). """ # Purge environment from previous tests. for key in cylc_vars.keys(): with suppress(KeyError): del os.environ[key] flow_file = tmp_path / WorkflowFiles.FLOW_FILE flow_config = """ [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = 'foo' """ flow_file.write_text(flow_config) # Make it look as if path is relative to cylc-run (i.e. installed). monkeypatch.setattr( 'cylc.flow.config.is_relative_to', lambda _a, _b: installed ) # Parse the workflow config then check the environment. WorkflowConfig( workflow="name", fpath=flow_file, options=SimpleNamespace(), run_dir=run_dir ) cylc_env = [k for k in os.environ.keys() if k.startswith('CYLC_')] for var, expected in cylc_vars.items(): if expected: assert var in cylc_env else: assert var not in cylc_env def test_force_workflow_compat_mode(tmp_path): fpath = (tmp_path / 'flow.cylc') fpath.write_text(dedent(""" [scheduler] allow implicit tasks = true [scheduling] [[graph]] R1 = a:succeeded | a:failed => b """)) # It fails without compat mode: with pytest.raises(GraphParseError, match='Opposite outputs'): WorkflowConfig('foo', str(fpath), {}) # It succeeds with compat mode: WorkflowConfig('foo', str(fpath), {}, force_compat_mode=True) @pytest.mark.parametrize( 'registered_outputs, tasks_and_outputs, fails', ( param([], ['foo:x'], True, id='output-unregistered'), param([], ['foo:x?'], True, id='optional-output-unregistered'), param([], ['foo'], False, id='no-modifier-unregistered'), param(['x'], ['foo:x'], False, id='output-registered'), param([], ['foo:succeed'], False, id='alt-default-ok'), param([], ['foo:failed'], False, id='default-ok'), ) ) def test_check_outputs(tmp_path, registered_outputs, tasks_and_outputs, fails): (tmp_path / 'flow.cylc').write_text(dedent(""" [scheduler] allow implicit tasks = true [scheduling] [[graph]] R1 = foo """)) cfg = WorkflowConfig('', tmp_path / 'flow.cylc', '') cfg.cfg['runtime']['foo']['outputs'] = registered_outputs if fails: with pytest.raises( WorkflowConfigError, match='Undefined custom output' ): cfg.check_terminal_outputs(tasks_and_outputs) else: assert cfg.check_terminal_outputs(tasks_and_outputs) is None @pytest.mark.parametrize('back_compat', [True, False]) def test_upg_wflow_event_names(back_compat, tmp_flow_config, log_filter): """Cylc 7 workflow handler/mail event names are upgraded.""" flags.cylc7_back_compat = back_compat events = 'inactivity, abort, stalled' expected = ['inactivity timeout', 'abort', 'stall'] flow_file = tmp_flow_config('foo', f""" [scheduler] allow implicit tasks = true [[events]] handler events = {events} mail events = {events} [scheduling] [[graph]] R1 = foo """) cfg = WorkflowConfig('foo', str(flow_file), ValidateOptions()) for item in ('handler events', 'mail events'): assert cfg.cfg['scheduler']['events'][item] == expected if back_compat: assert not log_filter(logging.WARNING) else: assert log_filter( logging.WARNING, 'Deprecated config items were automatically upgraded', ) @pytest.mark.parametrize('item', ['handler events', 'mail events']) def test_val_wflow_event_names(item, tmp_flow_config, log_filter): """Any invalid workflow handler/mail events raise a warning.""" flow_file = tmp_flow_config('foo', f""" [scheduler] allow implicit tasks = true [[events]] {item} = abort, badger, stall, alpaca [scheduling] [[graph]] R1 = foo """) WorkflowConfig('foo', str(flow_file), ValidateOptions()) assert log_filter(contains=f"{item} = badger, alpaca") @pytest.mark.parametrize('item', ['handler events', 'mail events']) def test_check_task_event_names(item, tmp_flow_config, log_filter): """"Any invalid task handler events are warned about.""" flow_file = tmp_flow_config('foo', f""" [scheduling] [[graph]] R1 = foo [runtime] [[foo]] [[[events]]] {item} = submitted, late, warning, custom,\ execution timeout, badger, owl, retry, horse [[[outputs]]] owl = who """) WorkflowConfig('foo', str(flow_file), ValidateOptions()) assert log_filter(contains=( f"Invalid event name(s) for [runtime][foo][events]{item}: " "badger, horse" )) cylc-flow-8.6.4/tests/unit/test_task_pool.py0000664000175000017500000000475715202510242021347 0ustar alastairalastair# 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 . from typing import List from unittest.mock import Mock import pytest from cylc.flow.flow_mgr import FlowNums from cylc.flow.prerequisite import SatisfiedState from cylc.flow.task_pool import TaskPool @pytest.mark.parametrize('pool, db_fnums, expected', [ pytest.param( [{1, 2}, {2, 3}], {5, 6}, {1, 2, 3}, id="all-active" ), pytest.param( [set(), set()], {5, 6}, {5, 6}, id="from-db" ), pytest.param( [set()], set(), {1}, id="fallback" # see https://github.com/cylc/cylc-flow/pull/6445 ), ]) def test_get_active_flow_nums( pool: List[FlowNums], db_fnums: FlowNums, expected ): mock_task_pool = Mock( get_tasks=lambda: [Mock(flow_nums=fnums) for fnums in pool], ) mock_task_pool.workflow_db_mgr.pri_dao.select_latest_flow_nums = ( lambda: db_fnums ) assert TaskPool._get_active_flow_nums(mock_task_pool) == expected @pytest.mark.parametrize('output_msg, flow_nums, db_flow_nums, expected', [ ('foo', set(), {1}, False), ('foo', set(), set(), False), ('foo', {1, 3}, {1}, 'satisfied from database'), ('goo', {1, 3}, {1, 2}, 'satisfied from database'), ('foo', {1, 3}, set(), False), ('foo', {2}, {1}, False), ('foo', {2}, {1, 2}, 'satisfied from database'), ('f', {1}, {1}, False), ]) def test_check_output( output_msg: str, flow_nums: set, db_flow_nums: set, expected: SatisfiedState, ): mock_task_pool = Mock() mock_task_pool.workflow_db_mgr.pri_dao.select_task_outputs.return_value = { '{"f": "foo", "g": "goo"}': db_flow_nums, } assert TaskPool.check_task_output( mock_task_pool, '2000', 'haddock', output_msg, flow_nums ) == expected cylc-flow-8.6.4/tests/unit/test_pathutil.py0000664000175000017500000004505215202510242021177 0ustar alastairalastair# 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 . """Tests for "cylc.flow.pathutil".""" import logging import os from pathlib import Path import pytest from pytest import param from typing import Callable, Dict, Iterable, List, Set from unittest.mock import Mock, patch, call from cylc.flow.exceptions import InputError, WorkflowFilesError from cylc.flow.pathutil import ( EXPLICIT_RELATIVE_PATH_REGEX, expand_path, get_dirs_to_symlink, get_next_rundir_number, get_remote_workflow_run_dir, get_remote_workflow_run_job_dir, get_workflow_run_dir, get_workflow_run_job_dir, get_workflow_run_scheduler_log_dir, get_workflow_run_scheduler_log_path, get_workflow_run_pub_db_path, get_workflow_run_config_log_dir, get_workflow_run_share_dir, get_workflow_run_work_dir, get_workflow_test_log_path, is_relative_to, make_localhost_symlinks, make_workflow_run_tree, parse_rm_dirs, remove_dir_and_target, remove_dir_or_file, remove_empty_parents, get_workflow_name_from_id ) from .conftest import MonkeyMock HOME = Path.home() @pytest.mark.parametrize( 'string, match_expected', [ ('./foo/bar', True), ('../foo', True), ('./', True), ('../', True), ('.', True), ('..', True), ('foo/bar', False), ('.foo/bar', False), ('foo/..', False), ] ) def test_explicit_relative_path_regex(string: str, match_expected: bool): assert bool(EXPLICIT_RELATIVE_PATH_REGEX.match(string)) is match_expected @pytest.mark.parametrize( 'path, expected', [('~/moo', os.path.join(HOME, 'moo')), ('$HOME/moo', os.path.join(HOME, 'moo')), ('~/$FOO/moo', os.path.join(HOME, 'foo', 'bar', 'moo')), ('$NON_EXIST/moo', '$NON_EXIST/moo')] ) def test_expand_path( path: str, expected: str, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv('FOO', 'foo/bar') monkeypatch.delenv('NON_EXIST', raising=False) assert expand_path(path) == expected @pytest.mark.parametrize( 'func, extra_args, expected', [ (get_remote_workflow_run_dir, (), "$HOME/cylc-run/foo"), ( get_remote_workflow_run_dir, ("comes", "true"), "$HOME/cylc-run/foo/comes/true", ), ( get_remote_workflow_run_job_dir, (), "$HOME/cylc-run/foo/log/job"), ( get_remote_workflow_run_job_dir, ("comes", "true"), "$HOME/cylc-run/foo/log/job/comes/true", ) ] ) def test_get_remote_workflow_run_dirs( func: Callable, extra_args: Iterable[str], expected: str, ) -> None: """Tests for get_remote_workflow_run_[|job|work]_dir""" if extra_args: result = func('foo', *extra_args) else: result = func('foo') assert result == expected @pytest.mark.parametrize( 'func, tail1', [(get_workflow_run_dir, ''), (get_workflow_run_job_dir, '/log/job'), (get_workflow_run_scheduler_log_dir, '/log/scheduler'), (get_workflow_run_config_log_dir, '/log/config'), (get_workflow_run_share_dir, '/share'), (get_workflow_run_work_dir, '/work')] ) @pytest.mark.parametrize( 'args, tail2', [([], ''), (['comes', 'true'], '/comes/true')] ) def test_get_workflow_run_dirs( func: Callable, tail1: str, args: List[str], tail2: str ) -> None: """Usage of get_workflow_run_*dir. Params: func: get_remote_* function to test tail1: expected tail of return value from configuration args: extra *args tail2: expected tail of return value from extra args """ homedir = os.getenv("HOME") expected_result = f'{homedir}/cylc-run/my-workflow/dream{tail1}{tail2}' assert func('my-workflow/dream', *args) == expected_result @pytest.mark.parametrize( 'func, tail', [(get_workflow_run_scheduler_log_path, '/log/scheduler/log'), (get_workflow_run_pub_db_path, '/log/db'), (get_workflow_test_log_path, '/log/scheduler/reftest.log')] ) def test_get_workflow_run_names(func: Callable, tail: str) -> None: """Usage of get_workflow_run_*name. Params: func: get_remote_* function to test cfg: configuration used in mocked global configuration tail: expected tail of return value from configuration """ homedir = os.getenv("HOME") assert ( func('my-workflow/dream') == f'{homedir}/cylc-run/my-workflow/dream{tail}' ) def test_make_workflow_run_tree( tmp_run_dir: Callable, caplog: pytest.LogCaptureFixture ) -> None: run_dir: Path = tmp_run_dir('my-workflow') caplog.set_level(logging.DEBUG) # Only used for debugging test make_workflow_run_tree('my-workflow') # Check that directories have been created for subdir in [ '', 'log/scheduler', 'log/job', 'log/config', 'share', 'work' ]: assert (run_dir / subdir).is_dir() is True @pytest.mark.parametrize( 'mocked_glbl_cfg, output', [ pytest.param( # basic ''' [install] [[symlink dirs]] [[[the_matrix]]] run = $DEE work = $DAH log = $DUH log/job =$RAY share = $DOH share/cycle = $DAH ''', { 'run': '$DEE/cylc-run/morpheus', 'work': '$DAH/cylc-run/morpheus/work', 'log': '$DUH/cylc-run/morpheus/log', 'log/job': '$RAY/cylc-run/morpheus/log/job', 'share': '$DOH/cylc-run/morpheus/share', 'share/cycle': '$DAH/cylc-run/morpheus/share/cycle' }, id="basic" ), pytest.param( # remove nested run symlinks ''' [install] [[symlink dirs]] [[[the_matrix]]] run = $DEE work = $DAH log = $DEE share = $DOH share/cycle = $DAH ''', { 'run': '$DEE/cylc-run/morpheus', 'work': '$DAH/cylc-run/morpheus/work', 'share': '$DOH/cylc-run/morpheus/share', 'share/cycle': '$DAH/cylc-run/morpheus/share/cycle' }, id="remove nested run symlinks" ), pytest.param( # remove only nested run symlinks ''' [install] [[symlink dirs]] [[[the_matrix]]] run = $DOH log = $DEE share = $DEE ''', { 'run': '$DOH/cylc-run/morpheus', 'log': '$DEE/cylc-run/morpheus/log', 'share': '$DEE/cylc-run/morpheus/share' }, id="remove only nested run symlinks" ), pytest.param( # blank entries ''' [install] [[symlink dirs]] [[[the_matrix]]] run = log = "" share = work = " " ''', {}, id="blank entries" ) ] ) def test_get_dirs_to_symlink( mocked_glbl_cfg: str, output: Dict[str, str], mock_glbl_cfg: Callable, monkeypatch: pytest.MonkeyPatch ) -> None: # Set env var 'DEE', but we expect it to be unexpanded monkeypatch.setenv('DEE', 'poiuytrewq') mock_glbl_cfg('cylc.flow.pathutil.glbl_cfg', mocked_glbl_cfg) dirs = get_dirs_to_symlink('the_matrix', 'morpheus') assert dirs == output @patch('cylc.flow.pathutil.get_workflow_run_dir') @patch('cylc.flow.pathutil.get_dirs_to_symlink') def test_make_localhost_symlinks_calls_make_symlink_for_each_key_value_dir( mocked_dirs_to_symlink: Mock, mocked_get_workflow_run_dir: Mock, monkeypatch: pytest.MonkeyPatch, monkeymock: MonkeyMock ) -> None: mocked_dirs_to_symlink.return_value = { 'run': '$DOH/trinity', 'log': '$DEE/trinity/log', 'share': '$DEE/trinity/share' } mocked_get_workflow_run_dir.return_value = "rund" for v in ('DOH', 'DEE'): monkeypatch.setenv(v, 'expanded') mocked_make_symlink = monkeymock('cylc.flow.pathutil.make_symlink_dir') make_localhost_symlinks('rund', 'workflow') mocked_make_symlink.assert_has_calls([ call('rund', 'expanded/trinity'), call('rund/log', 'expanded/trinity/log'), call('rund/share', 'expanded/trinity/share') ]) @patch('cylc.flow.pathutil.get_workflow_run_dir') @patch('cylc.flow.pathutil.make_symlink_dir') @patch('cylc.flow.pathutil.get_dirs_to_symlink') def test_incorrect_environment_variables_raise_error( mocked_dirs_to_symlink, mocked_make_symlink, mocked_get_workflow_run_dir, monkeypatch: pytest.MonkeyPatch ): monkeypatch.delenv('doh', raising=False) mocked_dirs_to_symlink.return_value = { 'run': '$doh/cylc-run/test_workflow'} mocked_get_workflow_run_dir.return_value = "rund" with pytest.raises(WorkflowFilesError) as excinfo: make_localhost_symlinks('rund', 'test_workflow') assert ( "Can't symlink to $doh/cylc-run/test_workflow\n" "Undefined variables, check global config: $doh" ) in str(excinfo.value) @pytest.mark.parametrize( 'filetype, expected_err', [('dir', None), ('file', NotADirectoryError), (None, FileNotFoundError)] ) def test_remove_dir_and_target(filetype, expected_err, tmp_path): """Test that remove_dir_and_target() can delete nested dirs and handle bad paths.""" test_path = tmp_path.joinpath('foo/bar') if filetype == 'dir': # Test removal of sub directories too sub_dir = test_path.joinpath('baz') sub_dir.mkdir(parents=True) sub_dir_file = sub_dir.joinpath('meow') sub_dir_file.touch() elif filetype == 'file': test_path = tmp_path.joinpath('meow') test_path.touch() if expected_err: with pytest.raises(expected_err): remove_dir_and_target(test_path) else: remove_dir_and_target(test_path) assert test_path.exists() is False assert test_path.is_symlink() is False @pytest.mark.parametrize( 'target, expected_err', [('dir', None), ('file', NotADirectoryError), (None, None)] ) def test_remove_dir_and_target_symlinks(target, expected_err, tmp_path): """Test that remove_dir_and_target() can delete symlinks, including the target.""" target_path = tmp_path.joinpath('x/y') target_path.mkdir(parents=True) tmp_path.joinpath('a').mkdir() symlink_path = tmp_path.joinpath('a/b') if target == 'dir': # Add a file into the the target dir to check it removes that ok target_path.joinpath('meow').touch() symlink_path.symlink_to(target_path) elif target == 'file': target_path = target_path.joinpath('meow') target_path.touch() symlink_path.symlink_to(target_path) elif target is None: symlink_path.symlink_to(target_path) # Break symlink target_path.rmdir() if expected_err: with pytest.raises(expected_err): remove_dir_and_target(symlink_path) else: remove_dir_and_target(symlink_path) for path in [symlink_path, target_path]: assert path.exists() is False assert path.is_symlink() is False @pytest.mark.parametrize( 'func', [remove_dir_and_target, remove_dir_or_file] ) def test_remove_relative(func: Callable, tmp_path: Path): """Test that you cannot use remove_dir_and_target() or remove_dir_or_file() on relative paths. When removing a path, we want to be absolute-ly sure where it is! """ # cd to temp dir in case we accidentally succeed in deleting the path os.chdir(tmp_path) with pytest.raises(ValueError) as cm: func('foo/bar') assert 'Path must be absolute' in str(cm.value) def test_remove_dir_or_file(tmp_path: Path): """Test remove_dir_or_file()""" a_file = tmp_path.joinpath('fyle') a_file.touch() assert a_file.exists() remove_dir_or_file(a_file) assert a_file.exists() is False a_symlink = tmp_path.joinpath('simlynk') a_file.touch() a_symlink.symlink_to(a_file) assert a_symlink.is_symlink() remove_dir_or_file(a_symlink) assert a_symlink.is_symlink() is False assert a_file.exists() a_dir = tmp_path.joinpath('der') # Add contents to check whole tree is removed sub_dir = a_dir.joinpath('sub_der') sub_dir.mkdir(parents=True) sub_dir.joinpath('fyle').touch() assert a_dir.exists() remove_dir_or_file(a_dir) assert a_dir.exists() is False def test_remove_empty_parents(tmp_path: Path): """Test that _remove_empty_parents() doesn't remove parents containing a sibling.""" # -- Setup -- id_ = 'foo/bar/baz/qux' path = tmp_path.joinpath(id_) tmp_path.joinpath('foo/bar/baz').mkdir(parents=True) # Note qux does not exist, but that shouldn't matter sibling_reg = 'foo/darmok' sibling_path = tmp_path.joinpath(sibling_reg) sibling_path.mkdir() # -- Test -- remove_empty_parents(path, id_) assert tmp_path.joinpath('foo/bar').exists() is False assert tmp_path.joinpath('foo').exists() is True # Check it skips non-existent dirs, and stops at the right place too tmp_path.joinpath('foo/bar').mkdir() sibling_path.rmdir() remove_empty_parents(path, id_) assert tmp_path.joinpath('foo').exists() is False assert tmp_path.exists() is True @pytest.mark.parametrize( 'path, tail, exc_msg', [ pytest.param( 'meow/foo/darmok', 'foo/darmok', "path must be absolute", id="relative path" ), pytest.param( '/meow/foo/darmok', '/foo/darmok', "tail must not be an absolute path", id="absolute tail" ), pytest.param( '/meow/foo/darmok', 'foo/jalad', "path '/meow/foo/darmok' does not end with 'foo/jalad'", id="tail not in path" ) ] ) def test_remove_empty_parents_bad(path: str, tail: str, exc_msg: str): """Test that _remove_empty_parents() fails appropriately with bad args.""" with pytest.raises(ValueError) as exc: remove_empty_parents(path, tail) assert exc_msg in str(exc.value) @pytest.mark.parametrize( 'dirs, expected', [ ([" "], set()), (["foo", "bar"], {"foo", "bar"}), (["foo:bar", "baz/*"], {"foo", "bar", "baz/*"}), ([" :foo :bar:"], {"foo", "bar"}), (["foo/:bar//baz "], {"foo/", "bar/baz"}), ([".foo", "..bar", " ./gah"], {".foo", "..bar", "gah"}) # Note '..bar' is a valid filename (doesn't point to parent dir) ] ) def test_parse_rm_dirs(dirs: List[str], expected: Set[str]): """Test parse_dirs()""" assert parse_rm_dirs(dirs) == expected @pytest.mark.parametrize( 'dirs, err_msg', [ (["foo:/bar"], "--rm option cannot take absolute paths"), ([".."], "cannot take paths that point to the run directory or above"), (["foo:../bar"], "cannot take paths that point to the run directory or above"), (["foo:bar/../../gah"], "cannot take paths that point to the run directory or above"), ] ) def test_parse_rm_dirs__bad(dirs: List[str], err_msg: str): """Test parse_dirs() with bad inputs""" with pytest.raises(InputError) as exc: parse_rm_dirs(dirs) assert err_msg in str(exc.value) @pytest.mark.parametrize( 'expect, files, runN', [ param(1, [], False, id='1st run (from filenames)'), param(2, ['run1'], False, id='2nd run (from filenames)'), param( 1000, ['run20', 'run400', 'run999'], False, id='1000th run (from filenames)' ), param( 6, ['run1', 'run5'], False, id='Non-sequential (from filenames)'), param(2, ['run1'], True, id='2nd run (from symlink)'), param(100, ['run1', 'run99'], True, id='100th run (from symlink)'), param(42, ['foo', 'foo12', 'run41'], False, id='with dirs not runX') ] ) def test_get_next_rundir_number(tmp_path, expect, files, runN): for file_ in files: (tmp_path / file_).mkdir() if runN: (tmp_path / 'runN').symlink_to(tmp_path / files[-1]) assert get_next_rundir_number(tmp_path) == expect @pytest.mark.parametrize( 'name, id_, src', ( param('my_workflow1', 'my_workflow1', False, id='--no-run-name'), param('my_workflow2', 'my_workflow2/run22', False, id='installed'), param( 'my_workflow3', 'my_workflow3/foo', False, id='--run-name="foo"'), param('my_workflow4', 'my_workflow4', True, id='not installed'), ) ) def test_get_workflow_name_from_id( tmp_path, monkeypatch, name: str, id_: str, src: bool ) -> None: """It gets the correct name. args: name: Workflow name id: Workflow id src: Is this workflow a source or installed workflow. """ monkeypatch.setattr( 'cylc.flow.pathutil.get_cylc_run_dir', lambda: tmp_path) (tmp_path / name).mkdir(exist_ok=True) if not src: (tmp_path / name / '_cylc-install').mkdir(exist_ok=True) (tmp_path / id_).mkdir(exist_ok=True) result = get_workflow_name_from_id(id_) assert result == name @pytest.mark.parametrize('abs_path', [True, False]) @pytest.mark.parametrize('path1, path2, expected', [ param('/foo/bar/baz', '/foo/bar', True, id="child"), param('/foo/bar', '/foo/bar/baz', False, id="parent"), param('/foo/bar', '/foo/bar', True, id="same-path"), param('/cat/dog', '/hat/bog', False, id="different"), param('/a/b/c/../x/y', '/a/b/x', True, id="trickery"), ]) def test_is_relative_to(abs_path, path1, path2, expected): if not abs_path: # absolute & relative versions of same test path1.lstrip('/') path2.lstrip('/') assert is_relative_to(path1, path2) == expected cylc-flow-8.6.4/tests/unit/test_task_proxy.py0000664000175000017500000001116515202510242021546 0ustar alastairalastair# 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 . from typing import Callable, Optional from unittest.mock import Mock import pytest from pytest import param from cylc.flow.cycling import PointBase from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.flow_mgr import FlowNums from cylc.flow.id import Tokens from cylc.flow.task_proxy import TaskProxy from cylc.flow.taskdef import TaskDef @pytest.mark.parametrize( 'itask_point, offset_str, expected', [ param( # date -u -d 19700101 "+%s" ISO8601Point('19700101T00Z'), 'PT0M', 0, id="zero epoch" ), param( # 2025 is not a leap year: Jan 1 + P2M = P59D ISO8601Point('20250101T00Z'), 'PT0M', 1735689600, id="nonleap base" ), param( ISO8601Point('20250101T00Z'), 'P59D', 1740787200, id="nonleap off1" ), param( ISO8601Point('20250101T00Z'), 'P2M', 1740787200, id="nonleap off2" ), param( # 2024 is a leap year: Jan 1 + P2M = P60D ISO8601Point('20240101T00Z'), 'PT0M', 1704067200, id="leap base" ), param( ISO8601Point('20240101T00Z'), 'P60D', 1709251200, id="leap off1" ), param( ISO8601Point('20240101T00Z'), 'P2M', 1709251200, id="leap off2" ), ] ) def test_get_clock_trigger_time( itask_point: PointBase, offset_str: str, expected: int, set_cycling_type: Callable ) -> None: """Test get_clock_trigger_time() for exact and inexact offsets.""" set_cycling_type(itask_point.TYPE) mock_itask = Mock( point=itask_point.standardise(), clock_trigger_times={} ) assert TaskProxy.get_clock_trigger_time( mock_itask, mock_itask.point, offset_str) == expected @pytest.mark.parametrize( 'name_str, expected', [('beer', True), ('FAM', True), ('root', True), ('horse', False), ('F*', True), ('*', True)] ) def test_name_match(name_str: str, expected: bool): """Test TaskProxy.name_match(). For a task named "beer" in family "FAM". """ mock_tdef = Mock(namespace_hierarchy=['root', 'FAM', 'beer']) mock_tdef.name = 'beer' mock_itask = Mock(tdef=mock_tdef) assert TaskProxy.name_match(mock_itask, name_str) is expected @pytest.mark.parametrize( 'status_str, expected', [param('waiting', True, id="Basic"), param('w*', False, id="Globs don't work"), param(None, True, id="None always matches")] ) def test_status_match(status_str: Optional[str], expected: bool): """Test TaskProxy.status_match(). For a task with status "waiting". """ mock_itask = Mock(state=Mock(status='waiting')) assert TaskProxy.status_match(mock_itask, status_str) is expected @pytest.mark.parametrize('itask_flow_nums, flow_nums, expected', [ param({1, 2}, {2}, {2}, id="subset"), param({2}, {1, 2}, {2}, id="superset"), param({1, 2}, {3, 4}, set(), id="disjoint"), param({1, 2}, set(), {1, 2}, id="all-matches-num"), param(set(), {1, 2}, set(), id="num-doesnt-match-none"), param(set(), set(), set(), id="all-doesnt-match-none"), ]) def test_match_flows( itask_flow_nums: FlowNums, flow_nums: FlowNums, expected: FlowNums ): mock_itask = Mock(flow_nums=itask_flow_nums) assert TaskProxy.match_flows(mock_itask, flow_nums) == expected def test_match_flows_copy(): """Test that this method does not return the same reference as itask.flow_nums, otherwise you could end up unexpectedly mutating itask.flow_nums.""" mock_itask = Mock(flow_nums={1, 2}) result = TaskProxy.match_flows(mock_itask, set()) assert result == mock_itask.flow_nums assert result is not mock_itask.flow_nums def test_job_tokens(): itask = TaskProxy( Tokens('wflow'), TaskDef('foo', {}, None, None), IntegerPoint('10'), submit_num=3, ) assert str(itask.job_tokens) == 'wflow//10/foo/03' cylc-flow-8.6.4/tests/unit/option_parsers.py0000664000175000017500000000325615202510242021355 0ustar alastairalastair# 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 . import optparse import pytest from cylc.flow.option_parsers import Options @pytest.fixture def simple_parser(): """Simple option parser.""" parser = optparse.OptionParser() parser.add_option('-a', action='store') parser.add_option('-b', action='store_true') parser.add_option('-c', default='C') return parser def test_options(simple_parser): """It is a substitute for an optparse options object.""" options = Options(parser=simple_parser) opts = options(a=1, b=True) # we can access options as attributes assert opts.a == 1 assert opts.b is True # defaults are automatically substituted assert opts.c == 'C' # get-like syntax should work assert opts.get('d', 42) == 42 # invalid keys result in KeyErrors with pytest.raises(KeyError): opts.d with pytest.raises(KeyError): opts(d=1) # just for fun we can still use dict syntax assert opts['a'] == 1 cylc-flow-8.6.4/tests/unit/test_taskdef.py0000664000175000017500000001732715202510242020772 0ustar alastairalastair# 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 . import pytest from pytest import param from cylc.flow.config import WorkflowConfig from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.taskdef import generate_graph_parents def test_generate_graph_parents_1(tmp_flow_config): # noqa: F811 """Test that parents are only generated from valid recurrences.""" id_ = 'pan-galactic' flow_file = tmp_flow_config( id_, """ [scheduler] UTC mode = True [scheduling] initial cycle point = 2023 [[graph]] R1 = run_once_at_midnight T00 = run_once_at_midnight[-PT0H] => every_cycle T03 = run_once_at_midnight[-PT3H] => every_cycle T06 = run_once_at_midnight[-PT6H] => every_cycle [runtime] [[every_cycle, run_once_at_midnight]] """ ) cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) # Each instance of every_cycle should have a parent only at T00. for point in [ ISO8601Point('20230101T00'), ISO8601Point('20230101T03'), ISO8601Point('20230101T06') ]: parents = generate_graph_parents( cfg.taskdefs['every_cycle'], point, cfg.taskdefs ) assert list(parents.values()) == [ [ ( "run_once_at_midnight", ISO8601Point('20230101T0000Z'), False ) ] ] def test_generate_graph_parents_2(tmp_flow_config): # noqa: F811 """Test inferred parents are valid w.r.t to their own recurrences.""" id_ = 'gargle-blaster' flow_file = tmp_flow_config( id_, """ [scheduling] cycling mode = integer [[graph]] P1 = "foo[-P1] => foo" [runtime] [[foo]] """ ) cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) # Each instance of every_cycle should have a parent only at T00. parents = generate_graph_parents( cfg.taskdefs['foo'], IntegerPoint("1"), cfg.taskdefs ) assert list(parents.values()) == [[]] # No parents at first point. parents = generate_graph_parents( cfg.taskdefs['foo'], IntegerPoint("2"), cfg.taskdefs ) assert list(parents.values()) == [ [ ( "foo", IntegerPoint('1'), False ) ] ] @pytest.mark.parametrize( "task, point, expected", [ param( 'foo', IntegerPoint("1"), ['0/foo'], id='it.gets-prerequisites', ), param( 'multiple_pre', IntegerPoint("2"), ['2/food', '2/fool', '2/foolhardy', '2/foolish'], id='it.gets-multiple-prerequisites', ), param( 'foo', IntegerPoint("3"), [], id='it.only-returns-for-valid-points', ), param( 'bar', IntegerPoint("2"), [], id='it.does-not-return-suicide-prereqs', ), ], ) def test_get_prereqs(tmp_flow_config, task, point, expected): # noqa: F811 """Test that get_prereqs() returns the correct prerequisites for a task.""" id_ = 'gargle-blaster' flow_file = tmp_flow_config( id_, """ [scheduler] allow implicit tasks = True [scheduling] final cycle point = 2 cycling mode = integer [[graph]] P1 = ''' foo[-P1] => foo bar:fail? => !bar food & fool => multiple_pre foolish | foolhardy => multiple_pre ''' """ ) cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) taskdef = cfg.taskdefs[task] point = IntegerPoint(point) res = sorted([ condition.get_id() for pre in taskdef.get_prereqs(point) for condition in pre.keys() ]) assert res == expected def test_get_xtrigs(tmp_flow_config): id = 'foo' flow_file = tmp_flow_config( id, """ [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 1 final cycle point = 16 cycling mode = integer [[xtriggers]] xt_once = xrandom(1) xt_every = xrandom(1) xt_odd = xrandom(1) xt_final = xrandom(1) [[graph]] R1 = @xt_once => foo P1 = @xt_every => foo P2 = @xt_odd => foo R1/$ = @xt_final => foo """ ) cfg = WorkflowConfig(workflow=id, fpath=flow_file, options=None) taskdef = cfg.taskdefs['foo'] assert taskdef.get_xtrigs(IntegerPoint('1')) == { 'xt_once', 'xt_odd', 'xt_every' } assert taskdef.get_xtrigs(IntegerPoint('2')) == {'xt_every'} assert taskdef.get_xtrigs(IntegerPoint('3')) == {'xt_odd', 'xt_every'} assert taskdef.get_xtrigs(IntegerPoint('16')) == {'xt_final', 'xt_every'} @pytest.mark.parametrize( "task, point, expected", [ param( 'foo', IntegerPoint("1"), ['foo[-P1]:succeeded'], id='it.gets-triggers', ), param( 'multiple_pre', IntegerPoint("2"), ['food:succeeded', 'fool:succeeded', 'foolhardy:succeeded', 'foolish:succeeded'], id='it.gets-multiple-triggers', ), param( 'foo', IntegerPoint("3"), [], id='it.only-returns-triggers-for-valid-points', ), param( 'bar', IntegerPoint("2"), [], id='it.does-not-return-suicide-triggers', ), ], ) def test_get_triggers(tmp_flow_config, task, point, expected): # noqa: F811 """Test that get_triggers() returns the correct triggers for a task. """ id_ = 'gargle-blaster' flow_file = tmp_flow_config( id_, """ [scheduler] allow implicit tasks = True [scheduling] final cycle point = 2 cycling mode = integer [[graph]] P1 = ''' foo[-P1] => foo bar:fail? => !bar food & fool => multiple_pre foolish | foolhardy => multiple_pre ''' """ ) cfg = WorkflowConfig(workflow=id_, fpath=flow_file, options=None) taskdef = cfg.taskdefs[task] point = IntegerPoint(point) res = sorted([str(t) for t in taskdef.get_triggers(point)]) assert res == expected cylc-flow-8.6.4/tests/unit/parsec/0000775000175000017500000000000015202510242017203 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/parsec/__init__.py0000664000175000017500000000135715202510242021322 0ustar alastairalastair# 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 . cylc-flow-8.6.4/tests/unit/parsec/test_include.py0000664000175000017500000000747415202510242022253 0ustar alastairalastair# 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 . """Basic tests for the include module and its functions. Some functions rely on file system operations, and are probably better tested by functional tests. So this suite of unit tests should not cover all the module features. """ import os import tempfile import unittest from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.parsec.include import inline, IncludeFileNotFoundError class TestInclude(unittest.TestCase): def test_include_file_not_found_error(self): dir_temp = tempfile.mkdtemp() with tempfile.NamedTemporaryFile(dir=dir_temp) as tf: file_list = [tf.name, tf.name, tf.name] error = IncludeFileNotFoundError(file_list) self.assertTrue(" via " in str(error)) def test_inline_error_mismatched_quotes(self): """The inline function throws an error when you have the %include statement with a value without the correct balance for quotes, e.g. %include "abc.txt """ with self.assertRaises(ParsecError): inline( lines=["%include 'abc.txt"], dir_=None, filename=None, level=None) def test_inline(self): with tempfile.NamedTemporaryFile() as tf: filename = tf.name file_lines = [ "#!jinja2", "[section]", "value 1" ] file_lines_with_include = file_lines + [ "%include '{0}'".format(filename) ] # same as before as there was no include lines r = inline(lines=file_lines, dir_=os.path.dirname(tf.name), filename=filename) self.assertEqual(file_lines, r) # here the include line is removed, so the value returned # is still file_lines, not file_lines_with_include r = inline(lines=file_lines_with_include, dir_=os.path.dirname(tf.name), filename=filename) self.assertEqual(file_lines, r) # the for_grep adds some marks helpful for when grep'ing the file r = inline(lines=file_lines_with_include, dir_=os.path.dirname(tf.name), filename=filename, for_grep=True) expected = file_lines + [ "#++++ START INLINED INCLUDE FILE {0}".format(filename), "#++++ END INLINED INCLUDE FILE {0}".format(filename) ] self.assertEqual(expected, r) # for_edit would call the backup function, which triggers file # system operations. So we avoid testing that option here. # test that whatever is in the included file appears in the output tf.write("[section2]".encode()) tf.flush() r = inline(lines=file_lines_with_include, dir_=os.path.dirname(tf.name), filename=filename) expected = file_lines + [ "[section2]" ] self.assertEqual(expected, r) cylc-flow-8.6.4/tests/unit/parsec/test_config.py0000664000175000017500000002350015202510242022061 0ustar alastairalastair# 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 . from pathlib import Path import tempfile import pytest from cylc.flow.parsec.config import ( ConfigNode as Conf, ParsecConfig ) from cylc.flow.parsec.exceptions import ( IllegalItemError, InvalidConfigError, ItemNotFoundError, ) from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults from cylc.flow.parsec.upgrade import upgrader from cylc.flow.parsec.validate import ( cylc_config_validate, CylcConfigValidator as VDR ) def test_loadcfg(sample_spec): with tempfile.NamedTemporaryFile() as output_file_name: with tempfile.NamedTemporaryFile() as rcfile: parsec_config = ParsecConfig( spec=sample_spec, upgrader=None, # new spec output_fname=output_file_name.name, tvars=None, validator=None # use default ) rcfile.write(""" [section1] value1 = 'test' value2 = 'test' [section2] enabled = True [section3] title = 'Ohm' [[entries]] key = 'product' value = 1, 2, 3, 4 """.encode()) rcfile.seek(0) parsec_config.loadcfg(rcfile.name, "File test_loadcfg") sparse = parsec_config.sparse value = sparse['section3']['entries']['value'] assert [1, 2, 3, 4] == value # calling it multiple times should still work parsec_config.loadcfg(rcfile.name, "File test_loadcfg") parsec_config.loadcfg(rcfile.name, "File test_loadcfg") sparse = parsec_config.sparse value = sparse['section3']['entries']['value'] assert [1, 2, 3, 4] == value def test_loadcfg_override(sample_spec): """Test that loading a second config file overrides common settings but leaves in settings only present in the first""" with tempfile.NamedTemporaryFile() as output_file_name: parsec_config = ParsecConfig( spec=sample_spec, upgrader=None, output_fname=output_file_name.name, tvars=None, validator=None ) with tempfile.NamedTemporaryFile() as conf_file1: conf_file1.write(""" [section1] value1 = 'frodo' value2 = 'sam' """.encode()) conf_file1.seek(0) parsec_config.loadcfg(conf_file1.name, "File test_loadcfg") sparse = parsec_config.sparse assert sparse['section1']['value1'] == 'frodo' assert sparse['section1']['value2'] == 'sam' with tempfile.NamedTemporaryFile() as conf_file2: conf_file2.write(""" [section1] value2 = 'pippin' """.encode()) conf_file2.seek(0) parsec_config.loadcfg(conf_file2.name, "File test_loadcfg") sparse = parsec_config.sparse assert sparse['section1']['value1'] == 'frodo' assert sparse['section1']['value2'] == 'pippin' def test_loadcfg_with_upgrade(sample_spec): def upg(cfg, description): u = upgrader(cfg, description) u.obsolete('1.0', ['section3', 'entries']) u.upgrade() with tempfile.NamedTemporaryFile() as output_file_name: with tempfile.NamedTemporaryFile() as rcfile: parsec_config = ParsecConfig( spec=sample_spec, upgrader=upg, output_fname=output_file_name.name, tvars=None, validator=None # use default ) rcfile.write(""" [section1] value1 = 'test' value2 = 'test' [section2] enabled = True [section3] title = 'Ohm' [[entries]] key = 'product' value = 1, 2, 3, 4 """.encode()) rcfile.seek(0) parsec_config.loadcfg(rcfile.name, "1.1") sparse = parsec_config.sparse # removed by the obsolete upgrade assert 'entries' not in sparse['section3'] def test_validate(): """ An interesting aspect of the ParsecConfig.validate, is that if you have a sparse dict produced by this class, and you call the validate on that dict again, you may have TypeErrors. That occurs because the values like 'True' are validated against the spec and converted from Strings with quotes, to bool types. So the next type you run the validation if expects Strings... :return: """ with Conf('myconf') as spec: with Conf('section'): Conf('name', VDR.V_STRING) Conf('address', VDR.V_STRING) parsec_config = ParsecConfig( spec=spec, upgrader=None, # new spec output_fname=None, # not going to call the loadcfg tvars=None, validator=None # use default ) sparse = OrderedDictWithDefaults() parsec_config.validate(sparse) # empty dict is OK with pytest.raises(IllegalItemError): sparse = OrderedDictWithDefaults() sparse['name'] = 'True' parsec_config.validate(sparse) # name is not valid sparse = OrderedDictWithDefaults() sparse['section'] = OrderedDictWithDefaults() sparse['section']['name'] = 'Wind' sparse['section']['address'] = 'Corner' parsec_config.validate(sparse) @pytest.fixture def parsec_config_2(tmp_path: Path): with Conf('myconf') as spec: with Conf('section'): Conf('name', VDR.V_STRING) Conf('address', VDR.V_INTEGER_LIST) with Conf('allow_many'): Conf('', VDR.V_STRING, '') with Conf('so_many'): with Conf(''): Conf('color', VDR.V_STRING) Conf('horsepower', VDR.V_INTEGER) parsec_config = ParsecConfig(spec, validator=cylc_config_validate) conf_file = tmp_path / 'myconf' conf_file.write_text(""" [section] name = test [allow_many] anything = yup [so_many] [[legs]] horsepower = 123 """) parsec_config.loadcfg(conf_file, "1.0") return parsec_config def test_expand(parsec_config_2: ParsecConfig): parsec_config_2.expand() sparse = parsec_config_2.sparse assert sparse['allow_many']['anything'] == 'yup' assert '__MANY__' not in sparse['allow_many'] def test_get(parsec_config_2: ParsecConfig): cfg = parsec_config_2.get(keys=None, sparse=False) assert cfg == parsec_config_2.dense cfg = parsec_config_2.get(keys=None, sparse=True) assert cfg == parsec_config_2.sparse cfg = parsec_config_2.get(keys=['section'], sparse=True) assert cfg == parsec_config_2.sparse['section'] @pytest.mark.parametrize('keys, expected', [ (['section', 'name'], 'test'), (['section', 'a'], InvalidConfigError), (['alloy_many', 'anything'], InvalidConfigError), (['allow_many', 'anything'], 'yup'), (['allow_many', 'a'], ItemNotFoundError), (['so_many', 'legs', 'horsepower'], 123), (['so_many', 'legs', 'color'], ItemNotFoundError), (['so_many', 'legs', 'a'], InvalidConfigError), (['so_many', 'teeth', 'horsepower'], ItemNotFoundError), ]) def test_get__sparse(parsec_config_2: ParsecConfig, keys, expected): if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected): parsec_config_2.get(keys, sparse=True) else: assert parsec_config_2.get(keys, sparse=True) == expected def test_mdump_none(config, sample_spec, capsys): cfg = config(sample_spec, ''' [section1] value1 = abc value2 = def ''') cfg.mdump() std = capsys.readouterr() assert std.out == '' assert std.err == '' def test_mdump_some(config, sample_spec, capsys): cfg = config(sample_spec, ''' [section1] value1 = abc value2 = def ''') cfg.mdump( [ ['section1', 'value1'], ['section1', 'value2'], ] ) std = capsys.readouterr() assert std.out == 'abc\ndef\n' assert std.err == '' def test_mdump_oneline(config, sample_spec, capsys): cfg = config(sample_spec, ''' [section1] value1 = abc value2 = def ''') cfg.mdump( [ ['section1', 'value1'], ['section1', 'value2'], ], oneline=True ) std = capsys.readouterr() assert std.out == 'abc def\n' assert std.err == '' def test_get_none(config, sample_spec): cfg = config(sample_spec, '') # blank config assert cfg.get(sparse=True) == {} def test__get_namespace_parents(): """It returns a list of parents and nothing else""" with Conf('myconfig.cylc') as myconf: with Conf('a'): with Conf('b'): with Conf(''): with Conf('d'): Conf('') with Conf('x'): Conf('y') cfg = ParsecConfig(myconf) assert cfg.manyparents == [ ['a', 'b'], ['a', 'b', '__MANY__', 'd'], ] cylc-flow-8.6.4/tests/unit/parsec/test_types.py0000664000175000017500000001112215202510242021755 0ustar alastairalastair# 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 . """Test parsec type parsing. .. note:: This test is a pytest port of the old ``tests/parsec/synonyms`` test battery. """ import pytest from cylc.flow.parsec.config import ( ConfigNode as Conf ) from cylc.flow.parsec.validate import ( CylcConfigValidator as VDR ) @pytest.fixture def generate_spec(): def _inner(typ, validator): """Return a sample spec for the given data type. Args: typ (str): An arbirtrary name for this type. validator (str): A parsec validator. Returns: cylc.flow.parsec.config.ConfigNode """ with Conf('/') as myconf: with Conf(typ): Conf('', validator) return myconf return _inner @pytest.fixture def generate_config(): def _inner(typ, value): """Return a sample config for the given data type. The aim is to cover every facetious combination of quotes newlines and comments. Args: typ (str): An arbitrary name for this type. value (object): A stringable value to dump in the conf. For list types provide a list of stringable values. """ if isinstance(value, list): return f''' [{typ}] plain = {','.join(value)} # comment spaced = {', '.join(value)} # comment badly spaced = {' , '.join(value)} # comment single quoted = {', '.join((f"'{x}'" for x in value))} double quoted = {', '.join((f'"{x}"' for x in value))} multi line = {value[0]}, \\ {', '.join(value[1:])} # comment ''' else: return f''' [{typ}] plain1 = {value} # comment single quoted = '{value}' # comment double quoted = "{value}" # comment triple single quoted = \'\'\'{value}\'\'\' # comment triple double quoted = """{value}""" # comment triple single quoted multi = \'\'\' {value} \'\'\' # comment triple double quoted multi = """ {value} """ # comment ''' return _inner def test_types(generate_spec, generate_config, config): """Test type parsing. Test every facetious combination of: * Data types. * Quotation. * Inline-commenting. """ for typ, validator, string_repr, parsed_value in [ ('boolean', VDR.V_BOOLEAN, 'true', True), ('boolean', VDR.V_BOOLEAN, 'True', True), ('boolean', VDR.V_BOOLEAN, 'false', False), ('boolean', VDR.V_BOOLEAN, 'False', False), ('integer', VDR.V_INTEGER, '42', 42), ('float', VDR.V_FLOAT, '9.9', 9.9), ('string', VDR.V_STRING, 'the quick brown fox', 'the quick brown fox'), ( 'integer_list', VDR.V_INTEGER_LIST, ['1', '2', '3', '4', '5'], [1, 2, 3, 4, 5] ), ( 'float_list', VDR.V_FLOAT_LIST, ['1.1', '2.2', '3.3', '4.4', '5.5'], [1.1, 2.2, 3.3, 4.4, 5.5] ), ( 'string_list', VDR.V_STRING_LIST, ['be', 'ef', 'we', 'll', 'in', 'gt', 'on'], ['be', 'ef', 'we', 'll', 'in', 'gt', 'on'] ), ]: spec = generate_spec(typ, validator) conf = generate_config(typ, string_repr) cfg = config(spec, conf) assert all(( value == parsed_value for value in cfg.get()[typ].values() )) cylc-flow-8.6.4/tests/unit/parsec/conftest.py0000664000175000017500000000417715202510242021413 0ustar alastairalastair# 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 . import pytest from cylc.flow.parsec.config import ( ParsecConfig, ConfigNode as Conf ) from cylc.flow.parsec.validate import ( CylcConfigValidator as VDR ) @pytest.fixture def config(tmp_path): """Returns a function for parsing Parsec configurations.""" def _inner(spec, conf): """Parse conf against spec and return the result. Arguments: spec (cylc.flow.parsec.config.ConfigNode): The spec to parse the config against. conf (str): Multiline string containing the configuration. Returns: cylc.flow.parsec.ParsecConfig """ filepath = tmp_path / 'cfg.cylc' with open(filepath, 'w+') as filehandle: filehandle.write(conf) cfg = ParsecConfig(spec) cfg.loadcfg(filepath) return cfg return _inner @pytest.fixture def sample_spec(): """An example cylc.flow.parsec.config.ConfigNode.""" with Conf('myconf') as myconf: with Conf('section1'): Conf('value1', VDR.V_STRING, '') Conf('value2', VDR.V_STRING, 'what?') with Conf('section2'): Conf('enabled', VDR.V_BOOLEAN, False) with Conf('section3'): Conf('title', VDR.V_STRING) with Conf('entries'): Conf('key', VDR.V_STRING) Conf('value', VDR.V_INTEGER_LIST) return myconf cylc-flow-8.6.4/tests/unit/parsec/test_dict_tree.py0000664000175000017500000001101615202510242022555 0ustar alastairalastair# 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 . from cylc.flow.parsec.OrderedDict import ( DictTree, OrderedDictWithDefaults as ODD ) import pytest def test_eq(): """It compares to identical instances.""" assert DictTree({'a': 1}) == DictTree({'a': 1}) assert DictTree({'a': 1}) != DictTree({'a': 2}) assert DictTree({'a': 1}) != {'a': 1} def test_nesting(): """It slices without duplicating data.""" this_a = dict(b=1) this = dict(a=this_a) that_a = dict(b=2) that = dict(a=that_a) both = DictTree(this, that) sub = both['a'] assert sub._tree == (this_a, that_a) r_this_a, r_that_a = sub._tree assert id(r_this_a) == id(this_a) assert id(r_that_a) == id(that_a) def test_iter(): """It yields keys from all dictionaries.""" a = DictTree({'a': 1}, {'a': 2, 'b': 3}) assert list(sorted(a)) == ['a', 'b'] def test_iter_defaults(): """It doesn't yield keys from defaults.""" this = ODD(a=1) that = ODD(b=2) this.defaults = dict(c=3) that.defaults = dict(d=4) a = DictTree(this, that) assert list(sorted(a)) == ['a', 'b'] def test_getitem(): """It returns values prioritised correctly via the `getitem` interface.""" this = { 'a': 1, 'b': 2, 'f': { 'x': 5, 'y': 6 } } that = { 'b': 3, 'd': 4, 'f': { 'y': 7, 'z': 8 } } a = DictTree(this, that) # key from this assert a['a'] == 1 # key from that assert a['d'] == 4 # key from this overrides key from that assert a['b'] == 2 # dict from both f = a['f'] assert f._tree == (this['f'], that['f']) assert f['x'] == 5 # key from this assert f['y'] == 6 # key from this which is also in that assert f['z'] == 8 # key from that # mutate definition object this['f']['y'] = 42 assert a['f']['y'] == 42 def test_get(): """It returns values correctly via the `get` interface.""" a = DictTree({'a': 1}, {'b': 2}) # key exists in this assert a.get('a') == 1 assert a.get('a', 42) == 1 # key exists in that assert a.get('b') == 2 assert a.get('b', 42) == 2 # key does not exist assert a.get('e') is None assert a.get('e', 42) == 42 def test_get_dict(): """It slices correctly via the `get` interface.""" a = DictTree({'a': {'b': 1}}, {'a': {'b': 2}}) assert a.get('a') == DictTree({'b': 1}, {'b': 2}) def test_odd_getitem(): """It returns default values if none of the dicts contains the key.""" this = ODD(a=1) this.defaults_ = dict(c=3) that = ODD(b=2) that.defaults_ = dict(d=4) both = DictTree(this, that) # key from this assert both['a'] == 1 # key from that assert both['b'] == 2 # default from this assert both['c'] == 3 # default from that assert both['d'] == 4 # missing value with pytest.raises(KeyError): both['e'] def test_defaults_getitem_dict(): """It returns defaults if a value isn't set.""" this = ODD() that = ODD(a=2) both = DictTree(this, that) # default in this gets overridden by value in that this.defaults_ = dict(a=1) that.defaults_ = dict() assert both['a'] == 2 # default in first dict gets priority this.defaults_ = dict(b=1) that.defaults_ = dict(b=2) assert both['b'] == 1 def test_defaults_getitem_nested(): """It preserves the defaults_ behaviour of ODD in nested dicts.""" this = ODD() that = ODD(a=2) both = DictTree(dict(a=this), dict(a=that)) # default in this gets overridden by value in that this.defaults_ = dict(a=1) that.defaults_ = dict() assert both['a']['a'] == 2 # default in first dict gets priority this.defaults_ = dict(b=1) that.defaults_ = dict(b=2) assert both['a']['b'] == 1 cylc-flow-8.6.4/tests/unit/parsec/test_validate.py0000664000175000017500000005642215202510242022416 0ustar alastairalastair# 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 . """Unit Tests for cylc.flow.parsec.validate.ParsecValidator.coerce methods.""" import logging from typing import List from cylc.flow import CYLC_LOG import pytest from pytest import approx, param from cylc.flow.parsec.config import ConfigNode as Conf from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults from cylc.flow.parsec.exceptions import IllegalValueError from cylc.flow.parsec.validate import ( BroadcastConfigValidator, CylcConfigValidator as VDR, DurationFloat, ListValueError, IllegalItemError, ParsecValidator, parsec_validate ) @pytest.fixture def sample_spec(): with Conf('myconf') as myconf: with Conf('section1'): Conf('value1', default='') Conf('value2', default='what?') with Conf('section2'): Conf('enabled', VDR.V_BOOLEAN) with Conf('section3'): Conf('title', default='default', options=['1', '2']) Conf( 'amounts', VDR.V_INTEGER_LIST, default=[1, 2, 3], # options=[[1, 2, 3]] ) with Conf('entries'): Conf('key') Conf('value') with Conf(''): Conf('section300000', default='') Conf('ids', VDR.V_INTEGER_LIST) return myconf def validator_invalid_values(): """ Data provider or invalid values for parsec validator. All values must not be null (covered elsewhere), and not dict's. Possible invalid scenarios must include: - cfg[key] is a list AND a value is not in list of the possible values - OR - cfg[key] is not a list AND cfg[key] not in the list of possible values :return: a list with sets of tuples for the test parameters :rtype: list """ values = [] # variables reused throughout spec = None msg = None # set 1 (t, f, f, t) with Conf('base') as spec: Conf('value', VDR.V_INTEGER_LIST, default=1, options=[1, 2, 3, 4]) cfg = OrderedDictWithDefaults() cfg['value'] = "1, 2, 3" msg = None values.append((spec, cfg, msg)) # set 2 (t, t, f, t) with Conf('base') as spec: Conf('value', VDR.V_INTEGER_LIST, default=1, options=[1, 2, 3, 4]) cfg = OrderedDictWithDefaults() cfg['value'] = "1, 2, 5" msg = '(type=option) value = 5' values.append((spec, cfg, msg)) # set 3 (f, f, t, f) with Conf('base') as spec: Conf('value', VDR.V_INTEGER, default=1, options=[2, 3, 4]) cfg = OrderedDictWithDefaults() cfg['value'] = "2" msg = None values.append((spec, cfg, msg)) # set 4 (f, f, t, t) with Conf('base') as spec: Conf('value', VDR.V_INTEGER, default=1, options=[1, 2, 3, 4]) cfg = OrderedDictWithDefaults() cfg['value'] = "5" msg = '(type=option) value = 5' values.append((spec, cfg, msg)) return values @pytest.fixture def strip_and_unquote_list(): return [ [ '"a,b", c, "d e"', # input ["a,b", "c", "d e"] # expected ], [ 'foo bar baz', # input ["foo bar baz"] # expected ], [ '"a", \'b\', c', # input ["a", "b", "c"] # expected ], [ 'a b c, d e f', # input ["a b c", "d e f"] # expected ], ] def test_list_value_error(): keys = ['a,', 'b', 'c'] value = 'a sample value' error = ListValueError(keys, value, "who cares") output = str(error) expected = '(type=list) [a,][b]c = a sample value - (who cares)' assert expected == output def test_list_value_error_with_exception(): keys = ['a,', 'b', 'c'] value = 'a sample value' exc = Exception('test') error = ListValueError(keys, value, "who cares", exc) output = str(error) expected = '(type=list) [a,][b]c = a sample value - (test: who cares)' assert expected == output def test_illegal_value_error(): value_type = 'ClassA' keys = ['a,', 'b', 'c'] value = 'a sample value' error = IllegalValueError(value_type, keys, value) output = str(error) expected = "(type=ClassA) [a,][b]c = a sample value" assert expected == output def test_illegal_value_error_with_exception(): value_type = 'ClassA' keys = ['a,', 'b', 'c'] value = 'a sample value' exc = Exception('test') error = IllegalValueError(value_type, keys, value, exc) output = str(error) expected = "(type=ClassA) [a,][b]c = a sample value - (test)" assert expected == output def test_illegal_item_error(): keys = ['a,', 'b', 'c'] key = 'a sample value' error = IllegalItemError(keys, key) output = str(error) expected = "[a,][b][c]a sample value" assert expected == output def test_illegal_item_error_message(): keys = ['a,', 'b', 'c'] key = 'a sample value' message = "invalid" error = IllegalItemError(keys, key, message) output = str(error) expected = "[a,][b][c]a sample value - (invalid)" assert expected == output def test_parsec_validator_invalid_key(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section1'] = OrderedDictWithDefaults() cfg['section1']['value1'] = '1' cfg['section1']['value2'] = '2' cfg['section22'] = 'abc' with pytest.raises(IllegalItemError): parsec_validator.validate(cfg, sample_spec) def test_parsec_validator_invalid_key_no_spec(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section1'] = OrderedDictWithDefaults() cfg['section1']['value1'] = '1' cfg['section1']['value2'] = '2' cfg['section22'] = 'abc' # remove the user-defined section from the spec sample_spec._children = { key: value for key, value in sample_spec._children.items() if key != '__MANY__' } with pytest.raises(IllegalItemError): parsec_validator.validate(cfg, sample_spec) def test_parsec_validator_invalid_key_with_many_spaces(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section1'] = OrderedDictWithDefaults() cfg['section1']['value1'] = '1' cfg['section1']['value2'] = '2' cfg['section 3000000'] = 'test' with pytest.raises(IllegalItemError) as cm: parsec_validator.validate(cfg, sample_spec) assert str(cm.exception) == "section 3000000 - (consecutive spaces)" @pytest.mark.parametrize('spec, cfg, msg', validator_invalid_values()) def test_parsec_validator_invalid_key_with_many_invalid_values( spec, cfg, msg ): parsec_validator = ParsecValidator() if msg is not None: with pytest.raises(IllegalValueError) as cm: parsec_validator.validate(cfg, spec) assert msg == str(cm.value) else: # cylc.flow.parsec_validator.validate(cfg, spec) # let's use the alias `parsec_validate` here parsec_validate(cfg, spec) # TBD assertIsNotNone when 2.6+ assert parsec_validator is not None def test_parsec_validator_warn_options(caplog): """Test the "warn_options" option. This should turn invalid option errors into warnings. """ with Conf('base') as spec: Conf( 'foo', VDR.V_STRING_LIST, default=1, options=['a', 'b'], warn_options=True, ) parsec_validator = ParsecValidator() caplog.set_level(logging.WARNING, CYLC_LOG) parsec_validator.validate({'foo': 'b, c'}, spec) # there should be one (and only one) warning assert caplog.messages == [ '(type=option) foo = c\nInvalid items have been removed' ] def test_parsec_validator_invalid_key_with_many_1(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section1'] = OrderedDictWithDefaults() cfg['section1']['value1'] = '1' cfg['section1']['value2'] = '2' cfg['section3000000'] = OrderedDictWithDefaults() parsec_validator.validate(cfg, sample_spec) # TBD assertIsNotNone when 2.6+ assert parsec_validator is not None def test_parsec_validator_invalid_key_with_many_2(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section3'] = OrderedDictWithDefaults() cfg['section3']['title'] = '1' cfg['section3']['entries'] = OrderedDictWithDefaults() cfg['section3']['entries']['key'] = 'name' cfg['section3']['entries']['value'] = "1, 2, 3, 4" parsec_validator.validate(cfg, sample_spec) # TBD assertIsNotNone when 2.6+ assert parsec_validator is not None def test_parsec_validator(sample_spec): parsec_validator = ParsecValidator() cfg = OrderedDictWithDefaults() cfg['section1'] = OrderedDictWithDefaults() cfg['section1']['value1'] = '1' cfg['section1']['value2'] = '2' cfg['section3'] = OrderedDictWithDefaults() cfg['section3']['title'] = None parsec_validator.validate(cfg, sample_spec) # TBD assertIsNotNone when 2.6+ assert parsec_validator is not None # --- static methods def test_coerce_none_fails(): with pytest.raises(AttributeError): ParsecValidator.coerce_boolean(None, []) with pytest.raises(AttributeError): ParsecValidator.coerce_float(None, []) with pytest.raises(AttributeError): ParsecValidator.coerce_int(None, []) def test_coerce_boolean(): """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) ]: assert validator.coerce_boolean(value, ['whatever']) == result # The bad for value in [ 'None', ' Who cares? ', '3.14', '[]', '[True]', 'True, False' ]: with pytest.raises(IllegalValueError): validator.coerce_boolean(value, ['whatever']) @pytest.mark.parametrize( 'value, expected', [ ('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), ] ) def test_coerce_float(value: str, expected: float): """Test coerce_float.""" assert ( ParsecValidator.coerce_float(value, ['whatever']) == approx(expected) ) def test_coerce_float__empty(): # not a number assert ParsecValidator.coerce_float('', ['whatever']) is None @pytest.mark.parametrize( 'value', ['None', ' Who cares? ', 'True', '[]', '[3.14]', '3.14, 2.72'] ) def test_coerce_float__bad(value: str): with pytest.raises(IllegalValueError): ParsecValidator.coerce_float(value, ['whatever']) @pytest.mark.parametrize( 'value, expected', [ ('', []), ('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]), ] ) def test_coerce_float_list(value: str, expected: List[float]): """Test coerce_float_list.""" items = ParsecValidator.coerce_float_list(value, ['whatever']) assert items == approx(expected) @pytest.mark.parametrize( 'value', ['None', 'e, i, e, i, o', '[]', '[3.14]', 'pi, 2.72', '2*True'] ) def test_coerce_float_list__bad(value: str): with pytest.raises(IllegalValueError): ParsecValidator.coerce_float_list(value, ['whatever']) @pytest.mark.parametrize( 'value, expected', [ ('0', 0), ('3', 3), ('-3', -3), ('-0', -0), ('653456', 653456), ('-8362583645365', -8362583645365) ] ) def test_coerce_int(value: str, expected: int): """Test coerce_int.""" assert ParsecValidator.coerce_int(value, ['whatever']) == expected def test_coerce_int__empty(): assert ParsecValidator.coerce_int('', ['whatever']) is None # not a number @pytest.mark.parametrize( 'value', ['None', ' Who cares? ', 'True', '4.8', '[]', '[3]', '60*60'] ) def test_coerce_int__bad(value: str): with pytest.raises(IllegalValueError): ParsecValidator.coerce_int(value, ['whatever']) def test_coerce_int_list(): """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]), ('18 .. 24', [18, 19, 20, 21, 22, 23, 24]), ('18 .. 24 .. 3', [18, 21, 24]), ('-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]) ]: assert validator.coerce_int_list(value, ['whatever']) == results # The bad for value in [ 'None', 'e, i, e, i, o', '[]', '1..3, x', 'one..ten' ]: with pytest.raises(IllegalValueError): validator.coerce_int_list(value, ['whatever']) @pytest.mark.parametrize( 'value, expected', [ ('', ''), ('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'), (['a', 'b'], 'a\nb'), ('abc#def', 'abc'), ] ) def test_coerce_str(value: str, expected: str): """Test coerce_str.""" validator = ParsecValidator() # The good assert validator.coerce_str(value, ['whatever']) == expected def test_coerce_str_list(): """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']) ]: assert validator.coerce_str_list(value, ['whatever']) == results @pytest.mark.parametrize('value, expected', [ param( "'a'", 'a', id="single quotes" ), param( '"\'a\'"', "'a'", id="single quotes inside double quotes" ), param( '" a b" # comment', ' a b', id="comment outside" ), param( '"""bene\ngesserit"""', 'bene\ngesserit', id="multiline double quotes" ), param( "'''kwisatz\n haderach'''", 'kwisatz\n haderach', id="multiline single quotes" ), param( '"""a\nb""" # comment', 'a\nb', id="multiline with comment outside" ), ]) def test_unquote(value: str, expected: str): """Test strip_and_unquote.""" assert ParsecValidator._unquote(['a'], value) == expected @pytest.mark.parametrize('value', [ '"""', "'''", "'don't do this'", ]) def test_strip_and_unquote__bad(value: str): with pytest.raises(IllegalValueError): ParsecValidator.strip_and_unquote(['a'], value) def test_strip_and_unquote_list_parsec(): """Test strip_and_unquote_list using ParsecValidator.""" for value, results in [ ('"a"\n"b"', ['a', 'b']), ('"a", "b"', ['a', 'b']), ('"a", "b"', ['a', 'b']), ('"c" # d', ['c']), ('"a", "b", "c" # d', ['a', 'b', 'c']), ('"a"\n"b"\n"c" # d', ['a', 'b', 'c']), ("'a', 'b'", ['a', 'b']), ("'c' #d", ['c']), ("'a', 'b', 'c' # d", ['a', 'b', 'c']), ("'a'\n'b'\n'c' # d", ['a', 'b', 'c']), ('a, b, c,', ['a', 'b', 'c']), ('a, b, c # d', ['a', 'b', 'c']), ('a, b, c\n"d"', ['a', 'b', 'd']), ('a, b, c\n"d" # e', ['a', 'b', '"d"']) ]: assert results == ParsecValidator.strip_and_unquote_list( ['a'], value) def test_strip_and_unquote_list_cylc(strip_and_unquote_list): """Test strip_and_unquote_list using CylcConfigValidator.""" validator = VDR() for values in strip_and_unquote_list: value = values[0] expected = values[1] output = validator.strip_and_unquote_list(keys=[], value=value) assert expected == output def test_strip_and_unquote_list_multiparam(): with pytest.raises(ListValueError): ParsecValidator.strip_and_unquote_list( ['a'], 'a, b, c' ) def test_coerce_cycle_point(): """Test coerce_cycle_point.""" validator = VDR() # 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')]: assert validator.coerce_cycle_point(value, ['whatever']) == result # The bad for value in [ 'None', ' Who cares? ', 'True', '1, 2', '20781340E10']: with pytest.raises(IllegalValueError): validator.coerce_cycle_point(value, ['whatever']) def test_coerce_cycle_point_format(): """Test coerce_cycle_point_format.""" validator = VDR() # The good for value, result in [ ('', None), ('%Y%m%dT%H%M%z', '%Y%m%dT%H%M%z'), ('CCYYMMDDThhmmZ', 'CCYYMMDDThhmmZ'), ('XCCYYMMDDThhmmZ', 'XCCYYMMDDThhmmZ')]: assert ( validator.coerce_cycle_point_format(value, ['whatever']) == result ) # The bad # '/' and ':' not allowed in cylc cycle points (they are used in paths). for value in ['%i%j', 'Y/M/D', '%Y-%m-%dT%H:%MZ']: with pytest.raises(IllegalValueError): validator.coerce_cycle_point_format(value, ['whatever']) def test_coerce_cycle_point_time_zone(): """Test coerce_cycle_point_time_zone.""" validator = VDR() # The good for value, result in [ ('', None), ('Z', 'Z'), ('+0000', '+0000'), ('+0100', '+0100'), ('+1300', '+1300'), ('-0630', '-0630')]: assert ( validator.coerce_cycle_point_time_zone(value, ['whatever']) == result ) # The bad for value in ['None', 'Big Bang Time', 'Standard Galaxy Time']: with pytest.raises(IllegalValueError): validator.coerce_cycle_point_time_zone(value, ['whatever']) def test_coerce_interval(): """Test coerce_interval.""" validator = VDR() # The good for value, result in [ ('', None), ('P3D', DurationFloat(259200)), ('PT10M10S', DurationFloat(610))]: assert validator.coerce_interval(value, ['whatever']) == result # The bad for value in ['None', '5 days', '20', '-12']: with pytest.raises(IllegalValueError): validator.coerce_interval(value, ['whatever']) @pytest.mark.parametrize( 'value, expected', [ ('', []), ('P3D', [DurationFloat(259200)]), ('P3D, PT10M10S', [DurationFloat(259200), DurationFloat(610)]), ('25*PT30M,10*PT1H', [DurationFloat(1800)] * 25 + [DurationFloat(3600)] * 10) ] ) def test_coerce_interval_list(value: str, expected: List[DurationFloat]): """Test coerce_interval_list.""" assert VDR.coerce_interval_list(value, ['whatever']) == approx(expected) @pytest.mark.parametrize( 'value', ['None', '5 days', '20', 'PT10S, -12'] ) def test_coerce_interval_list__bad(value: str): with pytest.raises(IllegalValueError): VDR.coerce_interval_list(value, ['whatever']) def test_coerce_parameter_list(): """Test coerce_parameter_list.""" validator = VDR() # 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])]: assert validator.coerce_parameter_list(value, ['whatever']) == result # The bad for value in ['foo/bar', 'p1, 1..10', '2..3, 4, p', 'x:,']: with pytest.raises(IllegalValueError): validator.coerce_parameter_list(value, ['whatever']) def test_coerce_xtrigger(): """Test coerce_xtrigger.""" validator = VDR() # The good for value, result in [ ('foo(x="bar")', 'foo(x=bar)'), ('foo(x, y, z="zebra")', 'foo(x, y, z=zebra)')]: assert ( validator.coerce_xtrigger(value, ['whatever']).get_signature() == result ) # The bad for value in [ '', 'foo(', 'foo)', 'foo,bar']: with pytest.raises(IllegalValueError): validator.coerce_xtrigger(value, ['whatever']) def test_type_help_examples(): types = { **ParsecValidator.V_TYPE_HELP, **VDR.V_TYPE_HELP } validator = VDR() for vdr, info in types.items(): coercer = validator.coercers[vdr] if len(info) > 2: for example in info[2]: try: coercer(example, [None]) except Exception: raise Exception( f'Example "{example}" failed for type "{vdr}"' ) @pytest.mark.parametrize( 'value, expected', [ param( """ a="don't have a cow" a=${a#*have} echo "$a" # let's see what happens """, "a=\"don't have a cow\"\na=${a#*have}\necho \"$a\"" " # let's see what happens", id="multiline", ), param('"sleep 30 # ja!" ', 'sleep 30 # ja!', id="quoted"), ], ) def test_broadcast_coerce_str(value: str, expected: str): assert BroadcastConfigValidator.coerce_str(value, ['whatever']) == expected cylc-flow-8.6.4/tests/unit/parsec/test_fileparse_templating_clash.py0000664000175000017500000000334215202510242026166 0ustar alastairalastair# 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 . import pytest from cylc.flow.parsec import fileparse from cylc.flow.parsec.fileparse import read_and_proc from cylc.flow.parsec.exceptions import TemplateVarLanguageClash @pytest.mark.parametrize( 'templating, hashbang', [ ['other', 'jinja2'], ['jinja2', 'other'] ] ) def test_read_and_proc_raises_TemplateVarLanguageClash( monkeypatch, tmp_path, templating, hashbang ): """func fails when diffn't templating engines set in hashbang and plugin. """ def fake_process_plugins(_, __): extra_vars = { 'env': {}, 'template_variables': {'foo': 52}, 'templating_detected': templating } return extra_vars monkeypatch.setattr(fileparse, 'process_plugins', fake_process_plugins) file_ = tmp_path / 'flow.cylc' file_.write_text( f'#!{hashbang}\nfoo' ) with pytest.raises(TemplateVarLanguageClash) as exc: read_and_proc(file_) assert exc.type == TemplateVarLanguageClash cylc-flow-8.6.4/tests/unit/parsec/test_parsec.py0000664000175000017500000000231715202510242022074 0ustar alastairalastair# 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 . import unittest from cylc.flow.parsec.exceptions import ParsecError class TestParsec(unittest.TestCase): def test_parsec_error_msg(self): parsec_error = ParsecError() self.assertEqual('', str(parsec_error)) parsec_error = ParsecError('foo') self.assertEqual('foo', str(parsec_error)) def test_parsec_error_str(self): msg = 'Turbulence!' parsec_error = ParsecError(msg) self.assertEqual(msg, str(parsec_error)) cylc-flow-8.6.4/tests/unit/parsec/test_jinja2support.py0000664000175000017500000000726315202510242023436 0ustar alastairalastair# 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 . import jinja2 import pytest import sys from cylc.flow.parsec.jinja2support import ( Jinja2AssertionError, Jinja2Error, PyModuleLoader, jinja2environment, jinja2process, assert_helper, raise_helper, ) def test_raise_helper(): message = 'Ops' with pytest.raises(Jinja2AssertionError) as cm: raise_helper(message=message) assert str(cm.value) == "Ops" def test_assert_helper(): assert_helper(logical=True, message="Doesn't matter") # harmless with pytest.raises(Exception): assert_helper(logical=False, message="Doesn't matter") def test_jinja2environment(tmp_path): # create a temp directory, in the temp directory, to prevent # issues running multiple test workflows in parallel filters_dir = tmp_path / 'Jinja2Filters' filters_dir.mkdir() with open(filters_dir / "min.py", "w") as tf: tf.write("def min():\n raise ArithmeticError('UP!')") tf.seek(0) env = jinja2environment(tmp_path) # our jinja env contains the following keys in the global namespace assert 'environ' in env.globals assert 'raise' in env.globals assert 'assert' in env.globals with pytest.raises(ArithmeticError) as cm: # jinja2environment must have loaded the function from the .py env.filters['min']() assert str(cm.value) == 'UP!' def test_jinja2process(tmp_path): lines = ["skipped", "My name is {{ name }}", ""] variables = {'name': 'Cylc'} r = jinja2process(None, lines, tmp_path, variables) assert ['My name is Cylc'] == r def test_jinja2process_missing_variables(tmp_path): lines = ["skipped", "My name is {{ name }}", ""] with pytest.raises(Jinja2Error) as exc: jinja2process(None, lines, tmp_path, template_vars=None) assert 'jinja2.UndefinedError' in str(exc) def test_pymoduleloader(tmp_path): filters_dir = tmp_path / 'Jinja2filters' filters_dir.mkdir() with open(filters_dir / 'jinja2jinja.py', 'bw+') as tf: tf.write( "def jinja2jinja():\n raise Exception('It works!')".encode()) tf.seek(0) env = jinja2environment(tmp_path) module_loader = PyModuleLoader() template = module_loader.load(environment=env, name='sys') assert sys.path == template.module.path template2 = module_loader.load( environment=env, name='__python__.sys') assert template.module.path == template2.module.path def test_pymoduleloader_invalid_module(tmp_path): filters_dir = tmp_path / 'Jinja2filters' filters_dir.mkdir() with open(filters_dir / 'jinja2jinja.py', 'bw+') as tf: tf.write( "def jinja2jinja():\n raise Exception('It works!')".encode()) tf.seek(0) env = jinja2environment(tmp_path) module_loader = PyModuleLoader() with pytest.raises(jinja2.TemplateNotFound): module_loader.load(environment=env, name='no way jose') cylc-flow-8.6.4/tests/unit/parsec/test_config_node.py0000664000175000017500000001022015202510242023061 0ustar alastairalastair# 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 . import pytest from cylc.flow.parsec.config import ConfigNode as Conf @pytest.fixture(scope='module') def basic_config(): """A basic config with a file, section and setting.""" with Conf('file.cylc') as file_: with Conf('section') as section: setting = Conf('setting') return (file_, section, setting) def test_config_node(basic_config): """It should associate parents & children in a tree.""" file_, section, setting = basic_config assert file_.name == 'file.cylc' assert file_._parent is None assert file_._children == {'section': section} assert section.name == 'section' assert section._parent == file_ assert section._children == {'setting': setting} assert setting.name == 'setting' assert setting._parent == section assert setting._children is None def test_config_str(basic_config): """A node should str as a relative path from its parent node..""" file_, section, setting = basic_config assert str(file_) == 'file.cylc' assert str(section) == '[section]' assert str(setting) == 'setting' def test_config_repr(basic_config): """A node should repr as a full path.""" file_, section, setting = basic_config assert repr(file_) == 'file.cylc' assert repr(section) == 'file.cylc[section]' assert repr(setting) == 'file.cylc[section]setting' @pytest.fixture(scope='module') def many_setting(): """A config containing a user-definable setting.""" with Conf('file.cylc') as file_: Conf('') # __MANY__ return file_ def test_many_setting(many_setting): """It should recognise this is a user-definable setting.""" setting = list(many_setting)[0] assert setting.name == '__MANY__' assert setting.display_name == '' assert str(setting) == '' assert repr(setting) == 'file.cylc|' @pytest.fixture(scope='module') def many_section(): """A config containing a user-definable section.""" with Conf('file.cylc') as file_: with Conf('

'): Conf('setting') return file_ def test_many_section(many_section): """It should recognise this is a user-definable section.""" section = list(many_section)[0] assert section.name == '__MANY__' assert section.display_name == '
' assert str(section) == '[
]' assert repr(section) == 'file.cylc[
]' setting = list(section)[0] assert str(setting) == 'setting' assert repr(setting) == 'file.cylc[
]setting' @pytest.fixture(scope='module') def meta_conf(): """A config with an inherited section.""" with Conf('Foo') as spec: with Conf('') as template: Conf('a', default='a') Conf('b', default='b') with Conf('y', meta=template) as copy: Conf('a', default='c') return spec, template, copy def test_meta(meta_conf): """It should inherit sections using the meta kwarg.""" spec, template, copy = meta_conf assert template.meta is None assert copy.meta == template # make sure the template is unaffected assert template['a'].default == 'a' assert template['b'].default == 'b' # make sure the copy is affected assert copy['a'].default == 'c' assert copy['b'].default == 'b' # make sure inherited configurations are marked accordingly assert copy['a'].meta is None # not inherited assert copy['b'].meta is True # inherited cylc-flow-8.6.4/tests/unit/parsec/test_ordered_dict.py0000664000175000017500000001064415202510242023250 0ustar alastairalastair# 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 . import unittest from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults class TestOrderedDict(unittest.TestCase): def test_getitem(self): d = OrderedDictWithDefaults() d['name'] = 'Joseph' d.defaults_ = { 'surname': 'Wyndham' } self.assertEqual('Joseph', d['name']) self.assertEqual('Wyndham', d['surname']) def test_setitem(self): d = OrderedDictWithDefaults() d['name'] = 'Matthews' self.assertEqual('Matthews', d['name']) d['name'] = 'Zaccharias' self.assertEqual('Zaccharias', d['name']) def test_keys(self): d = OrderedDictWithDefaults() d['name'] = 'Andrew' d['surname'] = 'Gray' d.defaults_ = { 'address': 'N/A' } keys = list(d.keys()) self.assertTrue(len(keys) == 3) self.assertTrue('name' in keys) self.assertTrue('surname' in keys) self.assertTrue('address' in keys) def test_values(self): d = OrderedDictWithDefaults() d['name'] = 'Paul' d['color'] = 'Green' values = list(d.values()) self.assertTrue(len(values) == 2) self.assertTrue('Paul' in values) self.assertTrue('Green' in values) def test_items(self): d = OrderedDictWithDefaults() self.assertEqual([], list(d.items())) d['key'] = 'Birds' d['len'] = '89' for _, v in list(d.items()): self.assertTrue(v in ['Birds', '89']) def test_iterkeys(self): d = OrderedDictWithDefaults() self.assertEqual([], list(d.items())) d['key'] = 'Birds' d['len'] = '89' d.defaults_ = { 'surname': 'Wyndham' } count = 0 for k in d.keys(): self.assertTrue(k in ['key', 'len', 'surname']) count += 1 self.assertEqual(3, count) def test_itervalues(self): d = OrderedDictWithDefaults() self.assertEqual([], list(d.items())) d['key'] = 'Birds' d['len'] = '89' d.defaults_ = { 'surname': 'Wyndham' } count = 0 for k in d.values(): self.assertTrue(k in ['Birds', '89', 'Wyndham']) count += 1 self.assertEqual(3, count) def test_iteritems(self): d = OrderedDictWithDefaults() self.assertEqual([], list(d.items())) d['key'] = 'Birds' d['len'] = '89' d.defaults_ = { 'surname': 'Wyndham' } count = 0 for k, v in d.items(): self.assertTrue(k in ['key', 'len', 'surname']) self.assertTrue(v in ['Birds', '89', 'Wyndham']) count += 1 self.assertEqual(3, count) def test_contains(self): d = OrderedDictWithDefaults() self.assertEqual([], list(d.items())) d['key'] = 'Birds' d.defaults_ = { 'value': '10' } self.assertTrue('key' in d) self.assertTrue('value' in d) self.assertFalse('test' in d) def test_nonzero(self): d = OrderedDictWithDefaults() self.assertFalse(d) d['value'] = 10 self.assertTrue(d) def test_prepend(self): d = OrderedDictWithDefaults() d['key'] = 'Birds' d.prepend('year', 1980) d.prepend('key', 2000) iterator = iter(d.keys()) self.assertEqual('key', next(iterator)) self.assertEqual('year', next(iterator)) d = OrderedDictWithDefaults() d['key'] = 'Birds' d.prepend('year', 1980) iterator = iter(d.keys()) self.assertEqual('year', next(iterator)) self.assertEqual('key', next(iterator)) cylc-flow-8.6.4/tests/unit/parsec/test_upgrade.py0000664000175000017500000002144515202510242022251 0ustar alastairalastair# 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 . import unittest import pytest from cylc.flow.parsec.upgrade import upgrader, converter from cylc.flow.parsec.exceptions import UpgradeError from cylc.flow.parsec.OrderedDict import OrderedDict def test_simple(): """A quick test of overall functionality.""" cfg = { 'item one': 1, 'item two': 'move me up', 'section A': { 'abc': 5, 'cde': 'foo', 'gah': 'bar' }, 'hostnames': { 'host 1': { 'work dir': '/a/b/c', 'running dir': '/a/b/c/d' }, 'host 2': { 'work dir': '/x/b/c', 'running dir': '/x/b/c/d' }, } } x2 = converter(lambda x: 2 * x, 'value x 2') upg = upgrader(cfg, 'test file') # successive upgrades are incremental - at least until I think of a # good way to remember what items have already been translated... upg.deprecate('1.3', ['item one'], ['item ONE'], x2) upg.deprecate('1.3', ['section A'], ['Heading A']) # NOTE change to new item keys here! upg.deprecate('1.3', ['Heading A', 'cde'], ['Heading A', 'CDE']) upg.deprecate( '1.4', ['Heading A', 'abc'], ['Heading A', 'abc'], cvtr=x2, silent=True) upg.deprecate( '1.4.1', ['item two'], ['Heading A', 'item two'], silent=True) upg.deprecate('1.5', ['hostnames'], ['hosts']) upg.deprecate( '1.5', ['hosts', '__MANY__', 'running dir'], ['hosts', '__MANY__', 'run dir']) # obsolete() but with a custom message - `[Heading A]gah` will be deleted: upg.deprecate( '1.3', ['Heading A', 'gah'], None, cvtr=converter(lambda x: x, 'Yaba daba do')) upg.upgrade() assert cfg == { 'item ONE': 2, 'Heading A': { 'CDE': 'foo', 'abc': 10, 'item two': 'move me up' }, 'hosts': { 'host 1': { 'work dir': '/a/b/c', 'run dir': '/a/b/c/d' }, 'host 2': { 'work dir': '/x/b/c', 'run dir': '/x/b/c/d' } } } def test_conflicting_items(): cfg = { 'item one': 1, 'item two': 2, } def get_upgrader(): upg = upgrader(cfg, 'test file') upg.deprecate('1.3', ['item one'], ['item two']) return upg # specifying both the old and new variants of a config should result in an # error upg = get_upgrader() with pytest.raises(UpgradeError): upg.upgrade() # unless the new config is unset cfg['item two'] = None upg = get_upgrader() upg.upgrade() class TestUpgrade(unittest.TestCase): def setUp(self): self.cfg = OrderedDict() self.cfg['section'] = OrderedDict() self.cfg['section']['a'] = '1' self.cfg['section']['b'] = '2' self.u = upgrader(self.cfg, "1.0 to 2.0") def test_converter(self): def callback(i): return 1 + i c = converter(callback=callback, descr="My callback") self.assertEqual("My callback", c.describe()) self.assertEqual(2, c.convert(1)) def test_constructor(self): self.assertEqual(self.cfg, self.u.cfg) self.assertEqual("1.0 to 2.0", self.u.descr) def test_deprecate(self): # b is being deprecated; use c instead self.u.deprecate('entry', ['section', 'b'], ['section', 'c']) self.assertTrue('entry' in self.u.upgrades) def callback(i): return 10 * i c = converter(callback=callback, descr="My callback") self.u.deprecate(vn='entry', oldkeys=[1, 2, 3], newkeys=[10, 20, 30], cvtr=c) # assert the key exists before deprecation self.assertTrue('b' in self.cfg['section']) self.assertFalse('c' in self.cfg['section']) self.u.upgrade() # assert the key exists before deprecation self.assertFalse('b' in self.cfg['section']) self.assertTrue('c' in self.cfg['section']) def test_obsolete(self): # b is obsolete, so the value is omitted self.u.obsolete(vn='entry', oldkeys=['section', 'b'], silent=True) self.assertTrue('entry' in self.u.upgrades) self.assertEqual(True, self.u.upgrades['entry'][0]['silent']) self.assertTrue('b' in self.cfg['section']) self.u.upgrade() self.assertFalse('b' in self.cfg['section']) self.u.obsolete( vn='entry', oldkeys=['section', 'b'], silent=False) self.assertEqual(True, self.u.upgrades['entry'][0]['silent']) self.assertEqual(False, self.u.upgrades['entry'][1]['silent']) self.u.obsolete(vn='whocalled?', oldkeys=['section', 'b'], silent=True) def test_get_item(self): for keys, results in [ (['section', 'a'], '1'), (['section', 'b'], '2'), (['section'], {'a': '1', 'b': '2'}) ]: item = self.u.get_item(keys) self.assertEqual(results, item) def test_put_item(self): for keys, value in [ (['section', 'a'], '100'), (['section', 'b'], '200'), (['special', 'c'], '3'), ]: self.u.put_item(keys, value) self.assertEqual(self.u.get_item(keys), value) def test_expand_not_many(self): upg = { 'new': None, 'cvt': None, 'silent': True, 'is_section': False, 'old': [ ] } self.assertEqual([upg], self.u.expand(upg)) def test_expand_too_many(self): upg = { 'new': None, 'cvt': None, 'silent': True, 'is_section': True, 'old': [ 'section', '__MANY__', '__MANY__' ] } with self.assertRaises(UpgradeError) as cm: self.u.expand(upg) self.assertTrue('Multiple simultaneous __MANY__ not supported' in str(cm.exception)) def test_expand_deprecate_many_mismatch(self): upg = { 'new': [ 'section', '__MANY__' ], 'cvt': None, 'silent': True, 'is_section': False, 'old': [ 'section', '__MANY__', 'b' ] } with self.assertRaises(UpgradeError) as cm: self.u.expand(upg) self.assertEqual('__MANY__ mismatch', str(cm.exception)) def test_expand_deprecate(self): def callback(i): return i c = converter(callback=callback, descr="My callback") upg = { 'new': [ 'section', '__MANY__', 'e' ], 'cvt': c, 'silent': True, 'is_section': False, 'old': [ 'section', '__MANY__', 'c' ] } self.u.upgrade() expanded = self.u.expand(upg) self.assertEqual(2, len(expanded)) self.assertEqual(['section', 'a', 'e'], expanded[0]['new']) def test_expand_obsolete(self): upg = { 'new': None, 'cvt': None, 'silent': True, 'is_section': False, 'old': [ 'section', '__MANY__', 'a' ] } self.cfg['__MANY__'] = OrderedDict() self.cfg['__MANY__']['name'] = 'Arthur' self.u.obsolete('entry', ['section', '__MANY__']) self.u.upgrade() expanded = self.u.expand(upg) self.assertEqual(1, len(expanded)) self.assertTrue(expanded[0]['new'] is None) def test_template_in_converter_description(caplog, capsys): """Before and after values are available to the conversion descriptor""" cfg = {'old': 42} u = upgrader(cfg, 'Whateva') u.deprecate( '2.0.0', ['old'], ['new'], cvtr=converter(lambda x: x + 20, '{old} -> {new}'), silent=False, ) u.upgrade() assert cfg == {'new': 62} assert '42 -> 62' in caplog.records[1].message cylc-flow-8.6.4/tests/unit/parsec/test_util.py0000664000175000017500000003663615202510242021607 0ustar alastairalastair# 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 . from io import StringIO import pytest from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults from cylc.flow.parsec.util import ( SECTION_EXPAND_PATTERN, expand_many_section, itemstr, listjoin, m_override, pdeepcopy, poverride, printcfg, replicate, un_many, ) def test_listjoin(): assert listjoin(None) == '' assert listjoin(None, 'test') == 'test' assert listjoin([], 'test') == 'test' assert listjoin([None], 'test') == 'test' assert listjoin(['test', 'test']) == 'test, test' assert listjoin(['test,', 'test']) == '\'test,\', test' # --- printcfg def test_printcfg(): cfg = OrderedDictWithDefaults() cfg['root'] = OrderedDictWithDefaults() cfg['root']['special'] = 1 cfg['root']['normal'] = 0 cfg['root'][None] = None cfg[None] = None myhandle = StringIO() printcfg(cfg, handle=myhandle) expected = "\n[root]\n special = 1\n normal = 0\n \n" actual = myhandle.getvalue() assert actual == expected def test_printcfg_none_str_is_none(): cfg = OrderedDictWithDefaults() cfg['root'] = OrderedDictWithDefaults() cfg['root']['special'] = 1 cfg['root']['normal'] = 0 cfg['root'][None] = None cfg[None] = None myhandle = StringIO() printcfg(cfg, handle=myhandle, none_str=None) expected = "[root]\n special = 1\n normal = 0\n" actual = myhandle.getvalue() assert actual == expected def test_printcfg_list_values(): cfg = OrderedDictWithDefaults() cfg['root'] = OrderedDictWithDefaults() cfg['root']['special'] = ['a', 'b', 'c', None] cfg['root']['normal'] = 0 myhandle = StringIO() printcfg(cfg, handle=myhandle, none_str='d') expected = "[root]\n special = a, b, c, d\n normal = 0\n" actual = myhandle.getvalue() assert actual == expected def test_printcfg_break_lines(): cfg = OrderedDictWithDefaults() cfg['root'] = OrderedDictWithDefaults() cfg['root']['special'] = "\nthis is\nvalid" cfg['root']['normal'] = 0 myhandle = StringIO() printcfg(cfg, handle=myhandle) expected = ( "[root]\n special = \"\"\"\n \n " " this is\n valid\n \"\"\"\n normal = 0\n" ) actual = myhandle.getvalue() assert actual == expected # --- replicate def test_replicate(): replicate('Name', None) # does nothing, no exception/error source_1 = OrderedDictWithDefaults() source_1["name"] = "sea" source_1["origin"] = "kitchen" source_1.defaults_ = {"brewery": False} source_2 = OrderedDictWithDefaults() source_2["name"] = ["sea", "legume"] source_2["origin"] = "fridge" source_3 = OrderedDictWithDefaults() source_3["name"] = OrderedDictWithDefaults() source_3["name"]["value"] = "oil" source_3["name"]["key"] = 1 source_3["name"].defaults_ = {"value": 1} target_1 = OrderedDictWithDefaults() target_2 = OrderedDictWithDefaults() target_3 = OrderedDictWithDefaults() replicate(target_1, source_1) replicate(target_2, source_2) replicate(target_3, source_3) # Note: assertDictEqual not available for Python 2.6 assert str(target_1) == str(source_1) assert str(target_2) == str(source_2) assert str(target_3) == str(source_3) # --- pdeepcopy def test_pdeepcopy(): """This is tested entirely by the tests in replicate as well""" source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["value"] = "oil" source["name"]["key"] = 1 source["name"].defaults_ = {"value": 1} target = pdeepcopy(source) assert target == source # --- poverride def test_poverride_append(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["value"] = "oil" source["name"]["key"] = [1, 2, 3, 4] target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 poverride(target, None) # harmless, and no error/exception poverride(target, source) expected = OrderedDictWithDefaults() expected["index"] = 0 expected["value"] = "oil" expected["key"] = [1, 2, 3, 4] assert target["name"] == expected def test_poverride_prepend(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["value"] = "oil" source["name"]["key"] = [1, 2, 3, 4] target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 poverride(target, None) # harmless, and no error/exception poverride(target, source, prepend=True) expected = OrderedDictWithDefaults() expected["key"] = [1, 2, 3, 4] expected["value"] = "oil" expected["index"] = 0 assert target["name"] == expected # -- m_override def test_m_override(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["index"] = "oil" source["name2"] = OrderedDictWithDefaults() source["name2"]["key"] = [1, 2, 3, 4] source["name2"]["text"] = "" source["name2"]["subdict"] = OrderedDictWithDefaults() source["name2"]["subdict"]['family'] = 'ALL' target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() target['__MANY__']['subdict'] = OrderedDictWithDefaults() target['__MANY__']['subdict']['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['subdict']['__MANY__']['family'] = 'LATIN' target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" assert target["name"]["index"] == 0 m_override(target, source) assert target["name"]["index"] == "oil" def test_m_override_many_with_many(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["index"] = "oil" source["name2"] = OrderedDictWithDefaults() source["name2"]["key"] = [1, 2, 3, 4] source["name2"]["text"] = "" source["name2"]["subdict"] = OrderedDictWithDefaults() source["name2"]["subdict"]['family'] = 'ALL' target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() target['__MANY__']['subdict'] = OrderedDictWithDefaults() target['__MANY__']['subdict']['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['subdict']['__MANY__']['family'] = 'LATIN' target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" # code is OK until here # It appears this is valid for now, but may change later target['__MANY__']['__MANY__'] = OrderedDictWithDefaults() with pytest.raises(Exception): m_override(target, source) def test_m_override_without_many_1(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["value"] = "oil" source["name"]["key"] = [1, 2, 3, 4] target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 with pytest.raises(Exception): m_override(target, source) def test_m_override_without_many_2(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["index"] = "oil" source["name2"] = OrderedDictWithDefaults() source["name2"]["key"] = [1, 2, 3, 4] target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() # target['__MANY__']['key'] = [] with pytest.raises(Exception): m_override(target, source) def test_m_override_without_many_3(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["index"] = "oil" source["name2"] = OrderedDictWithDefaults() source["name2"]["key"] = [1, 2, 3, 4] source["name2"]["text"] = "" source["name2"]["subdict"] = OrderedDictWithDefaults() source["name2"]["subdict"]['family'] = 'ALL' target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() target['__MANY__']['subdict'] = OrderedDictWithDefaults() target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" with pytest.raises(Exception): m_override(target, source) def test_m_override_without_many_4(): source = OrderedDictWithDefaults() source["name"] = OrderedDictWithDefaults() source["name"]["index"] = "oil" source["name2"] = OrderedDictWithDefaults() source["name2"]["key"] = [1, 2, 3, 4] source["name2"]["text"] = "" source["name2"]["subdict"] = OrderedDictWithDefaults() source["name2"]["subdict"]['family'] = 'ALL' target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" with pytest.raises(Exception): m_override(target, source) # --- un_many def test_un_many(): target = OrderedDictWithDefaults() target["name"] = OrderedDictWithDefaults() target["name"]["index"] = 0 target['__MANY__'] = OrderedDictWithDefaults() target['__MANY__']['name2'] = OrderedDictWithDefaults() target['__MANY__']['subdict'] = OrderedDictWithDefaults() target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" un_many(None) # harmless, no error/exception un_many(target) assert '__MANY__' not in list(target) def test_un_many_keyerror(): """ Only way that this may happen is if dict is updated elsewhere. """ class MyODWD(OrderedDictWithDefaults): def __delitem__(self, _): raise KeyError() target = MyODWD() target["name"] = "Anything" target.defaults_ = {} target.defaults_["name"] = True target.defaults_["__MANY__"] = True target['__MANY__'] = MyODWD() target['__MANY__']['name2'] = MyODWD() target['__MANY__']['subdict'] = MyODWD() target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" un_many(None) # harmless, no error/exception un_many(target) assert target assert '__MANY__' in target def test_un_many_keyerror_no_default(): """ Only way that this may happen is if dict is updated elsewhere. And in this case, when there is no defaults_, the API raises the current KeyError. """ class MyODWD(OrderedDictWithDefaults): def __delitem__(self, _): raise KeyError() target = MyODWD() target["name"] = "Anything" target['__MANY__'] = MyODWD() target['__MANY__']['name2'] = MyODWD() target['__MANY__']['subdict'] = MyODWD() target['__MANY__']['key'] = [] target['__MANY__']['text'] = "Ad infinitum" un_many(None) # harmless, no error/exception with pytest.raises(KeyError): un_many(target) # --- itemstr def test_itemstr(): parents = ["parent1", "parent2"] text = itemstr(parents=parents, item="Value", value="Anything") assert text == '[parent1][parent2]Value = Anything' def test_itemstr_no_item(): parents = ["parent1", "parent2", "Value"] text = itemstr(parents=parents, item=None, value="Anything") assert text == '[parent1][parent2]Value = Anything' def test_itemstr_no_parents_no_item(): text = itemstr(parents=None, item=None, value='Anything') assert text == 'Anything' def test_itemstr_no_parents(): text = itemstr(parents=None, item="Value", value='Anything') assert text == 'Value = Anything' def test_itemstr_no_parents_no_value(): text = itemstr(parents=None, item="Value", value=None) assert text == 'Value' # --- expand_many_section @pytest.mark.parametrize( 'in_,out', [ # basically a fancy version of string.split(',') ('foo', ['foo']), ('foo,bar', ['foo', 'bar']), ('foo, bar', ['foo', ' bar']), # doesn't remove whitespace # except that it doesn't split quoted things ('"foo", "bar"', ['"foo"', ' "bar"']), ('"foo,", "b,ar"', ['"foo,"', ' "b,ar"']), # doesn't split in " quotes ("'foo', 'bar'", ["'foo'", " 'bar'"]), ("'foo,', 'b,ar'", ["'foo,'", " 'b,ar'"]), # doesn"t split in ' quotes ] ) def test_SECTION_EXPAND_PATTERN(in_, out): """It should split sections which contain commas. This is used in order to expand [foo, bar] into [foo] and [bar]. """ assert SECTION_EXPAND_PATTERN.findall(in_) == out @pytest.mark.parametrize( 'in_,out', [ ('foo,bar', ['foo', 'bar']), ('foo , bar', ['foo', 'bar']), ('"foo", "bar"', ['foo', 'bar']), ('"foo,", "b,ar"', ['foo,', 'b,ar']), ] ) def test_expand_many_section_expand(in_, out): """It should expand sections which contain commas. E.G. it should expand [foo, bar] into [foo] and [bar]. """ config = {in_: {'whatever': True}} assert list(expand_many_section(config)) == out def test_expand_many_section_order(): """It should maintain order when expanding sections.""" assert list(expand_many_section({ 'a': {}, 'b, a': {}, 'c, b, a, d': {}, 'e': {}, 'a, e': {}, 'f, e': {}, 'g, h': {}, })) == ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] def test_expand_many_section_merge(): """It should merge sections together in definition order.""" config = expand_many_section({ 'b': {'x': 1}, 'b, a, c, d': {'x': 2}, 'c': {'x': 3}, }) assert config == { 'b': {'x': 2}, 'a': {'x': 2}, 'c': {'x': 3}, 'd': {'x': 2}, } # bonus marks: ensure all values copied rather than referenced config['a']['x'] = 4 assert config['b']['x'] == 2 def test_expand_many_section_merge_deep(): """It should deep-merge nested sections - see replicate().""" config = expand_many_section({ 'b': {'x': {'y': 1}}, 'b, a, c, d': {'x': {'y': 2}}, 'c': {'x': {'y': 3}}, }) assert config == { 'b': {'x': {'y': 2}}, 'a': {'x': {'y': 2}}, 'c': {'x': {'y': 3}}, 'd': {'x': {'y': 2}}, } # bonus marks: ensure all values are unique objects # (i.e. they have been copied rather than referenced) config['a']['x']['y'] = 4 assert config['b']['x']['y'] == 2 cylc-flow-8.6.4/tests/unit/parsec/test_fileparse.py0000664000175000017500000005613515202510242022600 0ustar alastairalastair# 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 . from tempfile import NamedTemporaryFile from contextlib import suppress import os import pytest from pytest import param import sqlite3 from types import SimpleNamespace from cylc.flow import __version__ as cylc_version from cylc.flow.parsec.exceptions import ( FileParseError, IncludeFileNotFoundError, Jinja2Error, ParsecError, ) from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults from cylc.flow.parsec.fileparse import ( EXTRA_VARS_TEMPLATE, _prepend_old_templatevars, _get_fpath_for_source, get_cylc_env_vars, addict, addsect, multiline, parse, process_plugins, read_and_proc, merge_template_vars ) def test_file_parse_error(): error = FileParseError(reason="No reason") assert str(error) == "No reason" error = FileParseError("", index=2) assert str(error) == ( " (line 3)\n" "(line numbers match 'cylc view -p')" ) error = FileParseError("", line="test") assert str(error) == ":\n test" error = FileParseError("ERROR", lines={"a": ["1", "2"], "b": ["3"]}) assert str(error) == '\n'.join([ 'ERROR', 'File a', ' 1', ' 2\t<--', 'File b', ' 3\t<--' ]) def test_addsect(): cfg = OrderedDictWithDefaults() cfg["section1"] = OrderedDictWithDefaults() cfg["section1"]["subsection"] = OrderedDictWithDefaults() new_section_name = "test" existing_section_name = "section1" parents = ["section1"] empty_cfg = OrderedDictWithDefaults() addsect(empty_cfg, "", []) assert len(empty_cfg) == 1 assert "" in empty_cfg assert len(cfg) == 1 addsect(cfg, existing_section_name, []) assert len(cfg) == 1 assert "test" not in cfg["section1"] addsect(cfg, new_section_name, parents) assert len(cfg) == 1 assert "test" in cfg["section1"] def test_addict_error_line_already_encountered(): with pytest.raises(FileParseError): addict({"title": "a"}, "title", None, ["title"], 1) def test_addict_new_value_added(): for key, val in [ ('a', '1'), ('b', '2'), ('c', '3') ]: cfg = OrderedDictWithDefaults() addict(cfg, key, val, [], 0) assert key in cfg def test_addict_replace_value(): cfg = OrderedDictWithDefaults() cfg['country'] = 'ABC' addict(cfg, 'country', 'test', [], 0) assert cfg['country'] == 'test' def test_addict_replace_value_1(): """"Special case depending on key and parents, for - key is 'graph' AND parents['scheduling']['graph'] OR - len(parents)==3 AND parents['scheduling']['graph'][?] """ # set 1 key is graph, parents is wrong (T, F) cfg = OrderedDictWithDefaults() cfg['graph'] = 'ABC' addict(cfg, 'graph', 'test', [], 0) assert cfg['graph'] == 'test' # set 2 key is graph, parents right (T, T) cfg = { 'scheduling': { 'graph': { 'graph': 'ABC' } } } addict(cfg, 'graph', 'test', ['scheduling', 'graph'], 0) assert ( cfg['scheduling']['graph']['graph'] ) == ['ABC', 'test'] # other side of boolean expression # set 3 len(parents) is 3, parents is wrong (T, F) cfg = { 'scheduling': { 'graph': { 'team': { 'graph': 'ABC' } }, 'notme': { 'team': { 'graph': '1' } } } } addict(cfg, 'graph', 'test', ['scheduling', 'notme', 'team'], 0) assert ( cfg['scheduling']['notme']['team']['graph'] ) == 'test' # set 3 len(parents) is 3, parents is right (T, T) cfg = { 'scheduling': { 'graph': { 'team': { 'graph': 'ABC' } } } } addict(cfg, 'graph', 'test', ['scheduling', 'graph', 'team'], 0) assert cfg['scheduling']['graph']['team']['graph'] == ['ABC', 'test'] @pytest.mark.parametrize( 'flines, value, index, maxline, exc, expected', ( param( [], "'''single line'''", 0, 0, None, ("'''single line'''", 0), id='single-line', ), param( ["'''single line"], "'''single line", # missing closing quote 0, 0, FileParseError, { 'reason': 'Multiline string not closed', 'line': "'''single line", }, id='missing-closing-quote', ), param( ["'''", "single line"], "'''\n '''single line", # missing closing quote 0, 0, FileParseError, {'reason': 'Invalid line', 'line': "'''"}, id='missing-closing-quote2', ), param( ["", "another value"], # multiline, but we forgot to close quotes "'''a\n#b", 0, 1, FileParseError, {'reason': "Multiline string not closed"}, id='multiline-missing-closing-quote', ), param( ["", "c'''"], "'''a\n#b", 0, 1, None, ("'''a\n#b\nc'''", 1), id='good-path', ), param( ["", "c'''"], # multiline, but we forgot to close quotes "'''a\n#b", 0, 10000, # no error. The function will stop before on the quotes None, ("'''a\n#b\nc'''", 1), id='multiline-missing-closing-quote2', ), param( ["", "c", "hello", ""], # quotes out of balance "'''a\n#b", 0, 3, FileParseError, {'reason': "Multiline string not closed"}, id='unbalanced-quotes', ), param( ["", "c", "hello", ""], "'''a\n#b", 0, 4, # one too many IndexError, None, id='one-too-many', ), param( ["", "a'''c", "hello", ""], "'''a\n#b", 0, 3, FileParseError, {'reason': 'Invalid line', 'line': "a'''c"}, id='invalid-quotes', ), ), ) def test_multiline(flines, value, index, maxline, exc, expected): if exc is not None: with pytest.raises(exc) as cm: multiline(flines, value, index, maxline) if isinstance(cm.value, FileParseError): exc = cm.value for key, attr in expected.items(): assert getattr(exc, key) == attr else: r = multiline(flines, value, index, maxline) assert r == expected def test_read_and_proc_no_template_engine(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = None viewcfg = { 'jinja2': False, 'contin': False, 'inline': False, } tf.write("a=b\n".encode()) tf.flush() r = read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert r == ['a=b'] # last \\ is ignored, becoming just '' tf.write("c=\\\nd\n\\".encode()) tf.flush() viewcfg = { 'jinja2': False, 'contin': True, 'inline': False, } r = read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert r == ['a=b', 'c=d', ''] def test_inline(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = None viewcfg = { 'jinja2': False, 'contin': False, 'inline': True, 'mark': None, 'single': None, 'label': None, } with NamedTemporaryFile() as include_file: include_file.write("c=d".encode()) include_file.flush() tf.write(("a=b\n%include \"{0}\"" .format(include_file.name)).encode()) tf.flush() r = read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert r == ['a=b', 'c=d'] def test_inline_error(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = None viewcfg = { 'jinja2': False, 'contin': False, 'inline': True, 'mark': None, 'single': None, 'label': None, } tf.write("a=b\n%include \"404.txt\"".encode()) tf.flush() with pytest.raises(IncludeFileNotFoundError) as cm: read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert "404.txt" in str(cm.value) def test_read_and_proc_jinja2(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } viewcfg = { 'jinja2': True, 'contin': False, 'inline': False, } tf.write("#!jinja2\na={{ name }}\n".encode()) tf.flush() r = read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert r == ['a=Cylc'] def test_read_and_proc_cwd(tmp_path): """The template processor should be able to read workflow files. This relies on moving to the config dir during file parsing. """ sdir = tmp_path / "sub" sdir.mkdir() for sub in ["a", "b", "c"]: (sdir / sub).touch() viewcfg = { 'jinja2': True, 'contin': False, 'inline': False } tmpf = tmp_path / "a.conf" with open(tmpf, 'w') as tf: tf.write( '#!Jinja2' '\n{% from "os" import listdir %}' '\n{% for f in listdir("sub") %}' '\n{{f}}' '\n{% endfor %}' ) with open(tmpf, 'r') as tf: r = read_and_proc(fpath=tf.name, viewcfg=viewcfg) assert sorted(r) == ['a', 'b', 'c'] def test_read_and_proc_jinja2_error(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } viewcfg = { 'jinja2': True, 'contin': False, 'inline': False, } tf.write("#!jinja2\na={{ name \n".encode()) tf.flush() with pytest.raises(Jinja2Error) as cm: read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert ( "unexpected end of template, expected " "'end of print statement'." ) in str(cm.value) def test_read_and_proc_jinja2_error_missing_shebang(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } viewcfg = { 'jinja2': True, 'contin': False, 'inline': False, } # first line is missing shebang! tf.write("a={{ name }}\n".encode()) tf.flush() r = read_and_proc(fpath=fpath, template_vars=template_vars, viewcfg=viewcfg) assert r == ['a={{ name }}'] def test_parse_keys_only_singleline(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write("#!jinja2\na={{ name }}\n".encode()) tf.flush() r = parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) expected = OrderedDictWithDefaults() expected['a'] = 'Cylc' assert r == expected of.flush() output_file_contents = of.read().decode() assert output_file_contents == 'a=Cylc\n' def test_parse_keys_only_multiline(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write( "#!jinja2\na='''value is \\\n{{ name }}'''\n".encode()) tf.flush() r = parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) expected = OrderedDictWithDefaults() expected['a'] = "'''value is Cylc'''" assert r == expected def test_parse_invalid_line(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write("#!jinja2\n{{ name }}\n".encode()) tf.flush() with pytest.raises(FileParseError) as cm: parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) exc = cm.value assert exc.reason == 'Invalid line' assert exc.line_num == 1 assert exc.line == 'Cylc' def test_parse_comments(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write("#!jinja2\na={{ name }}\n# comment!".encode()) tf.flush() r = parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) expected = OrderedDictWithDefaults() expected['a'] = 'Cylc' assert r == expected of.flush() output_file_contents = of.read().decode() assert output_file_contents == 'a=Cylc\n# comment!\n' def test_parse_with_sections(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write(("#!jinja2\n[section1]\n" "a={{ name }}\n# comment!\n" "[[subsection1]]\n" "[[subsection2]]\n" "[section2]").encode()) tf.flush() r = parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) expected = OrderedDictWithDefaults() expected['section1'] = OrderedDictWithDefaults() expected['section1']['a'] = 'Cylc' expected['section1']['subsection1'] = OrderedDictWithDefaults() expected['section1']['subsection2'] = OrderedDictWithDefaults() expected['section2'] = OrderedDictWithDefaults() assert r == expected of.flush() output_file_contents = of.read().decode() assert output_file_contents == ( '[section1]\na=Cylc\n# comment!\n' '[[subsection1]]\n' '[[subsection2]]\n' '[section2]\n' ) def test_parse_with_sections_missing_bracket(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write( "#!jinja2\n[[section1]\na={{ name }}\n# comment!".encode()) tf.flush() with pytest.raises(FileParseError) as cm: parse(fpath=fpath, output_fname="", template_vars=template_vars) exc = cm.value assert exc.reason == 'bracket mismatch' assert exc.line == '[[section1]' def test_parse_with_sections_error_wrong_level(): with NamedTemporaryFile() as of, NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write(("#!jinja2\n[section1]\n" "a={{ name }}\n# comment!\n" "[[[subsection1]]]\n") # expected [[]] instead! .encode()) tf.flush() with pytest.raises(FileParseError) as cm: parse(fpath=fpath, output_fname=of.name, template_vars=template_vars) exc = cm.value assert exc.line_num == 4 assert exc.line == '[[[subsection1]]]' def test_unclosed_multiline(): with NamedTemporaryFile() as tf: fpath = tf.name template_vars = { 'name': 'Cylc' } tf.write((''' [scheduling] [[graph]] R1 = """ foo [runtime] [[foo]] script = """ echo hello world """ ''').encode()) tf.flush() with pytest.raises(FileParseError) as cm: parse(fpath=fpath, output_fname="", template_vars=template_vars) exc = cm.value assert exc.reason == 'Invalid line' assert 'echo hello world' in exc.line assert 'Did you forget to close [scheduling][graph]R1?' in str(exc) @pytest.mark.parametrize( 'expect, native_tvars, plugin_result, log', [ pytest.param( {'FOO': 123}, {'FOO': 123}, { 'templating_detected': None, 'template_variables': {'FOO': 122} }, [], id='no templating engine set' ), pytest.param( {'FOO': 125}, {'FOO': 125}, { 'templating_detected': 'qux', 'template_variables': {'FOO': 124} }, ['Overriding FOO: 124 -> 125'], id='Variable overridden' ), pytest.param( {'FOO': 126}, {'FOO': 126}, { 'templating_detected': 'qux', 'template_variables': {'FOO': 126} }, [], id='Variable overridden quietly' ) ] ) def test_merge_template_vars(caplog, expect, native_tvars, plugin_result, log): assert merge_template_vars(native_tvars, plugin_result) == expect assert [r.msg for r in caplog.records] == log @pytest.fixture def _mock_old_template_vars_db(tmp_path): def _inner(create_srclink=True): # Create a fake workflow dir: (tmp_path / 'flow.cylc').touch() (tmp_path / 'log').mkdir() db_path = tmp_path / 'log/db' db_path.touch() # Set up a fake workflow database: conn = sqlite3.connect(str(db_path)) conn.execute( "CREATE TABLE workflow_template_vars" "(key TEXT, value TEXT, PRIMARY KEY(key)) ;" ) conn.execute( "INSERT INTO workflow_template_vars VALUES" " ('Marius', '\"Consul\"')" ) conn.execute( "CREATE TABLE workflow_params" "(key TEXT, value TEXT, PRIMARY KEY(key)) ;" ) conn.execute( "INSERT INTO workflow_params VALUES" f" ('cylc_version', '{cylc_version}')" ) conn.commit() conn.close() # Simulate being an installed rundir by creating a sourcelink: if create_srclink: src = (tmp_path / 'src') src.mkdir(exist_ok=True) link = tmp_path.parent / '_cylc-install/source' link.parent.mkdir(exist_ok=True) with suppress(FileExistsError): # We don't mind the link persisting. os.symlink(src, link) return tmp_path / 'flow.cylc' yield _inner @pytest.mark.parametrize( 'expect, tvars', [ param( {'Marius': 'Consul', 'Julius': 'Pontifex'}, {'Julius': 'Pontifex'}, id='It adds a new key at the end' ), param( {'Marius': 'Tribune'}, {'Marius': 'Tribune'}, id='It overrides an existing key' ), ] ) def test__prepend_old_templatevars(_mock_old_template_vars_db, expect, tvars): # Create a target for a source symlink result = _prepend_old_templatevars( _mock_old_template_vars_db(), tvars) assert result == expect def test_get_fpath_for_source(tmp_path): # Create rundir and srcdir: srcdir = tmp_path / 'source' rundir = tmp_path / 'run' srcdir.mkdir() rundir.mkdir() (rundir / 'flow.cylc').touch() (srcdir / 'flow.cylc').touch() # Mock Options object: opts = SimpleNamespace() # It raises an error if source is not linked: with pytest.raises( ParsecError, match=f'Cannot validate {rundir} against source:' ): opts.against_source = True _get_fpath_for_source(rundir / 'flow.cylc', opts) # Create symlinks: link = rundir / '_cylc-install/source' link.parent.mkdir(exist_ok=True) os.symlink(srcdir, link) # It does nothing if opts.against_source is False: opts.against_source = False assert _get_fpath_for_source( rundir / 'flow.cylc', opts) == rundir / 'flow.cylc' # It gets source dir if opts.against_source is True: opts.against_source = True assert _get_fpath_for_source( rundir / 'flow.cylc', opts) == str(srcdir / 'flow.cylc') def test_user_has_no_cwd(tmp_path): """Test we can parse a config file even if cwd does not exist.""" cwd = tmp_path / "cwd" os.mkdir(cwd) os.chdir(cwd) os.rmdir(cwd) # (I am now located in a non-existent directory. Outrageous!) with NamedTemporaryFile() as tf: fpath = tf.name tf.write((''' [scheduling] [[graph]] R1 = "foo" ''').encode()) tf.flush() # Should not raise FileNotFoundError from os.getcwd(): parse(fpath=fpath, output_fname="") def test_get_cylc_env_vars(monkeypatch): """It should return CYLC env vars but not CYLC_VERSION or CYLC_ENV_NAME.""" monkeypatch.setattr( 'os.environ', { "CYLC_VERSION": "betwixt", "CYLC_ENV_NAME": "between", "CYLC_QUESTION": "que?", "CYLC_ANSWER": "42", "FOO": "foo" } ) assert ( get_cylc_env_vars() == { "CYLC_QUESTION": "que?", "CYLC_ANSWER": "42", } ) class EntryPointWrapper: """Wraps a method to make it look like an entry point.""" def __init__(self, fcn): self.name = fcn.__name__ self.fcn = fcn def load(self): return self.fcn @EntryPointWrapper def pre_configure_basic(*_, **__): """Simple plugin that returns one env var and one template var.""" return {'env': {'foo': 44}, 'template_variables': {}} def test_plugins_not_called_on_global_config(monkeypatch): monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda x: [pre_configure_basic] ) result = process_plugins('/pennine/way/flow.cylc', {}) assert result != EXTRA_VARS_TEMPLATE result = process_plugins('/appalachian/trail/global.cylc', {}) assert result == EXTRA_VARS_TEMPLATE cylc-flow-8.6.4/tests/unit/network/0000775000175000017500000000000015202510242017417 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/network/test_graphql.py0000664000175000017500000001410515202510242022467 0ustar alastairalastair# 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 . import pytest from pytest import param from graphql import ( TypeInfo, TypeInfoVisitor, parse, visit ) from cylc.flow.data_messages_pb2 import PbTaskProxy, PbPrerequisite from cylc.flow.network.graphql import ( CylcVisitor, null_setter, strip_null, async_next, NULL_VALUE, grow_tree ) from cylc.flow.network.schema import schema TASK_PROXY_PREREQS = PbTaskProxy() TASK_PROXY_PREREQS.prerequisites.append(PbPrerequisite(expression="foo")) @pytest.mark.parametrize( 'query,' 'variables,' 'search_arg,' 'expected_result', [ pytest.param( ''' query ($workflowID: ID) { workflows (ids: [$workflowID]) { id } } ''', { 'workflowID': 'cylc|workflow' }, { 'ids': ['cylc|workflow'], }, True, id="simple query with correct variables" ), pytest.param( ''' query ($workflowID: ID) { ...WorkflowData } fragment WorkflowData on workflows { workflows (ids: [$workflowID]) { id } } ''', { 'workflowID': 'cylc|workflow' }, { 'ids': ['cylc|workflow'], }, True, id="query with a fragment and correct variables" ), pytest.param( ''' query ($workflowID: ID) { workflows (ids: [$workflowID]) { id } } ''', { 'workflowID': 'cylc|workflow' }, { 'ids': None, }, False, id="correct variable definition, but missing variable in " "provided values" ), pytest.param( ''' query ($workflowID: ID) { workflows (ids: [$workflowID]) { id } } ''', { 'workflowID': 'cylc|workflow' }, { 'idfsdf': ['cylc|workflow'], }, False, id="correct variable definition, but wrong search argument" ) ] ) def test_query_variables( query: str, variables: dict, search_arg: dict, expected_result: bool, ): """Test that query variables are parsed and found correctly. Args: query: a valid GraphQL query (using our schema) variables: map with variable values for the query search_arg: argument and value to search for expected_result: was the argument and value found """ document = parse(query) type_info = TypeInfo(schema.graphql_schema) cylc_visitor = CylcVisitor( type_info, variables, search_arg ) visit( document, TypeInfoVisitor( type_info, cylc_visitor ), None ) assert expected_result == cylc_visitor.arg_flag @pytest.mark.parametrize( 'pre_result,' 'expected_result', [ ( 'foo', 'foo' ), ( [], NULL_VALUE ), ( {}, NULL_VALUE ), ( TASK_PROXY_PREREQS.prerequisites, TASK_PROXY_PREREQS.prerequisites ), ( PbTaskProxy().prerequisites, NULL_VALUE ) ] ) def test_null_setter(pre_result, expected_result): """Test the null setting of different data types/results.""" post_result = null_setter(pre_result) assert post_result == expected_result @pytest.mark.parametrize( 'pre_result,' 'expected_result', [ ( 'foo', 'foo' ), ( [NULL_VALUE], [] ), ( {'nothing': NULL_VALUE}, {}, ), ( TASK_PROXY_PREREQS.prerequisites, TASK_PROXY_PREREQS.prerequisites ), ] ) async def test_strip_null(pre_result, expected_result): """Test the null stripping of different result data/types.""" # non-async post_result = async_next(strip_null, pre_result) assert post_result == expected_result async def async_result(result): return result # async async_post_result = async_next(strip_null, async_result(pre_result)) assert await async_post_result == expected_result @pytest.mark.parametrize( 'expect, tree, path, leaves', [ param( {'foo': {'bar': {'baz': {}}}}, {}, ['foo', 'bar', 'baz'], None, id='fill-empty-tree' ), param( {'bar': {'baz': {}}}, {'bar': {'baz': {}}}, ['bar', 'baz'], None, id='keep-full-tree' ), param( {'foo': {'bar': {}, 'qux': {}}}, {'foo': {'bar': {}}}, ['foo', 'qux'], None, id='add-new-branch' ), param( {'leaves': {'foo': {}}}, {}, [], {'foo': {}}, id='add-leaves' ) ] ) def test_grow_tree(expect, tree, path, leaves): grow_tree(tree, path, leaves) assert tree == expect cylc-flow-8.6.4/tests/unit/network/test_subscriber.py0000664000175000017500000000206015202510242023171 0ustar alastairalastair# 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 . """Test subsciber module components.""" from cylc.flow.network.subscriber import process_delta_msg def test_process_delta_msg(): """Test delta message processing.""" # test non-key not_topic, not_delta = process_delta_msg(b'foo', b'bar', None) assert not_topic == 'foo' assert not_delta == b'bar' cylc-flow-8.6.4/tests/unit/network/test_scan.py0000664000175000017500000001041415202510242021754 0ustar alastairalastair# 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 . """Test scan filters and data provision stuff.""" from pathlib import Path import re from textwrap import dedent from cylc.flow.network.scan import ( api_version, contact_info, cylc_version, filter_name, graphql_query, validate_contact_info ) from cylc.flow.workflow_files import ( ContactFileFields, WorkflowFiles ) SRV_DIR = Path(WorkflowFiles.Service.DIRNAME) CONTACT = Path(WorkflowFiles.Service.CONTACT) def test_filter_name_preprocess(): """It should combine provided patterns and compile them.""" pipe = filter_name('^f', '^c') assert pipe.args[0] == re.compile('(^f|^c)') async def test_filter_name(): """It should filter flows by registration name.""" pipe = filter_name('^f') assert await pipe.func( {'name': 'foo'}, *pipe.args ) assert not await pipe.func( {'name': 'bar'}, *pipe.args ) async def test_cylc_version(): """It should filter flows by cylc version.""" version = ContactFileFields.VERSION pipe = cylc_version('>= 8.0a1, < 9') assert await pipe.func( {version: '8.0a1'}, *pipe.args ) pipe = cylc_version('>= 8.0a1, < 9') assert not await pipe.func( {version: '7.8.4'}, *pipe.args ) async def test_api_version(): """It should filter flows by api version.""" version = ContactFileFields.API pipe = api_version('>= 4, < 5') assert await pipe.func( {version: '4'}, *pipe.args ) pipe = api_version('>= 4, < 5') assert not await pipe.func( {version: '5'}, *pipe.args ) async def test_contact_info(tmp_path): """It should load info from the contact file.""" # create a dummy flow Path(tmp_path, 'foo', SRV_DIR).mkdir(parents=True) # write a contact file with some junk in it with open(Path(tmp_path, 'foo', SRV_DIR, CONTACT), 'w+') as contact: contact.write(dedent(''' foo=1 bar=2 baz=3 ''').strip()) # create a flow dict as returned by scan flow = { 'name': 'foo', 'path': tmp_path / 'foo' } # ensure the contact fields get added to the flow dict assert await contact_info.func(flow) == { **flow, 'foo': '1', 'bar': '2', 'baz': '3' } def test_graphql_query_preproc(): """It should format graphql query fragments from the input data.""" pipe = graphql_query(['a', 'b', 'c']) assert pipe.args[0] == dedent(''' a b c ''') pipe = graphql_query({'a': None, 'b': None, 'c': None}) assert pipe.args[0] == dedent(''' a b c ''') pipe = graphql_query({'a': None, 'b': ['ba', 'bb'], 'c': None}) assert pipe.args[0] == dedent(''' a c b { ba bb } ''') async def test_validate_contact_file(tmp_path): """Ensure rejection for missing fields""" flow = { 'name': 'foo', 'path': tmp_path / 'foo', } # contact_info has already loaded info from contact file (missing fields) assert await validate_contact_info.func(flow) is False async def test_validate_contact_file_no_missing_fields(tmp_path): """Ensure rejection for missing fields""" version = ContactFileFields.API name = ContactFileFields.NAME host = ContactFileFields.HOST flow = { 'name': 'foo', 'path': tmp_path / 'foo', version: 1, name: 'moo', host: 'hosty' } assert await validate_contact_info.func(flow) == flow cylc-flow-8.6.4/tests/unit/network/test_multi.py0000664000175000017500000001144215202510242022164 0ustar alastairalastair# 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 . from functools import partial import pytest from cylc.flow.exceptions import CylcError, WorkflowStopped from cylc.flow.network.multi import _report, _process_response from cylc.flow.terminal import DIM def response(success, msg, operation='set'): return { operation: { 'result': [{'id': '~user/workflow', 'response': [success, msg]}] } } def test_report_valid(monkeypatch): """It should report command outcome.""" monkeypatch.setattr('cylc.flow.flags.verbosity', 0) # fail case assert _report(response(False, 'MyError')) == ( None, 'MyError', False, ) # success case assert _report(response(True, '12345')) == ( 'Command queued', None, True, ) # success case (debug mode) monkeypatch.setattr('cylc.flow.flags.verbosity', 1) assert _report(response(True, '12345')) == ( f'Command queued <{DIM}>id=12345', None, True, ) def test_report_invalid(monkeypatch): """It should report invalid responses. Tests that the code behaves as well as can be expected when confronted with responses which should not be possible. """ # test "None" response monkeypatch.setattr('cylc.flow.flags.verbosity', 0) assert _report({'set': None}) == ( None, 'Error processing command:' "\n TypeError: 'NoneType' object is not subscriptable", False, ) # test "None" response in debug mode monkeypatch.setattr('cylc.flow.flags.verbosity', 2) assert _report({'set': None}) == ( None, 'Error processing command:' "\n TypeError: 'NoneType' object is not subscriptable" # the response should be output in debug mode "\n response={'set': None}", False, ) # test multiple mutations in one operation (not supported) monkeypatch.setattr('cylc.flow.flags.verbosity', 0) assert _report( { **response(True, '12345'), **response(True, '23456', 'trigger'), } ) == ( None, 'Error processing command:' '\n NotImplementedError:' ' Cannot process multiple mutations in one operation.', False, ) # test zero mutations in the operation assert _report( {} ) == ( None, 'Error processing command:' '\n Exception: {}', False, ) def test_process_response(monkeypatch): """It should handle exceptions and return processed results.""" def report(exception_class, _response): raise exception_class('xxx') class Foo(Exception): pass # WorkflowStopped -> fail case monkeypatch.setattr('cylc.flow.flags.verbosity', 0) assert _process_response(partial(report, WorkflowStopped), {}) == ( None, 'WorkflowStopped: xxx is not running', False, ) # WorkflowStopped -> success case for this command monkeypatch.setattr('cylc.flow.flags.verbosity', 0) assert _process_response( partial(report, WorkflowStopped), {}, # this overrides the default interpretation of "WorkflowStopped" as a # fail case success_exceptions=(WorkflowStopped,), ) == ( 'WorkflowStopped: xxx is not running', None, True, # success outcome ) # CylcError -> expected error, log it monkeypatch.setattr('cylc.flow.flags.verbosity', 0) assert _process_response(partial(report, CylcError), {}) == ( None, 'CylcError: xxx', False, ) # CylcError -> expected error, raise it (debug mode) monkeypatch.setattr('cylc.flow.flags.verbosity', 2) with pytest.raises(CylcError): _process_response(partial(report, CylcError), {}) # Exception -> unexpected error, raise it monkeypatch.setattr('cylc.flow.flags.verbosity', 0) with pytest.raises(Foo): _process_response(partial(report, Foo), {}) cylc-flow-8.6.4/tests/unit/network/test_schema.py0000664000175000017500000001067415202510242022300 0ustar alastairalastair# 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 . from dataclasses import dataclass from inspect import isclass import re import graphene import pytest from cylc.flow.cfgspec.workflow import SPEC as WORKFLOW_SPEC from cylc.flow.network.schema import ( RUNTIME_FIELD_TO_CFG_MAP, Mutations, Runtime, WorkflowStopMode, runtime_schema_to_cfg, sort_elements, SortArgs, ) from cylc.flow.workflow_status import StopMode, WorkflowStatus @dataclass class DummyObject: value: int @pytest.mark.parametrize( 'elements,sort_args,expected_result', [ # sort asc by key ( [DummyObject(1), DummyObject(3), DummyObject(2)], { 'keys': ['value'], 'reverse': False # NOTE: GraphQL ensures reverse is not None! }, [DummyObject(1), DummyObject(2), DummyObject(3)] ), # sort desc by key ( [DummyObject(1), DummyObject(3), DummyObject(2)], { 'keys': ['value'], 'reverse': True }, [DummyObject(3), DummyObject(2), DummyObject(1)] ), # raise error when no keys given ( [DummyObject(1), DummyObject(3), DummyObject(2)], { 'keys': [], 'reverse': True }, ValueError ), # raise error when any of the keys given are not in the schema ( [DummyObject(1), DummyObject(3), DummyObject(2)], { 'keys': ['value', 'river_name'], 'reverse': True }, ValueError ) ] ) def test_sort_args(elements, sort_args, expected_result): """Test the sorting function used by the schema.""" sort = SortArgs() sort.keys = sort_args['keys'] sort.reverse = sort_args['reverse'] args = { 'sort': sort } if isclass(expected_result): with pytest.raises(expected_result): sort_elements(elements, args) else: sort_elements(elements, args) assert elements == expected_result def test_runtime_field_to_cfg_map(): """Ensure the Runtime type's fields can be mapped back to the workflow config.""" assert set(RUNTIME_FIELD_TO_CFG_MAP) == set(Runtime._meta.fields) for cfg_name in RUNTIME_FIELD_TO_CFG_MAP.values(): assert WORKFLOW_SPEC.get('runtime', '__MANY__', cfg_name) @pytest.mark.parametrize('runtime_dict,expected', [ pytest.param( {'run_mode': 'Skip'}, {'run mode': 'skip'}, id='edit-runtime' ), pytest.param( {'run mode': 'skip'}, {'run mode': 'skip'}, id='broadcast' ), ]) def test_runtime_schema_to_cfg(runtime_dict, expected): """Test this function can handle Edit Runtime submitted values as well as normal broadcast values.""" assert runtime_schema_to_cfg(runtime_dict) == expected @pytest.mark.parametrize('mutation', ( pytest.param(attr, id=name) for name, attr in Mutations.__dict__.items() if isinstance(attr, graphene.Field) )) def test_mutations_valid_for(mutation): """Check that all mutations have a "Valid for" in their description. This is needed by the UI to disable mutations that are not valid for the workflow state. """ match = re.search( r'Valid for:\s(.*)\sworkflows.', mutation.description ) assert match valid_states = set(match.group(1).split(', ')) assert valid_states assert not valid_states.difference(i.value for i in WorkflowStatus) @pytest.mark.parametrize('wflow_stop_mode', list(WorkflowStopMode)) def test_stop_mode_enum(wflow_stop_mode): """Check that WorkflowStopMode is a subset of StopMode.""" assert StopMode(wflow_stop_mode.value) assert wflow_stop_mode.description cylc-flow-8.6.4/tests/unit/network/test_publisher.py0000664000175000017500000000203015202510242023020 0ustar alastairalastair# 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 . from cylc.flow.network.publisher import serialize_data def test_serialize_data(): str1 = 'hello' assert serialize_data(str1, None) == str1 assert serialize_data(str1, 'encode', 'utf-8') == str1.encode('utf-8') assert serialize_data(str1, bytes, 'utf-8') == bytes(str1, 'utf-8') cylc-flow-8.6.4/tests/unit/network/test__init__.py0000664000175000017500000000434215202510242022433 0ustar alastairalastair# 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 . """Test __init__.py for network interfaces to Cylc scheduler objects.""" import pytest import cylc.flow from cylc.flow.exceptions import CylcVersionError from cylc.flow.network import get_location from cylc.flow.workflow_files import ContactFileFields BASE_CONTACT_DATA = { ContactFileFields.HOST: 'foo', ContactFileFields.PORT: '42', } @pytest.fixture() def mpatch_get_fqdn_by_host(monkeypatch): """Monkeypatch function used the same by all tests.""" monkeypatch.setattr( cylc.flow.network, 'get_fqdn_by_host', lambda _: 'myhost.x.y.z' ) def test_get_location_ok(monkeypatch, mpatch_get_fqdn_by_host): """It passes when information is available.""" contact_data = BASE_CONTACT_DATA.copy() contact_data[ContactFileFields.PUBLISH_PORT] = '8042' contact_data[ContactFileFields.VERSION] = cylc.flow.__version__ monkeypatch.setattr( cylc.flow.network, 'load_contact_file', lambda _: contact_data ) assert get_location('_') == ( 'myhost.x.y.z', 42, 8042, cylc.flow.__version__ ) def test_get_location_old_contact_file(monkeypatch, mpatch_get_fqdn_by_host): """It Fails because it's not a Cylc 8 workflow.""" contact_data = BASE_CONTACT_DATA.copy() contact_data['CYLC_SUITE_PUBLISH_PORT'] = '8042' contact_data['CYLC_VERSION'] = '5.1.2' monkeypatch.setattr( cylc.flow.network, 'load_contact_file', lambda _: contact_data ) with pytest.raises(CylcVersionError, match=r'.*5.1.2.*'): get_location('_') cylc-flow-8.6.4/tests/unit/test_task_trigger.py0000664000175000017500000001352115202510242022026 0ustar alastairalastair# 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 . from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.loader import ( get_point, get_sequence, ) from cylc.flow.task_outputs import TaskOutputs from cylc.flow.task_trigger import ( Dependency, TaskTrigger, ) def test_check_with_cycle_point(): task_trigger = TaskTrigger( 'fake_task_name', 1, 'fakeOutput', None, None, None, None) actual = str(task_trigger) expected = 'fake_task_name[1]:fakeOutput' assert actual == expected def test_check_with_no_cycle_point_with_offset(): task_trigger = TaskTrigger( 'fake_task_name', 2, 'fakeOutput', None, None, None, None) actual = str(task_trigger) expected = 'fake_task_name[2]:fakeOutput' assert actual == expected def test_check_with_no_cycle_point_or_offset(): task_trigger = TaskTrigger( 'fake_task_name', None, 'fakeOutput', None, None, None, None) actual = str(task_trigger) expected = 'fake_task_name:fakeOutput' assert actual == expected def test_check_for_false_suicide(): task_trigger = TaskTrigger( 'fake_task_name', 1, 'fakeOutput', None, None, None, None) dependency = Dependency( [task_trigger, '&', task_trigger], [task_trigger], False) actual = str(dependency) expected = ( '( fake_task_name[1]:fakeOutput ) ( & ) ( fake_task_name[1]' ':fakeOutput )') assert actual == expected def test_check_for_true_suicide(): task_trigger = TaskTrigger( 'fake_task_name', None, 'fakeOutput', None, None, None, None) dependency = Dependency( [task_trigger, '&', task_trigger], [task_trigger], True) actual = str(dependency) expected = ( '! ( fake_task_name:fakeOutput ) ( & ) ( fake_task_name:fakeOutput )') assert actual == expected def test_check_for_list_of_lists_exp(): task_trigger = TaskTrigger( 'fake_task_name', None, 'fakeOutput', None, None, None, None) dependency = Dependency( [ task_trigger, '&', ['task', '&', 'another_task'] ], [task_trigger], False ) actual = str(dependency) expected = ( "( fake_task_name:fakeOutput ) ( & ) ['task', '&', 'another_task']") assert actual == expected def test_check_trigger_name(): assert not TaskOutputs.is_valid_std_name("Elephant") def test_get_parent_point(set_cycling_type): set_cycling_type() one = get_point('1') two = get_point('2') trigger = TaskTrigger('name', None, 'output') assert trigger.get_parent_point(one) == one trigger = TaskTrigger('name', one, 'output', offset_is_absolute=True) assert trigger.get_parent_point(None) == one trigger = TaskTrigger('name', '+P1', 'output', initial_point=one) assert trigger.get_parent_point(one) == two trigger = TaskTrigger( 'name', '+P1', 'output', offset_is_from_icp=True, initial_point=one) assert trigger.get_parent_point(two) == two assert trigger.get_parent_point(one) == two def test_get_child_point(set_cycling_type): set_cycling_type() zero = get_point('0') one = get_point('1') two = get_point('2') p1 = get_sequence('P1', one) trigger = TaskTrigger('name', None, 'output') assert trigger.get_child_point(one, p1) == one assert trigger.get_child_point(two, p1) == two trigger = TaskTrigger('name', '+P1', 'output', offset_is_absolute=True) assert trigger.get_child_point(None, p1) == one trigger = TaskTrigger('name', '+P1', 'output', offset_is_from_icp=True) assert trigger.get_child_point(None, p1) == one trigger = TaskTrigger('name', '+P1', 'output', offset_is_irregular=True) assert trigger.get_child_point(one, p1) == zero trigger = TaskTrigger('name', '-P1', 'output', offset_is_irregular=True) assert trigger.get_child_point(one, p1) == two trigger = TaskTrigger('name', '+P1', 'output') assert trigger.get_child_point(one, None) == zero trigger = TaskTrigger('name', '-P1', 'output') assert trigger.get_child_point(one, None) == two def test_get_point(set_cycling_type): set_cycling_type() one = get_point('1') two = get_point('2') trigger = TaskTrigger('name', '1', 'output', offset_is_absolute=True) assert trigger.get_point(None) == one trigger = TaskTrigger( 'name', '+P1', 'output', offset_is_from_icp=True, initial_point=one) assert trigger.get_point(None) == two trigger = TaskTrigger('name', '+P1', 'output') assert trigger.get_point(one) == two trigger = TaskTrigger('name', None, 'output') assert trigger.get_point(one) == one def test_str(set_cycling_type): set_cycling_type() trigger = TaskTrigger('name', '1', 'output', offset_is_absolute=True) assert str(trigger) == 'name[1]:output' trigger = TaskTrigger('name', '+P1', 'output') assert str(trigger) == 'name[+P1]:output' trigger = TaskTrigger('name', None, 'output') assert str(trigger) == 'name:output' def test_eq(): args = ('foo', '+P1', 'succeeded') assert TaskTrigger(*args) == TaskTrigger(*args) assert TaskTrigger(*args) != TaskTrigger( *args, initial_point=IntegerPoint('1') ) cylc-flow-8.6.4/tests/unit/test_task_state.py0000664000175000017500000001322615202510242021505 0ustar alastairalastair# 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 . from unittest.mock import MagicMock import pytest from types import SimpleNamespace from cylc.flow.prerequisite import Prerequisite from cylc.flow.taskdef import TaskDef from cylc.flow.cycling.integer import IntegerSequence, IntegerPoint from cylc.flow.run_modes import RunMode, disable_task_event_handlers from cylc.flow.task_trigger import Dependency, TaskTrigger from cylc.flow.task_state import ( TaskState, TASK_STATUS_PREPARING, TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, TASK_STATUS_RUNNING, ) @pytest.mark.parametrize( 'state,is_held', [ (TASK_STATUS_WAITING, True), (TASK_STATUS_SUCCEEDED, False) ] ) def test_state_comparison(state, is_held): """Test the __call__ method.""" tdef = TaskDef('foo', {}, '123', '123') tstate = TaskState(tdef, '123', state, is_held) assert tstate(state, is_held=is_held) assert tstate(state) assert tstate(is_held=is_held) assert tstate(state, 'of', 'flux') assert tstate(state, 'of', 'flux', is_held=is_held) assert not tstate(state + 'x', is_held=not is_held) assert not tstate(state, is_held=not is_held) assert not tstate(state + 'x', is_held=is_held) assert not tstate(state + 'x') assert not tstate(is_held=not is_held) assert not tstate(state + 'x', 'of', 'flux') @pytest.mark.parametrize( 'state,is_held,should_reset', [ (None, None, False), (TASK_STATUS_WAITING, None, False), (None, True, False), (TASK_STATUS_WAITING, True, False), (TASK_STATUS_SUCCEEDED, None, True), (None, False, True), (TASK_STATUS_WAITING, False, True), ] ) def test_reset(state, is_held, should_reset): """Test that tasks do or don't have their state changed.""" tdef = TaskDef('foo', {}, '123', '123') # create task state: # * status: waiting # * is_held: true tstate = TaskState(tdef, '123', TASK_STATUS_WAITING, True) assert tstate.reset(state, is_held) == should_reset if is_held is not None: assert tstate.is_held == is_held if state is not None: assert tstate.status == state def test_task_prereq_duplicates(set_cycling_type): """Test prerequisite duplicates from multiple recurrences are discarded.""" set_cycling_type() seq1 = IntegerSequence('R1', "1") seq2 = IntegerSequence('R/1/P1', "1") trig = TaskTrigger('a', "1", 'succeeded', None, None, None, None) dep = Dependency([trig], [trig], False) tdef = TaskDef('foo', {}, IntegerPoint("1"), IntegerPoint("1")) tdef.add_dependency(dep, seq1) tdef.add_dependency(dep, seq2) # duplicate! tstate = TaskState(tdef, IntegerPoint("1"), TASK_STATUS_WAITING, False) prereqs = [p._satisfied for p in tstate.prerequisites] assert prereqs == [{("1", "a", "succeeded"): False}] def test_task_state_order(): """Test is_gt and is_gte methods.""" tdef = TaskDef('foo', {}, IntegerPoint("1"), IntegerPoint("1")) tstate = TaskState(tdef, IntegerPoint("1"), TASK_STATUS_SUBMITTED, False) assert tstate.is_gt(TASK_STATUS_WAITING) assert tstate.is_gt(TASK_STATUS_PREPARING) assert tstate.is_gt(TASK_STATUS_SUBMIT_FAILED) assert not tstate.is_gt(TASK_STATUS_SUBMITTED) assert tstate.is_gte(TASK_STATUS_SUBMITTED) assert not tstate.is_gt(TASK_STATUS_RUNNING) assert not tstate.is_gte(TASK_STATUS_RUNNING) def test_get_resolved_dependencies(): prereq1 = Prerequisite(IntegerPoint('2')) prereq1[('1', 'a', 'x')] = True prereq1[('1', 'b', 'x')] = False prereq1[('1', 'c', 'x')] = 'satisfied from database' prereq1[('1', 'd', 'x')] = 'force satisfied' prereq2 = Prerequisite(IntegerPoint('2')) prereq2[('1', 'e', 'succeeded')] = False prereq2[('1', 'e', 'failed')] = True task_state = TaskState( MagicMock(), IntegerPoint('2'), TASK_STATUS_WAITING, False ) task_state.prerequisites = [prereq1, prereq2] assert task_state.get_resolved_dependencies() == [ '1/a', '1/c', '1/d', '1/e', ] @pytest.mark.parametrize( 'itask_run_mode, disable_handlers, expect', ( ('live', True, False), ('live', False, False), ('dummy', True, False), ('dummy', False, False), ('simulation', True, True), ('simulation', False, True), ('skip', True, True), ('skip', False, False), ) ) def test_disable_task_event_handlers(itask_run_mode, disable_handlers, expect): """Conditions under which task event handlers should not be used. """ # Construct a fake itask object: itask = SimpleNamespace( run_mode=RunMode(itask_run_mode), platform={'disable task event handlers': disable_handlers}, tdef=SimpleNamespace( rtconfig={ 'skip': {'disable task event handlers': disable_handlers}}) ) # Check method: assert disable_task_event_handlers(itask) is expect cylc-flow-8.6.4/tests/unit/test_workflow_files.py0000664000175000017500000004111515202510242022375 0ustar alastairalastair# 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 . import logging from pathlib import Path from typing import ( Any, Callable, Dict, Optional, Type, Union, ) import pytest from cylc.flow import workflow_files from cylc.flow.exceptions import ( InputError, WorkflowFilesError, ) from cylc.flow.workflow_files import ( WorkflowFiles, abort_if_flow_file_in_path, check_flow_file, check_reserved_dir_names, detect_both_flow_and_suite, get_symlink_dirs, infer_latest_run, is_forbidden, is_installed, validate_workflow_name, ) from .filetree import ( FILETREE_1, FILETREE_2, FILETREE_3, FILETREE_4, create_filetree, ) NonCallableFixture = Any @pytest.mark.parametrize( 'path, expected', [('a/b/c', '/mock_cylc_dir/a/b/c'), ('/a/b/c', '/a/b/c')] ) def test_get_cylc_run_abs_path( path: str, expected: str, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr('cylc.flow.pathutil._CYLC_RUN_DIR', '/mock_cylc_dir') assert workflow_files.get_cylc_run_abs_path(path) == expected @pytest.mark.parametrize('is_abs_path', [False, True]) def test_is_valid_run_dir(is_abs_path: bool, tmp_run_dir: Callable): """Test that a directory is correctly identified as a valid run dir when it contains a service dir. """ cylc_run_dir: Path = tmp_run_dir() prefix = str(cylc_run_dir) if is_abs_path else '' # What if no dir there? assert workflow_files.is_valid_run_dir( Path(prefix, 'nothing/here')) is False # What if only flow.cylc exists but no service dir? # (Non-run dirs can still contain flow.cylc) run_dir = cylc_run_dir.joinpath('foo/bar') run_dir.mkdir(parents=True) flow_file = run_dir.joinpath(WorkflowFiles.FLOW_FILE) flow_file.touch() assert workflow_files.is_valid_run_dir(Path(prefix, 'foo/bar')) is True # What if service dir exists? flow_file.unlink() run_dir.joinpath(WorkflowFiles.Service.DIRNAME).mkdir() assert workflow_files.is_valid_run_dir(Path(prefix, 'foo/bar')) is True @pytest.mark.parametrize( 'id_, expected_err, expected_msg', [('foo/bar/', None, None), ('/foo/bar', WorkflowFilesError, "cannot be an absolute path"), ('$HOME/alone', WorkflowFilesError, "invalid workflow name"), ('./foo', WorkflowFilesError, "invalid workflow name"), ('meow/..', WorkflowFilesError, "cannot be a path that points to the cylc-run directory or above")] ) def test_validate_workflow_name(id_, expected_err, expected_msg): if expected_err: with pytest.raises(expected_err) as exc: validate_workflow_name(id_) if expected_msg: assert expected_msg in str(exc.value) else: validate_workflow_name(id_) @pytest.mark.parametrize( 'name, err_expected', [ # Basic ok: ('foo/bar/baz', False), # Reserved dir names: ('foo/log/baz', True), ('foo/runN/baz', True), ('foo/run9000/baz', True), ('work', True), # If not exact match, but substring, that's fine: ('foo/underrunN/baz', False), ('foo/overrun2', False), ('slog', False) ] ) def test_check_reserved_dir_names(name: str, err_expected: bool): if err_expected: with pytest.raises(WorkflowFilesError) as exc_inf: check_reserved_dir_names(name) assert "cannot contain a directory named" in str(exc_inf.value) else: check_reserved_dir_names(name) def test_validate_workflow_name__reserved_name(): """Check that validate_workflow_name() doesn't check for reserved dir names unless we tell it to with the arg.""" name = 'foo/runN' validate_workflow_name(name) with pytest.raises(WorkflowFilesError): validate_workflow_name(name, check_reserved_names=True) @pytest.mark.parametrize( 'path, implicit_runN, expected_reg', [ ('{cylc_run}/numbered/workflow', True, 'numbered/workflow/run2'), ('{cylc_run}/numbered/workflow', False, 'numbered/workflow'), ('{cylc_run}/numbered/workflow/runN', True, 'numbered/workflow/run2'), ('{cylc_run}/numbered/workflow/runN', False, 'numbered/workflow/run2'), ('{cylc_run}/numbered/workflow/run1', True, 'numbered/workflow/run1'), ('{cylc_run}/numbered/workflow/run1', False, 'numbered/workflow/run1'), ('{cylc_run}/non_numbered/workflow', True, 'non_numbered/workflow'), ('{cylc_run}/non_numbered/workflow', False, 'non_numbered/workflow'), ] ) def test_infer_latest_run( path: str, implicit_runN: bool, expected_reg: str, tmp_run_dir: Callable, ) -> None: """Test infer_latest_run(). Params: path: Input arg. implicit_runN: Input arg. expected_reg: The id_ part of the expected returned tuple. """ # Setup cylc_run_dir: Path = tmp_run_dir() run_dir = cylc_run_dir / 'numbered' / 'workflow' run_dir.mkdir(parents=True) (run_dir / 'run1').mkdir() (run_dir / 'run2').mkdir() (run_dir / 'runN').symlink_to('run2') run_dir = cylc_run_dir / 'non_numbered' / 'workflow' / 'named_run' run_dir.mkdir(parents=True) path: Path = Path(path.format(cylc_run=cylc_run_dir)) expected = (cylc_run_dir / expected_reg, expected_reg) # Test assert infer_latest_run(path, implicit_runN) == expected # Check implicit_runN=True is the default: if implicit_runN: assert infer_latest_run(path) == expected @pytest.mark.parametrize('warn_arg', [True, False]) def test_infer_latest_run_warns_for_runN( warn_arg: bool, log_filter: Callable, tmp_run_dir: Callable, monkeypatch: pytest.MonkeyPatch ): """Tests warning is produced to discourage use of /runN in workflow_id""" (tmp_run_dir() / 'run1').mkdir() runN_path = tmp_run_dir() / 'runN' runN_path.symlink_to('run1') monkeypatch.setattr('cylc.flow.LOG.level', logging.INFO) infer_latest_run(runN_path, warn_runN=warn_arg) filtered_log = log_filter( logging.WARNING, contains="You do not need to include" ) assert filtered_log if warn_arg else not filtered_log @pytest.mark.parametrize( ('reason', 'error_type'), [ ('not dir', WorkflowFilesError), ('not symlink', WorkflowFilesError), ('broken symlink', WorkflowFilesError), ('invalid target', WorkflowFilesError), ('not exist', InputError) ] ) def test_infer_latest_run__bad( reason: str, error_type: Type[Exception], tmp_run_dir: Callable, ) -> None: # -- Setup -- cylc_run_dir: Path = tmp_run_dir() run_dir = cylc_run_dir / 'sulu' run_dir.mkdir() runN_path = run_dir / 'runN' err_msg = f"{runN_path} symlink not valid" if reason == 'not dir': (run_dir / 'run1').touch() runN_path.symlink_to('run1') elif reason == 'not symlink': runN_path.mkdir() elif reason == 'broken symlink': runN_path.symlink_to('run1') elif reason == 'invalid target': # noqa: SIM106 (run_dir / 'palpatine').mkdir() runN_path.symlink_to('palpatine') err_msg = ( f"{runN_path} symlink target not valid: palpatine" ) elif reason == 'not exist': run_dir = run_dir / 'not-exist' err_msg = ( f"Workflow ID not found: sulu/not-exist\n" f"(Directory not found: {run_dir})" ) else: raise ValueError(reason) # -- Test -- with pytest.raises(error_type) as excinfo: infer_latest_run(run_dir) assert str(excinfo.value) == err_msg @pytest.mark.parametrize( 'filetree, expected', [ pytest.param( FILETREE_1, {'log': 'sym/cylc-run/foo/bar/log'}, id="filetree1" ), pytest.param( FILETREE_2, { 'share/cycle': 'sym-cycle/cylc-run/foo/bar/share/cycle', 'share': 'sym-share/cylc-run/foo/bar/share', '': 'sym-run/cylc-run/foo/bar/' }, id="filetree2" ), pytest.param( FILETREE_3, { 'share/cycle': 'sym-cycle/cylc-run/foo/bar/share/cycle', '': 'sym-run/cylc-run/foo/bar/' }, id="filetree3" ), pytest.param( FILETREE_4, {'share/cycle': 'sym-cycle/cylc-run/foo/bar/share/cycle'}, id="filetree4" ), ] ) def test_get_symlink_dirs( filetree: Dict[str, Any], expected: Dict[str, Union[Path, str]], tmp_run_dir: Callable, tmp_path: Path ): """Test get_symlink_dirs(). Params: filetree: The directory structure to test against. expected: The expected return dictionary, except with the values being relative to tmp_path instead of absolute paths. """ # Setup cylc_run_dir = tmp_run_dir() create_filetree(filetree, tmp_path, tmp_path) id_ = 'foo/bar' for k, v in expected.items(): expected[k] = Path(tmp_path / v) # Test assert get_symlink_dirs(id_, cylc_run_dir / id_) == expected @pytest.mark.parametrize( 'flow_file_exists, suiterc_exists, expected_file', [(True, False, WorkflowFiles.FLOW_FILE), (True, True, None), (False, True, WorkflowFiles.SUITE_RC)] ) def test_check_flow_file( flow_file_exists: bool, suiterc_exists: bool, expected_file: str, tmp_path: Path ) -> None: """Test check_flow_file() returns the expected path. Params: flow_file_exists: Whether a flow.cylc file is found in the dir. suiterc_exists: Whether a suite.rc file is found in the dir. expected_file: Which file's path should get returned. """ if flow_file_exists: tmp_path.joinpath(WorkflowFiles.FLOW_FILE).touch() if suiterc_exists: tmp_path.joinpath(WorkflowFiles.SUITE_RC).touch() if expected_file is None: with pytest.raises(WorkflowFilesError) as exc: check_flow_file(tmp_path) assert str(exc.value) == ( "Both flow.cylc and suite.rc files are present in " f"{tmp_path}. Please remove one and try again. " "For more information visit: " "https://cylc.github.io/cylc-doc/stable/html/7-to-8/summary.html" "#backward-compatibility" ) else: assert check_flow_file(tmp_path) == tmp_path.joinpath(expected_file) def test_detect_both_flow_and_suite(tmp_path): """Test flow.cylc and suite.rc (as files) together in dir raises error.""" tmp_path.joinpath(WorkflowFiles.FLOW_FILE).touch() tmp_path.joinpath(WorkflowFiles.SUITE_RC).touch() forbidden = is_forbidden(tmp_path / WorkflowFiles.FLOW_FILE) assert forbidden is True with pytest.raises(WorkflowFilesError) as exc: detect_both_flow_and_suite(tmp_path) assert str(exc.value) == ( f"Both flow.cylc and suite.rc files are present in {tmp_path}. Please " "remove one and try again. For more information visit: " "https://cylc.github.io/cylc-doc/stable/html/7-to-8/" "summary.html#backward-compatibility" ) def test_detect_both_flow_and_suite_symlinked(tmp_path): """Test flow.cylc symlinked to suite.rc together in dir is permitted.""" (tmp_path / WorkflowFiles.SUITE_RC).touch() flow_file = tmp_path.joinpath(WorkflowFiles.FLOW_FILE) flow_file.symlink_to(WorkflowFiles.SUITE_RC) detect_both_flow_and_suite(tmp_path) def test_flow_symlinked_elsewhere_and_suite_present(tmp_path: Path): """flow.cylc symlinked to suite.rc elsewhere, and suite.rc in dir raises""" tmp_path.joinpath('some_other_dir').mkdir(exist_ok=True) suite_file = tmp_path.joinpath('some_other_dir', WorkflowFiles.SUITE_RC) suite_file.touch() run_dir = tmp_path.joinpath('run_dir') run_dir.mkdir(exist_ok=True) flow_file = (run_dir / WorkflowFiles.FLOW_FILE) flow_file.symlink_to(suite_file) forbidden_external = is_forbidden(flow_file) assert forbidden_external is False (run_dir / WorkflowFiles.SUITE_RC).touch() forbidden = is_forbidden(flow_file) assert forbidden is True with pytest.raises(WorkflowFilesError) as exc: detect_both_flow_and_suite(run_dir) assert str(exc.value).startswith( "Both flow.cylc and suite.rc files are present in " f"{run_dir}. Please remove one and try again." ) def test_is_forbidden_symlink_returns_false_for_non_symlink(tmp_path): """Test sending a non symlink path is not marked as forbidden""" flow_file = (tmp_path / WorkflowFiles.FLOW_FILE) flow_file.touch() forbidden = is_forbidden(Path(flow_file)) assert forbidden is False @pytest.mark.parametrize( 'flow_file_target, suiterc_exists, err, expected_file', [ pytest.param( WorkflowFiles.SUITE_RC, True, None, WorkflowFiles.FLOW_FILE, id="flow.cylc symlinked to suite.rc" ), pytest.param( WorkflowFiles.SUITE_RC, False, WorkflowFilesError, None, id="flow.cylc symlinked to non-existent suite.rc" ), pytest.param( 'inside.cylc', True, WorkflowFilesError, None, id="flow.cylc symlinked to file in run dir, suite.rc exists" ), pytest.param( 'inside.cylc', False, None, WorkflowFiles.FLOW_FILE, id="flow.cylc symlinked to file in run dir, no suite.rc" ), pytest.param( '../outside.cylc', True, WorkflowFilesError, None, id="flow.cylc symlinked to file outside, suite.rc exists" ), pytest.param( '../outside.cylc', False, None, WorkflowFiles.FLOW_FILE, id="flow.cylc symlinked to file outside, no suite.rc" ), pytest.param( None, True, None, WorkflowFiles.SUITE_RC, id="No flow.cylc, suite.rc exists" ), pytest.param( None, False, WorkflowFilesError, None, id="No flow.cylc, no suite.rc" ), ] ) def test_check_flow_file_symlink( flow_file_target: Optional[str], suiterc_exists: bool, err: Optional[Type[Exception]], expected_file: Optional[str], tmp_path: Path ) -> None: """Test check_flow_file() when flow.cylc is a symlink or doesn't exist. Params: flow_file_target: Relative path of the flow.cylc symlink's target, or None if the symlink doesn't exist. suiterc_exists: Whether there is a suite.rc file in the dir. err: Type of exception if expected to get raised. expected_file: Which file's path should get returned, when symlink_suiterc_arg is FALSE (otherwise it will always be flow.cylc, assuming no exception occurred). """ run_dir = tmp_path / 'espresso' flow_file = run_dir / WorkflowFiles.FLOW_FILE suiterc = run_dir / WorkflowFiles.SUITE_RC run_dir.mkdir() (run_dir / '../outside.cylc').touch() (run_dir / 'inside.cylc').touch() if suiterc_exists: suiterc.touch() if flow_file_target: flow_file.symlink_to(flow_file_target) if err: with pytest.raises(err): check_flow_file(run_dir) else: assert expected_file is not None # otherwise test is wrong result = check_flow_file(run_dir) assert result == run_dir / expected_file @pytest.mark.parametrize( 'id_, installed, named, expected', [('reg1/run1', True, True, True), ('reg2', True, False, True), ('reg3', False, False, False)] ) def test_is_installed(tmp_run_dir: Callable, id_, installed, named, expected): """Test is_installed correctly identifies presence of _cylc-install dir""" cylc_run_dir: Path = tmp_run_dir(id_, installed=installed, named=named) actual = is_installed(cylc_run_dir) assert actual == expected def test_validate_abort_if_flow_file_in_path(): assert abort_if_flow_file_in_path(Path("path/to/wflow")) is None with pytest.raises(InputError) as exc_info: abort_if_flow_file_in_path(Path("path/to/wflow/flow.cylc")) assert "Not a valid workflow ID or source directory" in str(exc_info.value) cylc-flow-8.6.4/tests/unit/test_id.py0000664000175000017500000003200015202510242017726 0ustar alastairalastair# THIS FILE IS PART OF THE CYLC SUITE 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 . """Test the Cylc universal identifier system.""" import pytest from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.id import ( LEGACY_CYCLE_SLASH_TASK, LEGACY_TASK_DOT_CYCLE, RELATIVE_ID, UNIVERSAL_ID, Tokens, quick_relative_id, ) @pytest.mark.parametrize( 'identifier', [ '', '~', '~user//cycle' '~flow:state', 'flow:flow_sel:flow_sel', ] ) def test_univseral_id_illegal(identifier): """Test illegal formats of the universal identifier.""" assert UNIVERSAL_ID.match(identifier) is None @pytest.mark.parametrize( 'identifier', [ '~user', '~user/', '~user/workflow', '~user/workflow//', '~user/workflow:workflow_sel', '~user/workflow:workflow_sel//', '~user/workflow:workflow_sel//cycle', '~user/workflow:workflow_sel//cycle/', '~user/workflow:workflow_sel//cycle:cycle_sel', '~user/workflow:workflow_sel//cycle:cycle_sel/', '~user/workflow:workflow_sel//cycle:cycle_sel/task', '~user/workflow:workflow_sel//cycle:cycle_sel/task/', '~user/workflow:workflow_sel//cycle:cycle_sel/task:task_sel', '~user/workflow:workflow_sel//cycle:cycle_sel/task:task_sel/', '~user/workflow:workflow_sel//cycle:cycle_sel/task:task_sel/job', ( '~user/workflow:workflow_sel//cycle:cycle_sel/task:task_sel/job' ':job_sel' ), 'workflow', 'workflow//', 'workflow:workflow_sel', 'workflow:workflow_sel//', 'workflow:workflow_sel//cycle', 'workflow:workflow_sel//cycle/', 'workflow:workflow_sel//cycle:cycle_sel', 'workflow:workflow_sel//cycle:cycle_sel/', 'workflow:workflow_sel//cycle:cycle_sel/task', 'workflow:workflow_sel//cycle:cycle_sel/task/', 'workflow:workflow_sel//cycle:cycle_sel/task:task_sel', 'workflow:workflow_sel//cycle:cycle_sel/task:task_sel/', 'workflow:workflow_sel//cycle:cycle_sel/task:task_sel/job', 'workflow:workflow_sel//cycle:cycle_sel/task:task_sel/job:job_sel' ] ) def test_universal_id_matches(identifier): """test every legal format of the universal identifier.""" # fmt: off expected_tokens = { 'user': 'user' if 'user' in identifier else None, 'workflow': 'workflow' if 'workflow' in identifier else None, 'workflow_sel': 'workflow_sel' if 'workflow_sel' in identifier else None, 'cycle': 'cycle' if 'cycle' in identifier else None, 'cycle_sel': 'cycle_sel' if 'cycle_sel' in identifier else None, 'task': 'task' if 'task' in identifier else None, 'task_sel': 'task_sel' if 'task_sel' in identifier else None, 'job': 'job' if 'job' in identifier else None, 'job_sel': 'job_sel' if 'job_sel' in identifier else None } # fmt: on match = UNIVERSAL_ID.match(identifier) assert match assert match.groupdict() == expected_tokens @pytest.mark.parametrize( 'identifier', [ '~user/a/b/c', '~user/a/b/c//', '~user/a/b/c:workflow_sel', '~user/a/b/c:workflow_sel//', '~user/a/b/c:workflow_sel//cycle', '~user/a/b/c:workflow_sel//cycle/', '~user/a/b/c:workflow_sel//cycle:cycle_sel', '~user/a/b/c:workflow_sel//cycle:cycle_sel/', '~user/a/b/c:workflow_sel//cycle:cycle_sel/task', '~user/a/b/c:workflow_sel//cycle:cycle_sel/task/', '~user/a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel', '~user/a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/', '~user/a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/job', ( '~user/a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/job' ':job_sel' ), 'a/b/c', 'a/b/c//', 'a/b/c:workflow_sel', 'a/b/c:workflow_sel//', 'a/b/c:workflow_sel//cycle', 'a/b/c:workflow_sel//cycle/', 'a/b/c:workflow_sel//cycle:cycle_sel', 'a/b/c:workflow_sel//cycle:cycle_sel/', 'a/b/c:workflow_sel//cycle:cycle_sel/task', 'a/b/c:workflow_sel//cycle:cycle_sel/task/', 'a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel', 'a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/', 'a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/job', 'a/b/c:workflow_sel//cycle:cycle_sel/task:task_sel/job:job_sel' ] ) def test_universal_id_matches_hierarchical(identifier): """Test the UID with hierarchical workflow IDs.""" # fmt: off expected_tokens = { 'user': 'user' if 'user' in identifier else None, 'workflow': 'a/b/c', # the hierarchical workflow ID 'workflow_sel': 'workflow_sel' if 'workflow_sel' in identifier else None, 'cycle': 'cycle' if 'cycle' in identifier else None, 'cycle_sel': 'cycle_sel' if 'cycle_sel' in identifier else None, 'task': 'task' if 'task' in identifier else None, 'task_sel': 'task_sel' if 'task_sel' in identifier else None, 'job': 'job' if 'job' in identifier else None, 'job_sel': 'job_sel' if 'job_sel' in identifier else None } # fmt: on match = UNIVERSAL_ID.match(identifier) assert match assert match.groupdict() == expected_tokens @pytest.mark.parametrize( 'identifier', [ '', '~', ':', 'workflow//cycle', 'task:task_sel:task_sel', 'cycle/task' '//', '//~', '//:', '//workflow//cycle', '//cycle/task:task_sel:task_sel' ] ) def test_relative_id_illegal(identifier): """Test illegal formats of the universal identifier.""" assert RELATIVE_ID.match(identifier) is None @pytest.mark.parametrize( 'identifier', [ '//cycle', '//cycle/', '//cycle:cycle_sel', '//cycle:cycle_sel/', '//cycle:cycle_sel/task', '//cycle:cycle_sel/task/', '//cycle:cycle_sel/task:task_sel', '//cycle:cycle_sel/task:task_sel/', '//cycle:cycle_sel/task:task_sel/job', '//cycle:cycle_sel/task:task_sel/job:job_sel', ] ) def test_relative_id_matches(identifier): """test every legal format of the relative identifier.""" expected_tokens = { 'cycle': 'cycle' if 'cycle' in identifier else None, 'cycle_sel': 'cycle_sel' if 'cycle_sel' in identifier else None, 'task': 'task' if 'task' in identifier else None, 'task_sel': 'task_sel' if 'task_sel' in identifier else None, 'job': 'job' if 'job' in identifier else None, 'job_sel': 'job_sel' if 'job_sel' in identifier else None } match = RELATIVE_ID.match(identifier) assert match assert match.groupdict() == expected_tokens @pytest.mark.parametrize( 'identifier', [ '', '~', '/', ':', 'task.cycle', # the first digit of the cycle should be a number '//task.123', # don't match the new format 'task.cycle/job', 'task:task_sel.123' # selector should suffix the cycle ] ) def test_legacy_task_dot_cycle_illegal(identifier): """Test illegal formats of the legacy task.cycle identifier.""" assert LEGACY_TASK_DOT_CYCLE.match(identifier) is None @pytest.mark.parametrize( 'identifier,expected_tokens', [ ( 'task.1', # integer cycles can be one character long {'task': 'task', 'cycle': '1', 'task_sel': None} ), ( 't.a.s.k.123', {'task': 't.a.s.k', 'cycle': '123', 'task_sel': None} ), ( 'task.123:task_sel', {'task': 'task', 'cycle': '123', 'task_sel': 'task_sel'} ), ] ) def test_legacy_task_dot_cycle_matches(identifier, expected_tokens): match = LEGACY_TASK_DOT_CYCLE.match(identifier) assert match assert match.groupdict() == expected_tokens @pytest.mark.parametrize( 'identifier', [ '', '~', '/', ':', 'cycle/task', # the first digit of the cycle should be a number '//123/task', # don't match the new format 'cycle/task/job' ] ) def test_legacy_cycle_slash_task_illegal(identifier): """Test illegal formats of the legacy cycle/task identifier.""" assert LEGACY_CYCLE_SLASH_TASK.match(identifier) is None @pytest.mark.parametrize( 'identifier,expected_tokens', [ ( '123/task', {'task': 'task', 'cycle': '123', 'task_sel': None} ), ( '123/t.a.s.k', {'task': 't.a.s.k', 'cycle': '123', 'task_sel': None} ), ( '123/task:task_sel', {'task': 'task', 'cycle': '123', 'task_sel': 'task_sel'} ) ] ) def test_legacy_cycle_slash_task_matches(identifier, expected_tokens): match = LEGACY_CYCLE_SLASH_TASK.match(identifier) assert match assert match.groupdict() == expected_tokens def test_tokens(): # tested mainly in doctests Tokens('a') with pytest.raises(ValueError): Tokens('a', 'b') Tokens(cycle='a') with pytest.raises(ValueError): Tokens(foo='a') Tokens().duplicate(cycle='a') with pytest.raises(ValueError): Tokens(foo='a') # test equality assert Tokens('a') == Tokens('a') assert Tokens('a') != Tokens('b') assert Tokens('a', relative=True) == Tokens('a', relative=True) assert Tokens('a', relative=True) != Tokens('b', relative=True) assert Tokens() != Tokens('a') assert Tokens(workflow='a') == Tokens('a') # test equality with non Tokens objects assert Tokens('a') != 'a' assert not Tokens('a') == 'a' assert Tokens('a') != 1 assert not Tokens('a') == 1 tokens = Tokens('a//b') new_tokens = tokens.duplicate(cycle='c', task='d') assert new_tokens == Tokens('a//c/d') with pytest.raises(Exception): tokens.update({'foo': 'c'}) with pytest.raises(Exception): tokens['cycle'] = 'a' # test gt/lt assert sorted( tokens.id for tokens in [ Tokens('~u/c'), Tokens('~u/b//1'), Tokens('~u/a'), Tokens('~u/b'), Tokens('~u/b//2'), ] ) == ['~u/a', '~u/b', '~u/b//1', '~u/b//2', '~u/c'] def test_no_look_behind(): """Ensure the UID pattern does not use lookbehinds. Ideally this pattern should be work for both cylc-flow and cylc-ui. 2022-01-11: * Lookbehind support is at ~75% * https://caniuse.com/js-regexp-lookbehind """ assert '?<=' not in UNIVERSAL_ID.pattern assert '?. from contextlib import suppress from pathlib import Path from time import sleep import pytest from typing import (Any, Optional) from unittest.mock import MagicMock, Mock from cylc.flow.exceptions import PlatformError from cylc.flow.network.client_factory import CommsMeth from cylc.flow.task_remote_mgr import ( REMOTE_FILE_INSTALL_DONE, REMOTE_INIT_IN_PROGRESS, TaskRemoteMgr) from cylc.flow.workflow_files import WorkflowFiles, get_workflow_srv_dir Fixture = Any @pytest.mark.parametrize( 'comms_meth, expected', [ (CommsMeth.SSH, True), (CommsMeth.ZMQ, True), (CommsMeth.POLL, False) ] ) def test__remote_init_items(comms_meth: CommsMeth, expected: bool): """Test _remote_init_items(). Should only includes files under .service/ """ id_ = 'barclay' mock_mgr = Mock(workflow=id_) srv_dir = get_workflow_srv_dir(id_) items = TaskRemoteMgr._remote_init_items(mock_mgr, comms_meth) if expected: assert items for src_path, dst_path in items: Path(src_path).relative_to(srv_dir) Path(dst_path).relative_to(WorkflowFiles.Service.DIRNAME) else: assert not items @pytest.mark.parametrize( 'install_target, skip_expected, expected_status', [('localhost', True, REMOTE_FILE_INSTALL_DONE), ('something_else', False, REMOTE_INIT_IN_PROGRESS)] ) def test_remote_init_skip( install_target: str, skip_expected: bool, expected_status: str, monkeypatch: Fixture): """Test the TaskRemoteMgr.remote_init() skips localhost install target. Params: install_target: The platform's install target. skip_expected: Whether remote init is expected to be skipped. expected_status: The expected value of TaskRemoteMgr.remote_init_map[install_target]. """ platform = { 'install target': install_target, 'communication method': CommsMeth.POLL, 'hosts': ['localhost'], 'selection': {'method': 'random'}, 'name': 'foo' } mock_task_remote_mgr = MagicMock(remote_init_map={}, bad_hosts=[]) mock_construct_ssh_cmd = Mock() monkeypatch.setattr('cylc.flow.task_remote_mgr.construct_ssh_cmd', mock_construct_ssh_cmd) for item in ( 'tarfile', 'get_remote_workflow_run_dir', 'get_dirs_to_symlink'): monkeypatch.setattr(f'cylc.flow.task_remote_mgr.{item}', MagicMock()) TaskRemoteMgr.remote_init(mock_task_remote_mgr, platform) call_expected = not skip_expected assert mock_task_remote_mgr._remote_init_items.called is call_expected assert mock_construct_ssh_cmd.called is call_expected assert mock_task_remote_mgr.proc_pool.put_command.called is call_expected status = mock_task_remote_mgr.remote_init_map[install_target] assert status == expected_status @pytest.mark.parametrize( 'install_target, load_type, expected', [ ('install_target', None, '03-start-install_target.log'), ('some_install_target', 'restart', '03-restart-some_install_target.log'), ('another_install_target', 'reload', '03-reload-another_install_target.log') ] ) def test_get_log_file_name(tmp_path: Path, install_target: str, load_type: Optional[str], expected: str): task_remote_mgr = TaskRemoteMgr('some_workflow', None, None, None, None) if load_type == 'restart': task_remote_mgr.is_restart = True elif load_type == 'reload': task_remote_mgr.is_reload = True # else load type is start (no flag required) run_dir = tmp_path log_dir = run_dir / 'some_workflow' / 'log' / 'remote-install' log_dir.mkdir(parents=True) for log_num in range(1, 3): Path(f"{log_dir}/{log_num:02d}-start-{install_target}.log").touch() sleep(0.1) log_name = task_remote_mgr.get_log_file_name( install_target, install_log_dir=log_dir) assert log_name == expected @pytest.mark.parametrize( 'platform_names, install_targets, glblcfg, expect', [ pytest.param( # Two platforms share an install target. Both are reachable. ['sir_handel', 'peter_sam'], ['mountain_railway'], ''' [platforms] [[peter_sam, sir_handel]] install target = mountain_railway ''', { 'targets': {'mountain_railway': ['peter_sam', 'sir_handel']}, 'unreachable': set() }, id='basic' ), pytest.param( # Two platforms share an install target. Both are unreachable. set(), ['mountain_railway'], ''' [platforms] [[peter_sam, sir_handel]] install target = mountain_railway ''', { 'targets': {'mountain_railway': []}, 'unreachable': {'mountain_railway'} }, id='platform_unreachable' ), pytest.param( # One of our install targets matches one of our platforms, # but only implicitly; i.e. the platform name is the same as the # install target name. ['sir_handel'], ['sir_handel'], ''' [platforms] [[sir_handel]] ''', { 'targets': {'sir_handel': ['sir_handel']}, 'unreachable': set() }, id='implicit-target' ), pytest.param( # One of our install targets matches one of our platforms, # but only implicitly, and the platform name is defined using a # regex. ['sir_handel42'], ['sir_handel42'], ''' [platforms] [[sir_handel..]] ''', { 'targets': {'sir_handel42': ['sir_handel42']}, 'unreachable': set() }, id='implicit-target-regex' ), pytest.param( # One of our install targets (rusty) has no defined platforms # causing a PlatformLookupError. ['duncan', 'rusty'], ['mountain_railway', 'rusty'], ''' [platforms] [[duncan]] install target = mountain_railway ''', { 'targets': {'mountain_railway': ['duncan']}, 'unreachable': {'rusty'} }, id='PlatformLookupError' ) ] ) def test_map_platforms_used_for_install_targets( mock_glbl_cfg, platform_names, install_targets, glblcfg, expect, caplog ): def flatten_install_targets_map(itm): result = {} for target, platforms in itm.items(): result[target] = sorted([p['name'] for p in platforms]) return result mock_glbl_cfg('cylc.flow.platforms.glbl_cfg', glblcfg) install_targets_map = TaskRemoteMgr._get_remote_tidy_targets( set(platform_names), set(install_targets)) with suppress(KeyError): install_targets_map.pop('localhost') assert ( expect['targets'] == flatten_install_targets_map(install_targets_map)) if expect['unreachable']: for unreachable in expect["unreachable"]: assert ( unreachable in caplog.records[0].msg) else: assert not caplog.records shared_eval_params = [ pytest.param( 'localhost', {}, 'localhost', id="localhost" ), pytest.param( '$(some-cmd)', {}, None, id="subshell_1st_eval" ), pytest.param( '$(some-cmd)', {'some-cmd': None}, None, id="subshell_awaiting_eval" ), pytest.param( '$(some-cmd)', {'some-cmd': 'isaac clarke'}, 'isaac clarke', id="subshell_finished_eval" ), pytest.param( '$SOME_ENV', {}, 'nolan stross', id="env_var" ), pytest.param( '$(env-cmd)', {'env-cmd': '$SOME_ENV'}, 'nolan stross', id="subshell_env_var" ), pytest.param( 'titan_station', {}, 'titan_station', id="verbatim_name" ), pytest.param( '', {}, 'localhost', id="empty" ), ] @pytest.fixture def task_remote_mgr_eval(monkeypatch: pytest.MonkeyPatch): """Fixture providing a task remote manager for eval_platform() & eval_host() tests.""" def _task_remote_mgr_eval(remote_cmd_map: dict) -> TaskRemoteMgr: monkeypatch.setenv('SOME_ENV', 'nolan stross') task_remote_mgr = TaskRemoteMgr( workflow='usg_ishimura', proc_pool=Mock(), bad_hosts=[], db_mgr=None, server=None ) task_remote_mgr.remote_command_map = remote_cmd_map return task_remote_mgr return _task_remote_mgr_eval @pytest.mark.parametrize( 'eval_str, remote_cmd_map, expected', [ *shared_eval_params, pytest.param( 'localhost_tau_volantis', {}, 'localhost_tau_volantis', id="name_begin_w_localhost" ) ] ) def test_eval_platform( eval_str: str, remote_cmd_map: dict, expected: Optional[str], task_remote_mgr_eval, ): task_remote_mgr: TaskRemoteMgr = task_remote_mgr_eval(remote_cmd_map) assert task_remote_mgr.eval_platform(eval_str) == expected def test_eval_platform_bad(task_remote_mgr_eval): exc_msg = "foreign contaminant detected" task_remote_mgr: TaskRemoteMgr = task_remote_mgr_eval( {'cmd': PlatformError(exc_msg, 'aegis_vii')} ) with pytest.raises(PlatformError, match=exc_msg): task_remote_mgr.eval_platform('$(cmd)') @pytest.mark.parametrize( 'eval_str, remote_cmd_map, expected', [ *shared_eval_params, pytest.param( 'localhost4.localdomain4', {}, 'localhost', id="localhost_variant" ), pytest.param( '`other cmd`', {'other cmd': 'nicole brennan'}, 'nicole brennan', id="backticks" ), ] ) def test_eval_host( eval_str: str, remote_cmd_map: dict, expected: Optional[str], task_remote_mgr_eval, ): task_remote_mgr: TaskRemoteMgr = task_remote_mgr_eval(remote_cmd_map) assert task_remote_mgr.eval_host(eval_str) == expected cylc-flow-8.6.4/tests/unit/post_install/0000775000175000017500000000000015202510242020441 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/post_install/test_log_vc_info.py0000664000175000017500000002413615202510242024344 0ustar alastairalastair# 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 . import json import logging from pathlib import Path import pytest from pytest import MonkeyPatch, TempPathFactory from secrets import token_hex import shutil import subprocess from typing import Any, Callable, Tuple from unittest.mock import Mock from cylc.flow.install_plugins.log_vc_info import ( INFO_FILENAME, VCSNotInstalledError, _get_git_commit, _run_cmd, get_status, get_vc_info, main, write_diff, ) from cylc.flow.workflow_files import WorkflowFiles Fixture = Any BASIC_FLOW_1 = """ [scheduling] [[graph]] R1 = foo """ BASIC_FLOW_2 = """ [scheduling] [[graph]] R1 = bar """ require_git = pytest.mark.skipif( shutil.which('git') is None, reason="git is not installed" ) require_svn = pytest.mark.skipif( shutil.which('svn') is None, reason="svn is not installed" ) @pytest.fixture(scope='module') def git_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str]: """Init a git repo for a workflow source dir. The repo has uncommitted changes. This dir is reused by all tests requesting it in this module. Returns (source_dir_path, commit_hash) """ source_dir: Path = tmp_path_factory.getbasetemp() / 'git_repo' source_dir.mkdir() subprocess.run(['git', 'init'], cwd=source_dir, check=True) flow_file = source_dir / 'flow.cylc' flow_file.write_text(BASIC_FLOW_1) subprocess.run(['git', 'add', '-A'], cwd=source_dir, check=True) subprocess.run( ['git', 'commit', '-am', '"Initial commit"'], cwd=source_dir, check=True, capture_output=True) # Overwrite file to introduce uncommitted changes: flow_file.write_text(BASIC_FLOW_2) # Also add new file: (source_dir / 'gandalf.md').touch() commit_sha = subprocess.run( ['git', 'rev-parse', 'HEAD'], cwd=source_dir, check=True, capture_output=True, text=True ).stdout.splitlines()[0] return (str(source_dir), commit_sha) @pytest.fixture(scope='module') def svn_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str, str]: """Init an svn repo & working copy for a workflow source dir. The working copy has a flow.cylc file with uncommitted changes. This dir is reused by all tests requesting it in this module. Returns (source_dir_path, repository_UUID, repository_path) """ tmp_path: Path = tmp_path_factory.getbasetemp() repo = tmp_path.joinpath('svn_repo') subprocess.run( ['svnadmin', 'create', 'svn_repo'], cwd=tmp_path, check=True) uuid = subprocess.run( ['svnlook', 'uuid', repo], check=True, capture_output=True, text=True ).stdout.splitlines()[0] project_dir = tmp_path.joinpath('project') project_dir.mkdir() project_dir.joinpath('flow.cylc').write_text(BASIC_FLOW_1) subprocess.run( ['svn', 'import', project_dir, f'file://{repo}/project/trunk', '-m', '"Initial import"'], check=True) source_dir = tmp_path.joinpath('svn_working_copy') subprocess.run( ['svn', 'checkout', f'file://{repo}/project/trunk', source_dir], check=True) flow_file = source_dir.joinpath('flow.cylc') # Overwrite file to introduce uncommitted changes: flow_file.write_text(BASIC_FLOW_2) return (str(source_dir), uuid, str(repo)) @require_git def test_get_git_commit(git_source_repo: Tuple[str, str]): """Test get_git_commit()""" source_dir, commit_sha = git_source_repo assert _get_git_commit(source_dir) == commit_sha @require_git def test_get_status_git(git_source_repo: Tuple[str, str]): """Test get_status() for a git repo""" source_dir, commit_sha = git_source_repo assert get_status('git', source_dir) == [ " M flow.cylc", "?? gandalf.md" ] @require_git def test_get_vc_info_git(git_source_repo: Tuple[str, str]): """Test get_vc_info() for a git repo""" source_dir, commit_sha = git_source_repo vc_info = get_vc_info(source_dir) assert vc_info is not None expected = [ ('version control system', "git"), ('repository version', f"{commit_sha[:7]}-dirty"), ('commit', commit_sha), ('working copy root path', source_dir), ('status', [ " M flow.cylc", "?? gandalf.md" ]) ] assert list(vc_info.items()) == expected @require_git def test_write_diff_git(git_source_repo: Tuple[str, str], tmp_path: Path): """Test write_diff() for a git repo""" source_dir, _ = git_source_repo run_dir = tmp_path / 'run_dir' (run_dir / WorkflowFiles.LogDir.DIRNAME).mkdir(parents=True) diff_file = write_diff('git', source_dir, run_dir) diff_lines = diff_file.read_text().splitlines() assert diff_lines[0].startswith("# Auto-generated diff") for line in ("diff --git a/flow.cylc b/flow.cylc", "- R1 = foo", "+ R1 = bar"): assert line in diff_lines @require_git def test_main_git(git_source_repo: Tuple[str, str], tmp_run_dir: Callable): """Test the written JSON info file.""" source_dir, _ = git_source_repo run_dir: Path = tmp_run_dir('frodo') main(source_dir, None, run_dir) with open( run_dir / WorkflowFiles.LogDir.DIRNAME / WorkflowFiles.LogDir.VERSION / INFO_FILENAME, 'r' ) as f: loaded = json.loads(f.read()) assert isinstance(loaded, dict) assert loaded['version control system'] == 'git' assert isinstance(loaded['status'], list) assert len(loaded['status']) == 2 @require_svn def test_get_vc_info_svn(svn_source_repo: Tuple[str, str, str]): """Test get_vc_info() for an svn working copy""" source_dir, uuid, repo_path = svn_source_repo vc_info = get_vc_info(source_dir) assert vc_info is not None expected = [ ('version control system', "svn"), ('working copy root path', str(source_dir)), ('url', f"file://{repo_path}/project/trunk"), ('repository uuid', uuid), ('revision', "1"), ('status', ["M flow.cylc"]) ] assert list(vc_info.items()) == expected @require_svn def test_write_diff_svn(svn_source_repo: Tuple[str, str, str], tmp_path: Path): """Test write_diff() for an svn working copy""" source_dir, _, _ = svn_source_repo run_dir = tmp_path / 'run_dir' (run_dir / WorkflowFiles.LogDir.DIRNAME).mkdir(parents=True) diff_file = write_diff('svn', source_dir, run_dir) diff_lines = diff_file.read_text().splitlines() assert diff_lines[0].startswith("# Auto-generated diff") for line in (f"--- {source_dir}/flow.cylc (revision 1)", f"+++ {source_dir}/flow.cylc (working copy)", "- R1 = foo", "+ R1 = bar"): assert line in diff_lines def test_not_repo(tmp_path: Path, monkeypatch: MonkeyPatch): """Test get_vc_info() and main() for a dir that is not a supported repo""" source_dir = Path(tmp_path, 'git_repo') source_dir.mkdir() flow_file = source_dir.joinpath('flow.cylc') flow_file.write_text(BASIC_FLOW_1) mock_write_vc_info = Mock() monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_vc_info', mock_write_vc_info) mock_write_diff = Mock() monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_diff', mock_write_diff) assert get_vc_info(source_dir) is None assert main(source_dir, None, None) is False # type: ignore assert mock_write_vc_info.called is False assert mock_write_diff.called is False @require_git def test_no_base_commit_git(tmp_path: Path): """Test get_vc_info() and write_diff() for a recently init'd git source dir that does not have a base commit yet.""" source_dir = Path(tmp_path, 'new_git_repo') source_dir.mkdir() subprocess.run(['git', 'init'], cwd=source_dir, check=True) flow_file = source_dir.joinpath('flow.cylc') flow_file.write_text(BASIC_FLOW_1) run_dir = tmp_path / 'run_dir' (run_dir / WorkflowFiles.LogDir.DIRNAME).mkdir(parents=True) vc_info = get_vc_info(source_dir) assert vc_info is not None assert list(vc_info.items()) == [ ('version control system', "git"), ('working copy root path', str(source_dir)), ('status', ["?? flow.cylc"]) ] # Diff file expected to be empty (only containing comment lines), # but should work without raising diff_file = write_diff('git', source_dir, run_dir) for line in diff_file.read_text().splitlines(): assert line.startswith('#') @require_svn def test_untracked_svn_subdir( svn_source_repo: Tuple[str, str, str], log_filter ): repo_dir, *_ = svn_source_repo source_dir = Path(repo_dir, 'jar_jar_binks') source_dir.mkdir() assert get_vc_info(source_dir) is None assert log_filter(logging.WARNING, contains="$ svn info") def test_not_installed( tmp_path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, log_filter, ): """Test what happens if version control software is not installed.""" fake_vcs = token_hex(8) with pytest.raises(VCSNotInstalledError): _run_cmd(fake_vcs, [], cwd=tmp_path) monkeypatch.setattr( 'cylc.flow.install_plugins.log_vc_info.INFO_COMMANDS', {fake_vcs: []} ) caplog.set_level(logging.DEBUG) assert get_vc_info(tmp_path) is None assert log_filter( logging.DEBUG, contains=f"{fake_vcs} does not appear to be installed", ) cylc-flow-8.6.4/tests/unit/test_platforms_get_platform.py0000664000175000017500000002103315202510242024110 0ustar alastairalastair# 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 . # # Tests for the platform lookup module's get_platform method. from typing import Callable, Dict, Optional import pytest from cylc.flow.platforms import ( get_localhost_install_target, get_platform ) from cylc.flow.exceptions import PlatformLookupError def test_get_platform_no_args(): # If no task conf is given, we get localhost args. assert get_platform()['hosts'] == ['localhost'] @pytest.mark.parametrize( 'platform_re', [ None, 'localhost', 'localhost, otherplatform', 'otherplatform, localhost', 'localhost, xylophone\\d{1,5}' ] ) def test_get_localhost_platform(mock_glbl_cfg, platform_re): # Check that an arbitrary string name returns a sensible platform mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', f''' [platforms] [[localhost]] hosts = localhost ssh command = ssh -oConnectTimeout=42 [[{platform_re}]] hosts = localhost ssh command = ssh -oConnectTimeout=24 ''' ) platform = get_platform('localhost') if platform_re: assert platform['ssh command'] == 'ssh -oConnectTimeout=24' else: assert platform['ssh command'] == 'ssh -oConnectTimeout=42' @pytest.mark.parametrize( 'platform_re', [ 'saffron', 'sumac|saffron', 'sumac, saffron', 'sumac|asafoetida, saffron', ] ) def test_get_platform_from_platform_name_str(mock_glbl_cfg, platform_re): # Check that an arbitrary string name returns a sensible platform mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', f''' [platforms] [[{platform_re}]] hosts = saff01 job runner = slurm ''' ) platform = get_platform('saffron') assert platform['hosts'] == ['saff01'] assert platform['job runner'] == 'slurm' @pytest.mark.parametrize( 'task_conf, err_expected', [ ( { 'platform': 'localhost', 'remote': { 'host': 'localhost' } }, True ), ( { 'platform': 'gondor', 'remote': { 'retrieve job logs': False } }, True ), ( { 'platform': 'gondor', 'remote': { 'host': None } }, False ), ] ) def test_get_platform_cylc7_8_syntax_mix_fails( task_conf: dict, err_expected: bool, mock_glbl_cfg: Callable ): """If a task with a mix of Cylc7 and 8 syntax is passed to get_platform this should return an error. """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[gondor]] hosts = denethor ''' ) if err_expected: with pytest.raises( PlatformLookupError, match=( r"Task .* has the following deprecated '\[runtime\]' " r"setting\(s\) which cannot be used with 'platform.*" ) ): get_platform(task_conf) else: get_platform(task_conf) def test_get_platform_from_config_with_platform_name(mock_glbl_cfg): # A platform name is present, and no clashing cylc7 configs are: mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[mace]] hosts = mace001, mace002 job runner = slurm ''' ) task_conf = {'platform': 'mace'} platform = get_platform(task_conf) assert platform['hosts'] == ['mace001', 'mace002'] assert platform['job runner'] == 'slurm' @pytest.mark.parametrize( 'task_conf, expected_platform_name', [ ( { 'remote': {'host': 'cumin'}, 'job': {'batch system': 'slurm'} }, 'ras_el_hanout' ), ( {'remote': {'host': 'cumin'}}, 'spice_bg' ), ( {'job': {'batch system': 'batchyMcBatchFace'}}, 'local_job_runner' ), ( {'script': 'true'}, 'localhost' ), ( { 'remote': {'host': 'localhost'}, 'job': { 'batch system': None, 'batch submit command template': None, 'execution polling intervals': None } }, 'localhost' ), ( { 'remote': {'host': 'cylcdevbox'}, 'job': { 'batch system': None, 'batch submit command template': None, 'execution polling intervals': None } }, 'cylcdevbox' ) ] ) def test_get_platform_using_platform_name_from_job_info( mock_glbl_cfg, task_conf, expected_platform_name ): """Calculate platform from Cylc 7 config: n.b. If this fails we don't warn because this might lead to many thousands of warnings This should not contain a comprehensive set of use-cases - these should be coverend by the unit tests for `platform_from_host_items` """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[ras_el_hanout]] hosts = rose, chilli, cumin, paprika job runner = slurm [[spice_bg]] hosts = rose, chilli, cumin, paprika [[local_job_runner]] hosts = localhost job runner = batchyMcBatchFace [[cylcdevbox]] hosts = cylcdevbox ''' ) assert get_platform(task_conf)['name'] == expected_platform_name def test_get_platform_groups_basic(mock_glbl_cfg): """get platform from group works. Additionally, ensure that we stop after selecting the first appropriate platform. """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[aleph, bet, alpha, beta]] [platform groups] [[hebrew_letters]] platforms = alpha, beta [[[selection]]] method = definition order [[aleph]] # Group with same name as platform to try and # trip up the platform selection logic after it # has processed [[.*_letters]] below platforms = alpha [[.*_letters]] platforms = aleph, bet [[[selection]]] method = definition order ''' ) output = get_platform('hebrew_letters') assert output['name'] == 'aleph' @pytest.mark.parametrize( 'task_conf, expected_err_msg', [ ({'platform': '$(host)'}, None), ({'platform': '$(host)-suffix'}, None), ({'platform': '`echo ${chamber}`'}, "backticks are not supported") ] ) def test_get_platform_subshell( task_conf: Dict[str, str], expected_err_msg: Optional[str]): """Test get_platform() for subshell platform definition.""" if expected_err_msg: with pytest.raises(PlatformLookupError) as err: get_platform(task_conf) assert expected_err_msg in str(err.value) else: assert get_platform(task_conf) is None def test_get_localhost_install_target(): assert get_localhost_install_target() == 'localhost' def test_localhost_different_install_target(mock_glbl_cfg): mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] install target = file_system_1 ''' ) assert get_localhost_install_target() == 'file_system_1' cylc-flow-8.6.4/tests/unit/test_time_parser.py0000664000175000017500000002014215202510242021650 0ustar alastairalastair# 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 . """Test Cylc recurring date/time syntax parsing.""" import pytest from cylc.flow.time_parser import ( CylcTimeParser, UTC_UTC_OFFSET_HOURS_MINUTES, ) @pytest.fixture def parsers(): _start_point = "19991226T0930Z" # Note: the following timezone will be Z-ified *after* truncation # or offsets are applied. _end_point = "20010506T1200+0200" return { 0: CylcTimeParser( _start_point, _end_point, CylcTimeParser.initiate_parsers( assumed_time_zone=UTC_UTC_OFFSET_HOURS_MINUTES ) ), 2: CylcTimeParser( _start_point, _end_point, CylcTimeParser.initiate_parsers( num_expanded_year_digits=2, assumed_time_zone=UTC_UTC_OFFSET_HOURS_MINUTES ) ) } @pytest.mark.parametrize( 'expression, num_expanded_year_digits, ctrl_data, ctrl_results', [ ( "R5/T06/04T1230-0300", 0, "R5/19991227T0600Z/20010604T1530Z", ["19991227T0600Z", "20010604T1530Z", "20021112T0100Z", "20040420T1030Z", "20050927T2000Z"] ), ( "R2/-0450000101T04Z/+0020630405T2200-05", 2, "R2/-0450000101T0400Z/+0020630406T0300Z", ["-0450000101T0400Z", "+0020630406T0300Z"] ), ( "R10/T12+P10D/-P1Y", 0, "R10/20000105T1200Z/20000506T1000Z", ["20000105T1200Z", "20000506T1000Z", "20000905T0800Z", "20010105T0600Z", "20010507T0400Z", "20010906T0200Z", "20020106T0000Z", "20020507T2200Z", "20020906T2000Z", "20030106T1800Z"] ) ] ) def test_first_recurrence_format( expression, num_expanded_year_digits, ctrl_data, ctrl_results, parsers ): """Test the first ISO 8601 recurrence format.""" parser = parsers[num_expanded_year_digits] recurrence = parser.parse_recurrence(expression)[0] test_data = str(recurrence) assert test_data == ctrl_data test_results = [str(res) for res in recurrence] assert test_results == ctrl_results def test_third_recurrence_format(parsers): """Test the third ISO 8601 recurrence format.""" tests = [("T06", "R/19991227T0600Z/P1D"), ("T1230", "R/19991226T1230Z/P1D"), ("04T00", "R/20000104T0000Z/P1M"), ("01T06/PT2H", "R/20000101T0600Z/PT2H"), ("0501T/P4Y3M", "R/20000501T0000Z/P4Y3M"), ("P1YT5H", "R/19991226T0930Z/P1YT5H", ["19991226T0930Z", "20001226T1430Z"]), ("PT5M", "R/19991226T0930Z/PT5M"), ("R/T-40", "R/19991226T0940Z/PT1H"), ("R/150401T", "R/20150401T0000Z/P100Y"), ("R5/T04-0300", "R5/19991227T0700Z/P1D"), ("R2/T23Z/", "R2/19991226T2300Z/P1D"), ("R/19991226T2359Z/P1W", "R/19991226T2359Z/P1W"), ("R/-P1W/P1W", "R/19991219T0930Z/P1W"), ("R/+P1D/P1Y", "R/19991227T0930Z/P1Y"), ("R/T06-P1D/PT1H", "R/19991226T0600Z/PT1H"), ("R/T12/PT10,5H", "R/19991226T1200Z/PT10,5H"), ("R/T12/PT10.5H", "R/19991226T1200Z/PT10,5H"), ("R5/+P10Y5M4D/PT2H", "R5/20100529T0930Z/PT2H"), ("R30/-P200D/P10D", "R30/19990609T0930Z/P10D"), ("R10/T06/PT2H", "R10/19991227T0600Z/PT2H", ["19991227T0600Z", "19991227T0800Z", "19991227T1000Z", "19991227T1200Z", "19991227T1400Z", "19991227T1600Z", "19991227T1800Z", "19991227T2000Z", "19991227T2200Z", "19991228T0000Z"]), ("R//P1Y", "R/19991226T0930Z/P1Y", ["19991226T0930Z", "20001226T0930Z"]), ("R5//P1D", "R5/19991226T0930Z/P1D", ["19991226T0930Z", "19991227T0930Z", "19991228T0930Z", "19991229T0930Z", "19991230T0930Z"]), ("R1", "R1/19991226T0930Z/P0Y", ["19991226T0930Z"])] for test in tests: if len(test) == 2: expression, ctrl_data = test ctrl_results = None else: expression, ctrl_data, ctrl_results = test recurrence = (parsers[0].parse_recurrence(expression))[0] test_data = str(recurrence) assert test_data == ctrl_data if ctrl_results is None: continue test_results = [] for i, test_result in enumerate(recurrence): if i > len(ctrl_results) - 1: break assert str(test_result) == ctrl_results[i] test_results.append(str(test_result)) assert test_results == ctrl_results def test_fourth_recurrence_format(parsers): """Test the fourth ISO 8601 recurrence format.""" tests = [("PT6H/20000101T0500Z", "R25/19991226T0500Z/PT6H"), ("P12D/+P2W", "R44/19991221T1000Z/P12D"), ("R2/P1W/-P1M1D", "R2/P1W/20010405T1000Z"), ("R3/P6D/T12+02", "R3/P6D/20010506T1000Z"), ("R4/P6DT12H/01T00+02", "R4/P6DT12H/20010531T2200Z"), ("R5/P1D/20010506T1200+0200", "R5/P1D/20010506T1000Z"), ("R6/PT5M/+PT2M", "R6/PT5M/20010506T1002Z"), ("R7/P20Y/-P20Y", "R7/P20Y/19810506T1000Z"), ("R8/P3YT2H/T18-02", "R8/P3YT2H/20010506T2000Z"), ("R9/PT3H/31T", "R9/PT3H/20010531T0000Z"), ("R10/P1Y/", "R10/P1Y/20010506T1000Z"), ("R3/P2Y/02T", "R3/P2Y/20010602T0000Z"), ("R/P2Y", "R2/19990506T1000Z/P2Y"), ("R48/PT2H", "R48/PT2H/20010506T1000Z"), ("R100/P21Y/", "R100/P21Y/20010506T1000Z")] for test in tests: if len(test) == 2: expression, ctrl_data = test ctrl_results = None else: expression, ctrl_data, ctrl_results = test recurrence = (parsers[0].parse_recurrence( expression))[0] test_data = str(recurrence) assert test_data == ctrl_data if ctrl_results is None: continue test_results = [] for i, test_result in enumerate(recurrence): assert str(test_result) == ctrl_results[i] test_results.append(str(test_result)) assert test_results == ctrl_results def test_inter_cycle_timepoints(parsers): """Test the inter-cycle point parsing.""" task_cycle_time = parsers[0].parse_timepoint("20000101T00Z") tests = [("T06", "20000101T0600Z", 0), ("-PT6H", "19991231T1800Z", 0), ("+P5Y2M", "20050301T0000Z", 0), ("0229T", "20000229T0000Z", 0), ("+P54D", "20000224T0000Z", 0), ("T12+P5W", "20000205T1200Z", 0), ("-P1Y", "19990101T0000Z", 0), ("-9999990101T00Z", "-9999990101T0000Z", 2), ("20050601T2359+0200", "20050601T2159Z", 0)] for expression, ctrl_data, num_expanded_year_digits in tests: parser = parsers[num_expanded_year_digits] test_data = str(parser.parse_timepoint( expression, context_point=task_cycle_time)) assert test_data == ctrl_data def test_interval(parsers): """Test the interval timepoint parsing (purposefully weak tests).""" tests = ["PT6H2M", "PT6H", "P2Y5D", "P5W", "PT12M2S", "PT65S", "PT2M", "P1YT567,4M"] for expression in tests: test_data = str(parsers[0].parse_interval(expression)) assert test_data == expression cylc-flow-8.6.4/tests/unit/scripts/0000775000175000017500000000000015202510242017415 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/scripts/test_validate_reinstall_units.py0000664000175000017500000000331315202510242026116 0ustar alastairalastair# 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 . """Tests for cylc.flow.scripts.validate_reinstall.py """ import pytest from cylc.flow.scripts.validate_reinstall import ( check_tvars_and_workflow_stopped) @pytest.mark.parametrize( 'is_running, tvars, tvars_file, expect', [ (True, [], None, True), (True, ['FOO="Bar"'], None, False), (True, [], ['bar.txt'], False), (True, ['FOO="Bar"'], ['bar.txt'], False), (False, [], None, True), (False, ['FOO="Bar"'], ['bar.txt'], True), (False, [], ['bar.txt'], True), (False, ['FOO="Bar"'], ['bar.txt'], True), ] ) def test_check_tvars_and_workflow_stopped( caplog, is_running, tvars, tvars_file, expect ): """It returns true if workflow is running and tvars or tvars_file is set. """ result = check_tvars_and_workflow_stopped(is_running, tvars, tvars_file) assert result == expect if expect is False: warn = 'can only be changed if' assert warn in caplog.records[0].msg cylc-flow-8.6.4/tests/unit/scripts/test_release.py0000664000175000017500000000347115202510242022453 0ustar alastairalastair# 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 . """Test logic in cylc-release script.""" from optparse import Values import pytest from typing import Iterable, Optional, Tuple, Type from cylc.flow.exceptions import InputError from cylc.flow.option_parsers import Options from cylc.flow.scripts.release import get_option_parser, _validate Opts = Options(get_option_parser()) @pytest.mark.parametrize( 'opts, task_globs, expected_err', [ (Opts(), ['*'], None), (Opts(release_all=True), [], None), ( Opts(release_all=True), ['*'], (InputError, "Cannot combine --all with Cycle/Task IDs") ), ( Opts(), [], (InputError, "Must define Cycles/Tasks") ), ] ) def test_validate( opts: Values, task_globs: Iterable[str], expected_err: Optional[Tuple[Type[Exception], str]]): if expected_err: err, msg = expected_err with pytest.raises(err) as exc: _validate(opts, *task_globs) assert msg in str(exc.value) else: _validate(opts, *task_globs) cylc-flow-8.6.4/tests/unit/scripts/__init__.py0000664000175000017500000000135715202510242021534 0ustar alastairalastair# 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 . cylc-flow-8.6.4/tests/unit/scripts/test_check_versions.py0000664000175000017500000000343515202510242024040 0ustar alastairalastair# 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 . import pytest from cylc.flow.exceptions import NoHostsError from cylc.flow.scripts.check_versions import check_versions @pytest.fixture def break_host_selection(monkeypatch): """Make host selection for any platform fail with NoHostsError.""" def _get_host_from_platform(platform, *args, **kwargs): raise NoHostsError(platform) monkeypatch.setattr( 'cylc.flow.scripts.check_versions.get_host_from_platform', _get_host_from_platform, ) def _get_platform(platform_name, *args, **kwargs): return {'name': platform_name} monkeypatch.setattr( 'cylc.flow.scripts.check_versions.get_platform', _get_platform, ) def test_no_hosts_error(break_host_selection, capsys): """It should handle NoHostsError events.""" versions = check_versions(['buggered'], True) # the broken platform should be skipped (so no returned versions) assert not versions # a warning should have been logged to stderr out, err = capsys.readouterr() assert 'Could not connect to buggered' in err cylc-flow-8.6.4/tests/unit/scripts/test_config.py0000664000175000017500000001505415202510242022300 0ustar alastairalastair# 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 . import asyncio import os from pathlib import Path from textwrap import dedent from typing import Any, Optional, List import pytest from cylc.flow.cfgspec.globalcfg import GlobalConfig from cylc.flow.option_parsers import Options from cylc.flow.scripts.config import ( _main, get_config_file_hierarchy, get_option_parser, ) from cylc.flow.workflow_files import WorkflowFiles Fixture = Any HOME: str = str(Path('~').expanduser()) @pytest.fixture def conf_env(monkeypatch): """Clear any env vars that affect which conf files get loaded. Return a convenience function for setting environment variables. """ # wipe any cached config monkeypatch.setattr( GlobalConfig, '_DEFAULT', None, ) for envvar in ('CYLC_SITE_CONF_PATH', 'CYLC_CONF_PATH'): if envvar in os.environ: monkeypatch.delenv(envvar) def _set_env(key, value): if value: monkeypatch.setenv(key, value) return _set_env @pytest.fixture def dummy_version_hierarchy(monkeypatch): """Set the config version hierarchy.""" monkeypatch.setattr( 'cylc.flow.cfgspec.globalcfg.GlobalConfig.VERSION_HIERARCHY', ['', '1', '1.0'] ) @pytest.fixture def capload(monkeypatch): """Capture configuration load events. This prevents actual file loading. If the file name contains the string "invalid" it will not appear in the results as if it diddn't exist on the filesystem. """ files = [] def _capload(glblcfg, fname, _): if 'invalid' not in fname: # if the file is called invalid skip it # this is to replicate the behaviour of skipping files that # don't exist files.append(fname.replace(HOME, '~')) monkeypatch.setattr( GlobalConfig, '_load', _capload ) return files def test_get_config_file_hierarchy_global( monkeypatch: Fixture, conf_env: Fixture, capload: Fixture, dummy_version_hierarchy: Fixture ): """Test get_config_file_hierarchy() for the global hierarchy only.""" assert [ path.replace(HOME, '~') for path in get_config_file_hierarchy() ] == [ '/etc/cylc/flow/global.cylc', '/etc/cylc/flow/1/global.cylc', '/etc/cylc/flow/1.0/global.cylc', '~/.cylc/flow/global.cylc', '~/.cylc/flow/1/global.cylc', '~/.cylc/flow/1.0/global.cylc' ] @pytest.mark.parametrize( 'conf_path,site_conf_path,files', [ pytest.param( None, None, [ '/etc/cylc/flow/global.cylc', '/etc/cylc/flow/1/global.cylc', '/etc/cylc/flow/1.0/global.cylc', '~/.cylc/flow/global.cylc', '~/.cylc/flow/1/global.cylc', '~/.cylc/flow/1.0/global.cylc', ], id='(default)' ), pytest.param( None, '', [ '/flow/global.cylc', '/flow/1/global.cylc', '/flow/1.0/global.cylc', '~/.cylc/flow/global.cylc', '~/.cylc/flow/1/global.cylc', '~/.cylc/flow/1.0/global.cylc', ], id='CYLC_SITE_CONF_PATH=valid' ), pytest.param( None, 'invalid', [ '~/.cylc/flow/global.cylc', '~/.cylc/flow/1/global.cylc', '~/.cylc/flow/1.0/global.cylc', ], id='CYLC_SITE_CONF_PATH=invalid' ), pytest.param( '', None, ['/global.cylc'], id='CYLC_CONF_PATH=valid' ), pytest.param( 'invalid', None, [], id='CYLC_CONF_PATH=invalid' ), pytest.param( '', '', ['/global.cylc'], # should ignore CYLC_SITE_CONF_PATH id='CYLC_CONF_PATH=valid, CYLC_SITE_CONF_PATH=valid' ), pytest.param( 'invalid', '', [], id='CYLC_CONF_PATH=invalid, CYLC_SITE_CONF_PATH=valid' ), ] ) def test_cylc_site_conf_path_env_var( monkeypatch: Fixture, conf_env: Fixture, capload: Fixture, dummy_version_hierarchy: Fixture, conf_path: Optional[str], site_conf_path: Optional[str], files: List[str], ): """Test that the right files are loaded according to env vars.""" # set the relevant environment variables conf_env('CYLC_CONF_PATH', conf_path) conf_env('CYLC_SITE_CONF_PATH', site_conf_path) # load the global config GlobalConfig.get_inst() assert capload == files def test_cylc_config_xtriggers(tmp_run_dir, capsys: pytest.CaptureFixture): """Test `cylc config` outputs any xtriggers properly""" run_dir: Path = tmp_run_dir('constellation') flow_file = run_dir / WorkflowFiles.FLOW_FILE flow_file.write_text(dedent(""" [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 2020-05-05 [[xtriggers]] clock_1 = wall_clock(offset=PT1H):PT4S rotund = xrandom(90, 2) [[graph]] R1 = @rotund => foo """)) option_parser = get_option_parser() asyncio.run( _main(option_parser, Options(option_parser)(), 'constellation') ) assert capsys.readouterr().out == dedent("""\ [scheduler] allow implicit tasks = True [scheduling] initial cycle point = 2020-05-05 [[xtriggers]] clock_1 = wall_clock(offset=PT1H):4.0 rotund = xrandom(90, 2):10.0 [[graph]] R1 = @rotund => foo [runtime] [[root]] [[foo]] completion = succeeded """) cylc-flow-8.6.4/tests/unit/scripts/test_graph.py0000664000175000017500000002004415202510242022127 0ustar alastairalastair# 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 . from types import SimpleNamespace import typing as t import pytest from cylc.flow.scripts.graph import ( Edge, Node, format_cylc_reference, format_graphviz, get_nodes_and_edges, ) @pytest.fixture def example_graph(): """Example workflow graph with inter-cycle dependencies.""" nodes: t.List[Node] = [ '1/a', '1/b', '1/c', '2/a', '2/b', '2/c', ] edges: t.List[Edge] = [ ('1/a', '1/b'), ('1/b', '1/c'), ('2/a', '2/b'), ('2/b', '2/c'), ('1/b', '2/b'), ] return nodes, edges @pytest.fixture def example_namespace_graph(): """Example namespace graph with inheritance.""" nodes: t.List[Node] = [ 'A', 'a1', 'a2', 'B', 'B1', 'b11', 'B2', 'b22', ] edges: t.List[Edge] = [ ('A', 'a1'), ('A', 'a2'), ('B', 'B1'), ('B', 'B2'), ('B1', 'b11'), ('B2', 'b22'), ] return nodes, edges @pytest.fixture def null_config(monkeypatch): """Patch the config loader to return a workflow with no nodes or edges.""" def _get_graph_raw(*args, **kwargs): return None def _get_parents_lists(*args, **kwargs): return {} config = SimpleNamespace( get_graph_raw=_get_graph_raw, get_parent_lists=_get_parents_lists, ) monkeypatch.setattr( 'cylc.flow.scripts.graph.get_config', lambda x, y, z: config ) def test_format_graphviz_normal(example_graph): """Test graphviz output for default options. Tests both orientations (--transpose). """ nodes, edges = example_graph # format the graph in regular orientation opts = SimpleNamespace(transpose=False, namespaces=False, cycles=False) lines = format_graphviz(opts, nodes, edges) assert lines == [ 'digraph {', ' graph [fontname="sans" fontsize="25"]', ' node [fontname="sans"]', '', ' "1/a" [label="a\\n1"]', ' "1/b" [label="b\\n1"]', ' "1/c" [label="c\\n1"]', '', ' "2/a" [label="a\\n2"]', ' "2/b" [label="b\\n2"]', ' "2/c" [label="c\\n2"]', '', ' "1/a" -> "1/b"', ' "1/b" -> "1/c"', ' "2/a" -> "2/b"', ' "2/b" -> "2/c"', ' "1/b" -> "2/b"', '}', ] # format the graph in transposed orientation opts = SimpleNamespace(transpose=True, namespaces=False, cycles=False) transposed_lines = format_graphviz(opts, nodes, edges) # the transposed graph should be the same except for one line... assert [ line for line in transposed_lines if line not in lines ] == [ # ...the one which sets the orientation ' rankdir="LR"', ] def test_format_graphviz_namespace(example_namespace_graph): """Test graphviz output for a namespace graph. Tests both orientations (--transpose). """ nodes, edges = example_namespace_graph # format the graph in regular orientation opts = SimpleNamespace(transpose=False, namespaces=True, cycles=False) lines = format_graphviz(opts, nodes, edges) assert lines == [ 'digraph {', ' graph [fontname="sans" fontsize="25"]', ' node [fontname="sans"]', ' node [shape="rect"]', '', ' "A"', ' "a1"', ' "a2"', ' "B"', ' "B1"', ' "b11"', ' "B2"', ' "b22"', '', ' "A" -> "a1"', ' "A" -> "a2"', ' "B" -> "B1"', ' "B" -> "B2"', ' "B1" -> "b11"', ' "B2" -> "b22"', '}', ] # format the graph in transposed orientation opts = SimpleNamespace(transpose=True, namespaces=True, cycles=False) transposed_lines = format_graphviz(opts, nodes, edges) # the transposed graph should be the same except for one line... assert [ line for line in transposed_lines if line not in lines ] == [ # ...the one which sets the orientation ' rankdir="LR"', ] def test_format_graphviz_cycles(example_graph): """Test graphviz format for the --cycles option (group by cycle). Note: There is no difference between iso8601 and integer cycle points here, the graph logic is cycle point format agnostic. Sorting is not performed in this funtion. """ nodes, edges = example_graph opts = SimpleNamespace(transpose=False, namespaces=False, cycles=True) lines = format_graphviz(opts, nodes, edges) assert lines == [ 'digraph {', ' graph [fontname="sans" fontsize="25"]', ' node [fontname="sans"]', '', ' subgraph "cluster_1" { ', ' label="1"', ' style="dashed"', ' "1/a" [label="a\\n1"]', ' "1/b" [label="b\\n1"]', ' "1/c" [label="c\\n1"]', ' }', '', ' subgraph "cluster_2" { ', ' label="2"', ' style="dashed"', ' "2/a" [label="a\\n2"]', ' "2/b" [label="b\\n2"]', ' "2/c" [label="c\\n2"]', ' }', '', ' "1/a" -> "1/b"', ' "1/b" -> "1/c"', ' "2/a" -> "2/b"', ' "2/b" -> "2/c"', ' "1/b" -> "2/b"', '}', ] def test_format_cylc_reference_normal(example_graph): """Test Cylc "reference" format (used by the test battery). Note: There is no difference between iso8601 and integer cycle points here, the graph logic is cycle point format agnostic. Sorting is not performed in this funtion. Note: There is no transpose mode for reference graphs. """ nodes, edges = example_graph opts = SimpleNamespace(namespaces=False) lines = format_cylc_reference(opts, nodes, edges) assert lines == [ 'edge "1/a" "1/b"', 'edge "1/b" "1/c"', 'edge "2/a" "2/b"', 'edge "2/b" "2/c"', 'edge "1/b" "2/b"', 'graph', 'node "1/a" "a\\n1"', 'node "1/b" "b\\n1"', 'node "1/c" "c\\n1"', 'node "2/a" "a\\n2"', 'node "2/b" "b\\n2"', 'node "2/c" "c\\n2"', 'stop', ] def test_format_cylc_reference_namespace(example_namespace_graph): """Test Cylc "reference" format for namespace graphs. Note: There is no transpose mode for reference graphs. """ nodes, edges = example_namespace_graph opts = SimpleNamespace(namespaces=True) lines = format_cylc_reference(opts, nodes, edges) assert lines == [ 'edge "A" "a1"', 'edge "A" "a2"', 'edge "B" "B1"', 'edge "B" "B2"', 'edge "B1" "b11"', 'edge "B2" "b22"', 'graph', 'node "A" "A"', 'node "a1" "a1"', 'node "a2" "a2"', 'node "B" "B"', 'node "B1" "B1"', 'node "b11" "b11"', 'node "B2" "B2"', 'node "b22" "b22"', 'stop', ] def test_null(null_config): """Ensure that an empty graph is handled elegantly.""" opts = SimpleNamespace( namespaces=False, grouping=False, show_suicide=False ) assert get_nodes_and_edges(opts, None, 1, 2, '') == ([], []) opts = SimpleNamespace( namespaces=True, grouping=False, show_suicide=False ) assert get_nodes_and_edges(opts, None, 1, 2, '') == ([], []) cylc-flow-8.6.4/tests/unit/scripts/test_lint.py0000664000175000017500000005063615202510242022006 0ustar alastairalastair#!/usr/bin/env python3 # 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 . """Tests `cylc lint` CLI Utility.""" from collections import Counter import logging from pathlib import Path from pprint import pformat import re from textwrap import dedent from types import SimpleNamespace import pytest from pytest import param from cylc.flow.scripts.lint import ( LINT_SECTION, MANUAL_DEPRECATIONS, check_lowercase_family_names, get_cylc_files, get_pyproject_toml, get_reference, get_upgrader_info, lint, _merge_cli_with_tomldata, parse_checks, validate_toml_items ) from cylc.flow.exceptions import CylcError STYLE_CHECKS = parse_checks(['style']) UPG_CHECKS = parse_checks(['728']) TEST_FILE = ''' [visualization] [cylc] include at start-up = foo exclude at start-up = bar reset timer = false log resolved dependencies = True required run mode = False health check interval = PT10M abort if any task fails = true suite definition directory = '/woo' disable automatic shutdown = false spawn to max active cycle points = false [[reference test]] allow task failures = true [[simulation]] disable suite event handlers = true [[authentication]] [[environment]] force run mode = dummy [[events]] reset inactivity timer = 42 abort on stalled = True abort on timeout = False abort if startup handler fails= True # deliberately not added a space. abort if shutdown handler fails= True abort if timeout handler fails = True abort if stalled handler fails = True abort if inactivity handler fails = False aborted handler = woo stalled handler = bar timeout handler = bas shutdown handler = qux startup handler = now inactivity handler = bored mail to = eleanor.rigby@beatles.lv mail from = fr.mckenzie@beatles.lv mail footer = "Collecting The Rice" mail smtp = 123.456.789.10 timeout = 30 inactivity = 30 abort on inactivity = 30 [[parameters]] [[parameter templates]] [[mail]] task event mail interval = PT4M # deliberately added lots of spaces. [scheduling] max active cycle points = 5 hold after point = 20220101T0000Z [[dependencies]] [[[R1]]] graph = """ MyFaM:finish-all => remote => !mash_theme a & \\ b => c c | \\ d => e """ [runtime] [[root]] [[[environment]]] CYLC_VERSION={{CYLC_VERSION}} ROSE_VERSION = {{ROSE_VERSION }} FCM_VERSION = {{ FCM_VERSION }} [[MyFaM]] extra log files = True {% from 'cylc.flow' import LOG %} pre-script = "echo ${CYLC_SUITE_DEF_PATH}" script = {{HELLOWORLD}} post-script = "echo ${CYLC_SUITE_INITIAL_CYCLE_TIME}" env-script = POINT=$(rose date 2059 --offset P1M) [[[suite state polling]]] template = and [[[remote]]] host = parasite suite definition directory = '/home/bar' [[[job]]] batch system = slurm shell = fish [[[events]]] mail retry delays = PT30S warning handler = frr.sh submission timeout handler = faiuhdf submission retry handler = vhbayhrbfgau submission failed handler = giaSEHFUIHJ failed handler = woo execution timeout handler = sdfghjkl expired handler = %(suite_uuid)s %(user@host)s late handler = dafuhj submitted handler = dafuhj started handler = dafuhj succeeded handler = dafuhj custom handler = efrgh critical handler = fgjdsfs retry handler = dfaiuhfrgpa sumbission handler = fas9hrfgaiuph # Shouldn't object to a comment, unlike the terrible indents below: [[bad indent]] inherit = MyFaM [[remote]] platform = $(rose host-select parasite) script = "cylc nudge" post-script = "rose suite-hook" [meta] [[and_another_thing]] [[[remote]]] host = `rose host-select thingy` %include foo.cylc ''' LINT_TEST_FILE = ''' \t[scheduler] [scheduler] [[dependencies]] {% set N = 009 %} {% foo %} {{foo}} # {{quix}} R1 = """ foo & \\ bar => \\ baz """ [runtime] [[this_is_ok]] script = echo "this is incorrectly indented" [[foo]] inherit = hello [[[job]]] something\t [[bar]] platform = $(some-script foo) [[[directives]]] -l walltime = 666 [[baz]] run mode = skip platform = `no backticks` [[[skip]]] outputs = succeeded, failed ''' + ( '\nscript = the quick brown fox jumps over the lazy dog until it becomes ' 'clear that this line is longer than the default 130 character limit.' ) def lint_text(text, checks, ignores=None, modify=False): checks = parse_checks(checks, ignores) counter = Counter() messages = [] outlines = list( lint( Path('flow.cylc'), iter(text.splitlines()), checks, counter, modify=modify, write=messages.append, ) ) return SimpleNamespace( counter=counter, messages=messages, outlines=outlines ) def filter_strings(items, contains): """Return only items which contain a given string.""" return [ message for message in items if contains in message ] def assert_contains(items, contains, instances=None): """Pass if at least one item contains a given string.""" filtered = filter_strings(items, contains) if not filtered: raise Exception( f'Could not find: "{contains}" in:\n' + pformat(items)) elif instances and len(filtered) != instances: raise Exception( f'Expected "{contains}" to appear {instances} times' f', got it {len(filtered)} times.') EXPECT_INSTANCES_OF_ERR = { 16: 3, } @pytest.mark.parametrize( # 11 won't be tested because there is no jinja2 shebang 'number', set(range(1, len(MANUAL_DEPRECATIONS) + 1)) - {11} ) def test_check_cylc_file_7to8(number): """TEST File has one of each manual deprecation;""" lint = lint_text(TEST_FILE, ['728']) instances = EXPECT_INSTANCES_OF_ERR.get(number, None) assert_contains(lint.messages, f'[U{number:03d}]', instances) def test_check_cylc_file_7to8_has_shebang(): """Jinja2 code comments will not be added if shebang present""" lint = lint_text('#!jinja2\n{{FOO}}', '', '[scheduler]') assert not lint.counter def test_check_cylc_file_line_no(): """It prints the correct line numbers""" lint = lint_text(TEST_FILE, ['728']) # the first message should be for line number 2 (line is a shebang) assert ':2:' in lint.messages[0] @pytest.mark.parametrize( 'line', [ # lowercase family names are not permitted 'inherit = g', 'inherit = FOO, bar', 'inherit = None, bar', 'inherit = A, b, C', 'inherit = "A", "b"', "inherit = 'A', 'b'", 'inherit = FOO_BAr', # whitespace & trailing commas ' inherit = a , ', # parameters, templating code should be ignored # but any lowercase chars before or after should not 'inherit = Az', 'inherit = A{{ x }}z', 'inherit = N{# #}one', 'inherit = A@( x )z', ] ) def test_check_lowercase_family_names__true(line): assert check_lowercase_family_names(line) is True @pytest.mark.parametrize( 'line', [ # undefined values are ok 'inherit =', 'inherit = ', # none, None and root are ok 'inherit = none', 'inherit = None', 'inherit = root', # whitespace & trailing commas 'inherit = None,', 'inherit = None, ', ' inherit = None , ', # uppercase family names are ok 'inherit = None, FOO, BAR', 'inherit = FOO', 'inherit = FOO_BAR_0', # parameters should be ignored 'inherit = AZ', 'inherit = ', # jinja2 should be ignored param( 'inherit = A{{ a }}Z, {% for x in range(5) %}' 'A{{ x }}, {% endfor %}', id='jinja2-long' ), # trailing comments should be ignored 'inherit = A, B # no, comment', 'inherit = # a', # quotes are ok 'inherit = "A", "B"', "inherit = 'A', 'B'", 'inherit = "None", B', 'inherit = ', # one really awkward, but valid example param( 'inherit = none, FOO_BAR_0, "", AZ, A{{a}}Z', id='awkward' ), ] ) def test_check_lowercase_family_names__false(line): assert check_lowercase_family_names(line) is False def test_inherit_lowercase_matches(): lint = lint_text('inherit = a', ['style']) assert any('S007' in msg for msg in lint.messages) @pytest.mark.parametrize( # 8 and 11 won't be tested because there is no jinja2 shebang 'number', set(range(1, len(STYLE_CHECKS) + 1)) - {8, 11} ) def test_check_cylc_file_lint(number): lint = lint_text(LINT_TEST_FILE, ['style']) assert_contains(lint.messages, f'S{number:03d}') @pytest.mark.parametrize('code', STYLE_CHECKS.keys()) def test_check_exclusions(code): """It does not report any items excluded.""" lint = lint_text(LINT_TEST_FILE, ['style'], [code]) assert not filter_strings(lint.messages, code) def test_check_cylc_file_jinja2_comments(): """Jinja2 inside a Jinja2 comment should not warn""" lint = lint_text('#!jinja2\n{# {{ foo }} #}', ['style']) assert not any('S011' in msg for msg in lint.messages) def test_check_cylc_file_jinja2_comments_shell_arithmetic_not_warned(): """Jinja2 after a $((10#$variable)) should not warn""" lint = lint_text('#!jinja2\na = b$((10#$foo+5)) {{ BAR }}', ['style']) assert not any('S011' in msg for msg in lint.messages) @pytest.mark.parametrize( # 11 won't be tested because there is no jinja2 shebang 'number', set(range(1, len(MANUAL_DEPRECATIONS) + 1)) - {11} ) def test_check_cylc_file_inplace(number): lint = lint_text(TEST_FILE, ['728', 'style'], modify=True) assert_contains(lint.outlines, f'[U{number:03d}]') def test_get_cylc_files_get_all_rcs(tmp_path): """It returns all paths except `log/**`. """ expect = [('etc', 'foo.rc'), ('bin', 'foo.rc'), ('an_other', 'foo.rc')] # Create a fake run directory, including the log file which should not # be searched: dirs = ['etc', 'bin', 'log', 'an_other', 'log/skarloey/'] for path in dirs: thispath = tmp_path / path thispath.mkdir(parents=True) (thispath / 'foo.rc').touch() # Run the test result = [(i.parent.name, i.name) for i in get_cylc_files(tmp_path)] assert sorted(result) == sorted(expect) def mock_parse_checks(*args, **kwargs): return { 'U042': { 'short': 'section `[vizualization]` has been removed.', 'url': 'some url or other', 'purpose': 'U', 'rst': 'section ``[vizualization]`` has been removed.', 'function': re.compile('not a regex') }, } def test_get_reference_rst(monkeypatch): """It produces a reference file for our linting.""" monkeypatch.setattr( 'cylc.flow.scripts.lint.parse_checks', mock_parse_checks ) ref = get_reference('all', 'rst') expect = ( '\n7 to 8 upgrades\n---------------\n\n' '`U042 `_' f'\n{"^" * 78}' '\nsection ``[vizualization]`` has been ' 'removed.\n\n\n' ) assert ref == expect def test_get_reference_text(monkeypatch): """It produces a reference file for our linting.""" monkeypatch.setattr( 'cylc.flow.scripts.lint.parse_checks', mock_parse_checks ) ref = get_reference('all', 'text') expect = ( '\n7 to 8 upgrades\n---------------\n\n' 'U042:\n section `[vizualization]` has been ' 'removed.' '\n https://cylc.github.io/cylc-doc/stable/html/7-to-8/some' ' url or other\n\n\n' ) assert ref == expect @pytest.fixture() def fixture_get_deprecations(): """Get the deprections list for cylc.flow.cfgspec.workflow""" deprecations = get_upgrader_info() return deprecations @pytest.mark.parametrize( 'findme', [ pytest.param( 'abort if any task fails =', id='Item not available at Cylc 8' ), pytest.param( 'timeout =', id='Item renamed at Cylc 8' ), pytest.param( '!execution retry delays', id='Item moved, name unchanged at Cylc 8' ), pytest.param( '[cylc]', id='Section changed at Cylc 8' ), ] ) def test_get_upg_info(fixture_get_deprecations, findme): """It correctly scrapes the Cylc upgrader object. n.b this is just sampling to ensure that the test it getting items. """ if findme.startswith('!'): assert not any( i['function'](findme) for i in fixture_get_deprecations.values() ) else: assert any( i['function'](findme) for i in fixture_get_deprecations.values() ) is True @pytest.mark.parametrize( 'settings, expected', [ param( """ rulesets = ['style'] ignore = ['S004'] exclude = ['sites/*.cylc'] """, { 'rulesets': ['style'], 'ignore': ['S004'], 'exclude': ['sites/*.cylc'], 'max-line-length': None, }, id="returns what we want" ), param( """ northgate = ['sites/*.cylc'] mons-meg = 42 """, (CylcError, ".*northgate"), id="invalid settings fail validation" ), param( "max-line-length = 22", { 'exclude': [], 'ignore': [], 'rulesets': [], 'max-line-length': 22, }, id='sets max line length' ) ] ) def test_get_pyproject_toml(tmp_path, settings, expected): """It returns only the lists we want from the toml file.""" tomlcontent = "[tool.cylc.lint]\n" + dedent(settings) (tmp_path / 'pyproject.toml').write_text(tomlcontent) if isinstance(expected, tuple): exc, match = expected with pytest.raises(exc, match=match): get_pyproject_toml(tmp_path) else: assert get_pyproject_toml(tmp_path) == expected @pytest.mark.parametrize( 'tomlfile', [None, '', '[tool.cylc.lint]', '[cylc-lint]'] ) def test_get_pyproject_toml_returns_blank(tomlfile, tmp_path): if tomlfile is not None: tfile = (tmp_path / 'pyproject.toml') tfile.write_text(tomlfile) expect = { 'exclude': [], 'ignore': [], 'max-line-length': None, 'rulesets': [] } assert get_pyproject_toml(tmp_path) == expect def test_get_pyproject_toml__depr( tmp_path: Path, caplog: pytest.LogCaptureFixture ): """It warns if the section is deprecated.""" file = tmp_path / 'pyproject.toml' caplog.set_level(logging.WARNING) file.write_text(f'[{LINT_SECTION}]\nmax-line-length=14') assert get_pyproject_toml(tmp_path)['max-line-length'] == 14 assert not caplog.text file.write_text('[cylc-lint]\nmax-line-length=17') assert get_pyproject_toml(tmp_path)['max-line-length'] == 17 assert "[cylc-lint] section in pyproject.toml is deprecated" in caplog.text @pytest.mark.parametrize( 'input_, error', [ param( {'exclude': ['hey', 'there', 'Delilah']}, None, id='it works' ), param( {'foo': ['hey', 'there', 'Delilah', 42]}, 'allowed', id='it fails with illegal section name' ), param( {'exclude': 'woo!'}, 'should be a list, but', id='it fails with illegal section type' ), param( {'exclude': ['hey', 'there', 'Delilah', 42]}, 'should be a string', id='it fails with illegal value name' ), param( {'rulesets': ['hey']}, 'hey not valid: Rulesets can be', id='it fails with illegal ruleset' ), param( {'ignore': ['hey']}, 'hey not valid: Ignore codes', id='it fails with illegal ignores' ), param( {'ignore': ['R999']}, 'R999 is a not a known linter code.', id='it fails with non-existant checks ignored' ) ] ) def test_validate_toml_items(input_, error): """It chucks out the wrong sort of items.""" if error is not None: with pytest.raises(CylcError, match=error): validate_toml_items(input_) else: assert validate_toml_items(input_) is True @pytest.mark.parametrize( 'clidata, tomldata, expect', [ param( { 'rulesets': ['foo', 'bar'], 'ignore': ['R101'], }, { 'rulesets': ['baz'], 'ignore': ['R100'], 'exclude': ['not_me-*.cylc'] }, { 'rulesets': ['foo', 'bar'], 'ignore': ['R100', 'R101'], 'exclude': ['not_me-*.cylc'], 'max-line-length': None }, id='It works with good path' ), ] ) def test_merge_cli_with_tomldata(clidata, tomldata, expect): """It merges each of the three sections correctly: see function.__doc__""" assert _merge_cli_with_tomldata(clidata, tomldata) == expect def test_invalid_tomlfile(tmp_path): """It fails nicely if pyproject.toml is malformed""" tomlfile = (tmp_path / 'pyproject.toml') tomlfile.write_text('foo :{}') expected_msg = 'pyproject.toml did not load:' with pytest.raises(CylcError, match=expected_msg): get_pyproject_toml(tmp_path) @pytest.mark.parametrize( 'ref, expect', [ [True, 'line > ```` characters'], [False, 'line > 42 characters'] ] ) def test_parse_checks_reference_mode(ref, expect): """Add extra explanation of max line legth setting in reference mode. """ result = parse_checks(['style'], reference=ref, max_line_len=42) value = result['S012'] assert expect in value['short'] @pytest.mark.parametrize( 'spaces, expect', ( (0, 'S002'), (1, 'S013'), (2, 'S013'), (3, 'S013'), (4, None), (5, 'S013'), (6, 'S013'), (7, 'S013'), (8, None), (9, 'S013') ) ) def test_indents(spaces, expect): """Test different wrong indentations Parameterization deliberately over-obvious to avoid replicating arithmetic logic from code. Dangerously close to re-testing ``%`` builtin. """ result = lint_text( f"{' ' * spaces}foo = 42", ['style'] ) result = ''.join(result.messages) if expect: assert expect in result else: assert not result def test_noqa(): """Comments turn of checks. """ output = lint_text( 'foo = bar#noqa\n' 'qux = baz # noqa: S002\n' 'buzz = food # noqa: S007\n' 'quixotic = foolish # noqa: S007, S992 S002\n', ['style'] ) assert len(output.messages) == 1 assert 'flow.cylc:3' in output.messages[0] cylc-flow-8.6.4/tests/unit/scripts/test_completion_server.py0000664000175000017500000005500015202510242024565 0ustar alastairalastair# 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 . from types import SimpleNamespace from cylc.flow.async_util import pipe from cylc.flow.id import Tokens from cylc.flow.network.scan import scan from cylc.flow.scripts.completion_server import ( _list_prereqs_and_outputs, server, complete_cylc, complete_command, complete_option, complete_option_value, complete_argument, list_cylc_id, list_options, list_option_values, list_workflows, list_src_workflows, list_in_workflow, list_resources, list_dir, list_flows, list_colours, cli_detokenise, get_completion_script_file, get_current_completion_script_version, check_completion_script_compatibility, COMMANDS, ) from cylc.flow.scripts.trigger import get_option_parser import pytest def setify(coro): """Cast returned lists to sets for coroutines. Convenience function to use when you want to test output not order. """ async def _coro(*args, **kwargs): ret = await coro(*args, **kwargs) if isinstance(ret, list): return set(ret) return ret return _coro @pytest.fixture def dummy_workflow(tmp_path, monkeypatch, mock_glbl_cfg): """A simple workflow run dir with some job directories to inspect. This patches the relevant interfaces so that this workflow will show up e.g. in the scan interface. """ install_dir = tmp_path / 'foo' install_dir.mkdir() (install_dir / 'run1').mkdir() run_dir = install_dir / 'run2' run_dir.mkdir() (install_dir / 'runN').symlink_to('run2', target_is_directory=True) (run_dir / 'flow.cylc').touch() job_log_dir = (run_dir / 'log') / 'job' job_log_dir.mkdir(parents=True) for cycle in ('1', '2', '3'): for task in ('foo', 'bar', 'baz'): for job in ('01', 'NN'): ((job_log_dir / cycle) / task / job).mkdir(parents=True) # patch scan for list_workflows @pipe async def _scan(*args, **kwargs): kwargs['run_dir'] = tmp_path async for flow in scan(*args, **kwargs): yield flow monkeypatch.setattr( 'cylc.flow.scripts.scan.scan', _scan, ) # patch scan for list_src_workflows mock_glbl_cfg( 'cylc.flow.scripts.completion_server.glbl_cfg', f''' [install] source dirs = {tmp_path} ''' ) # patch get_workflow_run_job_dir for list_in_workflow monkeypatch.setattr( 'cylc.flow.pathutil._CYLC_RUN_DIR', tmp_path, ) async def test_server(): """Test the request/response server.""" def _listener(timeout=None): """The listener yields requests.""" yield 'cylc|trigger|' yield 'cylc|trigger|one|' async def _responder(*parts): """The responder computes responses.""" return [f'x{part}' for part in parts] # in "once" mode the server shuts down after returning the first response ret = [] await server(_listener, _responder, once=True, write=ret.append) assert ret == [ 'xcylc xtrigger' ] # otherwise the server stays up until the listener returns ret = [] await server(_listener, _responder, once=False, write=ret.append) assert ret == [ 'xcylc xtrigger', 'xcylc xone xtrigger', # the server sorts the responses ] # if *anything* goes wrong the server should send an empty response # rather than crashing async def _responder(*parts): raise Exception() ret = [] await server(_listener, _responder, once=True, write=ret.append) assert ret == [''] async def test_complete_cylc(dummy_workflow): """Test the completion for everything Cylc. Each individual completion function is tested individually too. This test is to ensure they work together correctly. """ _complete_cylc = setify(complete_cylc) # results are un-ordered # $ cylc assert 'trigger' in await _complete_cylc('cylc') assert 'help' in await _complete_cylc('cylc') # $ cylc tri assert 'trigger' in await _complete_cylc('cylc', 'tri') # $ cylc trigger assert await _complete_cylc('cylc', 'trigger') == { 'trigger', } # $ cylc trigger assert await _complete_cylc('cylc', 'trigger', '') == { 'foo/run2//', } # $ cylc trigger f assert await _complete_cylc('cylc', 'trigger', 'f') == { 'foo/run2//', } # $ cylc trigger foo/run2// assert await _complete_cylc('cylc', 'trigger', 'foo/run2//') == { 'foo/run2//1/', 'foo/run2//2/', 'foo/run2//3/', } # $ cylc trigger foo/run2//1 assert await _complete_cylc('cylc', 'trigger', 'foo/run2//1') == { 'foo/run2//1/', } # $ cylc trigger foo/run2//1/ assert set(await _complete_cylc('cylc', 'trigger', 'foo/run2//1/')) == { 'foo/run2//1/foo/', 'foo/run2//1/bar/', 'foo/run2//1/baz/', } # $ cylc trigger foo/run2//1/f assert await _complete_cylc('cylc', 'trigger', 'foo/run2//1/f') == { 'foo/run2//1/foo/', } # $ cylc trigger foo/run2//1/foo/ assert await _complete_cylc('cylc', 'trigger', 'foo/run2//1/foo/') == { 'foo/run2//1/foo/01/', 'foo/run2//1/foo/NN/', } # $ cylc trigger foo/run2//1/foo/N assert await _complete_cylc('cylc', 'trigger', 'foo/run2//1/foo/N') == { 'foo/run2//1/foo/NN/', } # $ cylc trigger - assert '--flow' in await _complete_cylc('cylc', 'trigger', '-') # $ cylc trigger --flow assert await _complete_cylc('cylc', 'trigger', '--flow', '') == { 'none', 'new', } # $ cylc trigger --flow assert await _complete_cylc('cylc', 'trigger', '--flow', '', '') == { 'foo/run2//' } # $ cylc trigger --flow=none assert await _complete_cylc('cylc', 'trigger', '--flow=none', '') == { 'foo/run2//' } # $ cylc trigger --flow= assert await _complete_cylc('cylc', 'trigger', '--flow=') == { '--flow=none', '--flow=new', } # $ cylc trigger --flow=1 assert await _complete_cylc('cylc', 'trigger', '--flow=1', '') == { 'foo/run2//' } # $ cylc trigger --62656566 assert await _complete_cylc('cylc', 'trigger', '--62656566') == set() # $ cylc trigger 62656566 --77656C6C696E67746F6E assert await _complete_cylc( 'cylc', '62656566', '--77656C6C696E67746F6E=' ) == set() # $ cylc cat-log f assert await _complete_cylc('cylc', 'cat-log', 'f') == {'foo/run2//'} # $ cylc log f # NOTE: "log" is an alias for "cat-log" assert await _complete_cylc('cylc', 'log', 'f') == {'foo/run2//'} # $ cylc help assert 'all' in await _complete_cylc('cylc', 'help', '') # $ cylc version assert '--long' in await _complete_cylc('cylc', 'version', '') async def test_complete_command(): """Test completion for Cylc commands.""" ret = await complete_command('t') assert 'tui' in ret assert 'trigger' in ret ret = await complete_command('tri') assert 'tui' not in ret assert 'trigger' in ret # we should get an empty list for a non-existent command ret = await complete_command('626565662077656C6C696E67746F6E') assert ret == [] async def test_complete_option(): """Test completion for --options of Cylc commands.""" ret = await complete_option('trigger') assert all( item.startswith('-') for item in ret ) assert '--flow' in ret # we should get an empty list for a non-existent options ret = await complete_option('626565662077656C6C696E67746F6E') assert ret == [] assert await complete_option('trigger', '--flow=no') == ['--flow=none'] assert await complete_option('trigger', '--flow=ne') == ['--flow=new'] # we should get None for no existent options, this enables use to fail # over to other completion methods assert await complete_option('trigger', '--float=a') is None async def test_option_value(): """Test completion for values of --options of Cylc commands.""" ret = await complete_option_value('trigger', '--flow') assert 'none' in ret ret = await complete_option_value('trigger', '--flow', 'a') assert 'none' not in ret # we should get None for no existent values, this enables use to fail # over to other completion methods ret = await complete_option_value('626565662077656C6C696E67746F6E', 'x') assert ret is None ret = await complete_option_value('trigger', '626565662077656C6C696E6774') assert ret is None async def test_complete_argument(monkeypatch): """Test completions for positional arguments of Cylc commands.""" # register two fake commands with their own special completions def _complete_arg(x): async def __complete_arg(*args): return x return __complete_arg monkeypatch.setattr( 'cylc.flow.scripts.completion_server.COMMAND_MAP', { 'foo': _complete_arg(['aaa', 'bbb']), 'bar': _complete_arg(['ccc', 'ddd']), 'baz': None, } ) # patch the default ID listing completion async def _list_cylc_id(*args): return ['eee', 'fff'] monkeypatch.setattr( 'cylc.flow.scripts.completion_server.list_cylc_id', _list_cylc_id ) # the foo command should provide foo-specific completions ret = await complete_argument('foo') assert ret == ['aaa', 'bbb'] ret = await complete_argument('foo', 'a') assert ret == ['aaa'] # the bar command should provide bar-specific completions ret = await complete_argument('bar') assert ret == ['ccc', 'ddd'] # the bax command should not provide argument completions ret = await complete_argument('baz') assert ret == [] # all other commands should fallback to the default completions ret = await complete_argument('pub') assert ret == ['eee', 'fff'] async def test_list_cylc_id(monkeypatch): """Test listing Cylc IDs. This test ensures that list_cylc_ids is calling the right interfaces. """ _list_cylc_id = setify(list_cylc_id) async def _list_workflows(): return ['abc', 'bcd', 'cde'] async def _list_in_workflow(tokens): if tokens.workflow_id == 'abc': return ['1', '2'] return ['2', '3'] monkeypatch.setattr( 'cylc.flow.scripts.completion_server.list_workflows', _list_workflows, ) monkeypatch.setattr( 'cylc.flow.scripts.completion_server.list_in_workflow', _list_in_workflow, ) assert await _list_cylc_id(None) == {'abc', 'bcd', 'cde'} assert await _list_cylc_id('a') == {'abc', 'bcd', 'cde'} assert await _list_cylc_id('abc//') == {'1', '2'} assert await _list_cylc_id('bcd//') == {'2', '3'} def test_list_options(monkeypatch): """Test listing of command --options.""" assert '--flow' in list_options('trigger') assert '--color' in list_options('trigger') # we should get an empty list if anything goes wrong assert list_options('zz9+za') == [] # patch the logic to turn off the auto_add behaviour of CylcOptionParser class EntryPoint: def load(self): def _parser_function(): parser = get_option_parser() del parser.auto_add return parser return SimpleNamespace(parser_function=_parser_function) monkeypatch.setitem( COMMANDS, 'trigger', EntryPoint(), ) # with auto_add turned off the --color option should be absent assert '--color' not in list_options('trigger') async def test_list_option_values(monkeypatch): """Test listing of --option values.""" _list_option_values = setify(list_option_values) async def _list_a_options(*args): return ['foo', 'bar', 'baz'] # register two options monkeypatch.setattr( 'cylc.flow.scripts.completion_server.OPTION_MAP', { # --a has a registered completion '--a': _list_a_options, # --b has completions explicitly turned off '--b': None, # --c is not registered }, ) assert await _list_option_values(None, '--a', None) == { 'foo', 'bar', 'baz' } assert await _list_option_values(None, '--b', None) == set() assert await _list_option_values(None, '--c', None) is None async def test_list_workflows(dummy_workflow): """Test listing workflows (via "scan").""" # test list_workflows assert await list_workflows() == ['foo/run2//'] assert await list_workflows(states={'running'}) == [] # test list_src_workflows assert await list_src_workflows(None) == ['foo/run2'] async def test_list_in_workflow(dummy_workflow): """Test listing of "things" within workflows. Things i.e. cycles/tasks/jobs. """ _list_in_workflow = setify(list_in_workflow) # workflow => list cycles assert await _list_in_workflow(Tokens('foo/run2//')) == { 'foo/run2//1/', 'foo/run2//2/', 'foo/run2//3/', } # cycle => list tasks assert await _list_in_workflow(Tokens('foo/run2//1')) == { 'foo/run2//1/foo/', 'foo/run2//1/bar/', 'foo/run2//1/baz/', } # task => list jobs assert await _list_in_workflow(Tokens('foo/run2//1/foo/')) == { 'foo/run2//1/foo/01/', 'foo/run2//1/foo/NN/', } # jobs => nothing to do assert await _list_in_workflow( Tokens('foo/run2//1/foo/01'), ) == set() # no tokens => nothing to list assert await _list_in_workflow(Tokens()) == set() # non-existant workflow => nothing to list assert await _list_in_workflow( Tokens('forty-two'), # set infer_run to false as this workflow does not exist so will raise # an exception # (note exceptions are fine, they get caught and ignored at the top # level) infer_run=False ) == set() # non-existant cycle => nothing to list assert await _list_in_workflow(Tokens('foo/run2//4')) == set() # non-existant task => nothing to list assert await _list_in_workflow(Tokens('foo/run2//4/foo')) == set() # non-existant job => nothing to list assert await _list_in_workflow(Tokens('foo/run2//4/foo/02')) == set() async def test_list_in_workflow_inference(dummy_workflow): """It should infer the latest run when appropriate.""" _list_in_workflow = setify(list_in_workflow) assert await _list_in_workflow(Tokens('foo/run2//')) == { 'foo/run2//1/', 'foo/run2//2/', 'foo/run2//3/', } assert await _list_in_workflow(Tokens('foo//')) == { 'foo//1/', 'foo//2/', 'foo//3/', } async def test_list_resources(): """Test listing of "resources. Resources i.e. things provided by `cylc get-resources`. """ assert 'cylc-completion.bash' in await list_resources(None) async def test_list_dir(tmp_path, monkeypatch): """Test directory listing.""" (tmp_path / 'x').mkdir() ((tmp_path / 'x') / 'y').mkdir() ((tmp_path / 'x') / 'z').touch() monkeypatch.chdir(tmp_path) _list_dir = setify(list_dir) # --- relative paths --- # no "partial" # => list $PWD assert { str(path) for path in await _list_dir(None) } == {'x/'} # no trailing `/` at the end of the path # (i.e. an incomplete path) # => list the parent assert { str(path) for path in await _list_dir('x') } == {'x/'} # # trailing `/` at the end of the path # # (i.e. complete path) # # => list dir path assert { str(path) for path in await _list_dir('x/') } == {'x/y/', 'x/z'} # "y" is a dir, "z" is a file # listing a file # => noting to list, just return the file assert { str(path) for path in await _list_dir('x/z/') } == {'x/z'} # --- absolute paths --- # no trailing `/` at the end of the path # (i.e. an incomplete path) # => list the parent assert { # '/'.join(path.rsplit('/', 2)[-2:]) path.replace(str(tmp_path), '') for path in await _list_dir(str(tmp_path / 'x')) } == {'/x/'} # trailing `/` at the end of the path # (i.e. complete path) # => list dir path assert { path.replace(str(tmp_path), '') for path in await _list_dir(str(tmp_path / 'x') + '/') } == {'/x/y/', '/x/z'} # "y" is a dir, "z" is a file # listing a file # => noting to list, just return the file assert { path.replace(str(tmp_path), '') for path in await _list_dir(str(tmp_path / 'x' / 'z') + '/') } == {'/x/z'} async def test_list_flows(): """Test listing values for the --flow option. Currently this only provides the textual options i.e. it doesn't list "flows" running in a workflow, yet... """ assert 'none' in await list_flows(None) async def test_list_colours(): """Test listing values for the --color option.""" assert 'always' in await list_colours(None) async def test_cli_detokenise(): """Test that Cylc IDs are detokenised with a trailing slash. Cylc completion used the trailing slash to determine that the previous part of the ID has been completed the same way as regular directory completion does. """ assert cli_detokenise(Tokens()) == '' assert cli_detokenise(Tokens('~u/w')) == '~u/w//' assert cli_detokenise(Tokens('~u/w//c')) == '~u/w//c/' assert cli_detokenise(Tokens('~u/w//c/t')) == '~u/w//c/t/' assert cli_detokenise(Tokens('~u/w//c/t/01')) == '~u/w//c/t/01/' def test_get_completion_script_file(): """Test retrieving the completion script file.""" assert get_completion_script_file('bash').exists() # if the requested language is not supported it should return None assert get_completion_script_file('ksh') is None def test_get_current_completion_script_version(tmp_path): """Test extracting the completion script version from the script file. We do this to determine the version of the completion script bundled with this version of Cylc (in order to inform users of upgrades). """ completion_script = get_completion_script_file('bash') # it should extract the version for bash assert get_current_completion_script_version( completion_script, 'bash', ) is not None # it should return None for ksh assert get_current_completion_script_version( completion_script, 'ksh', ) is None # it should return None if it can't find the version completion_script = (tmp_path / 'foo') completion_script.touch() assert get_current_completion_script_version( completion_script, 'bash', ) is None def test_check_completion_script_compatibility(monkeypatch, capsys): """Test whether a completion script is compatible with the server. Incase the server interface changes at a later date this will allow us to exit gracefully rather than crashing in a horrible way. """ # set the bash completion script version to 1.0.1 def _get_current_completion_script_version(_script, lang): if lang == 'bash': return '1.0.1' return None # set the completion script compatibility range to >=1.0.0, <2.0.0 monkeypatch.setattr( 'cylc.flow.scripts.completion_server.REQUIRED_SCRIPT_VERSION', '>=1.0.0, <2.0.0', ) monkeypatch.setattr( 'cylc.flow.scripts.completion_server' '.get_current_completion_script_version', _get_current_completion_script_version ) # versions which match ">=1.0.0, <2.0.0" should be valid assert check_completion_script_compatibility('bash', '0.9.9') is False assert check_completion_script_compatibility('bash', '1.0.0') is True assert check_completion_script_compatibility('bash', '1.0.1') is True assert check_completion_script_compatibility('bash', '1.0.2') is True assert check_completion_script_compatibility('bash', '2.0.0') is False # all versions should be invalid (because we don't offer ksh support) assert check_completion_script_compatibility('ksh', '0.9.9') is False assert check_completion_script_compatibility('ksh', '1.0.0') is False assert check_completion_script_compatibility('ksh', '1.0.1') is False assert check_completion_script_compatibility('ksh', '1.0.2') is False assert check_completion_script_compatibility('ksh', '2.0.0') is False # it should tell the user when a new version of the script is available capsys.readouterr() # clear assert check_completion_script_compatibility('bash', '1.0.0') is True out, err = capsys.readouterr() assert not out # never write to stdout assert 'A new version of the Cylc bash script is available' in err # it should tell the user if the script is incompatible capsys.readouterr() # clear assert check_completion_script_compatibility('bash', '0.9.9') is False out, err = capsys.readouterr() assert not out # never write to stdout assert 'The Cylc bash script needs to be updated' in err # it shouldn't say anything unless necessary capsys.readouterr() # clear assert check_completion_script_compatibility('bash', '1.0.1') is True out, err = capsys.readouterr() assert not out # never write to stdout assert not err async def test_prereqs_and_outputs(): """Test the error cases for listing task prereqs/outputs. The success cases are tested in an integration test (requires a running scheduler). """ # if no tokens are provided, no prereqs or outputs are returned assert await _list_prereqs_and_outputs([]) == ([], []) # if an invalid workflow is provided, we can't list anything assert await _list_prereqs_and_outputs( [Tokens(workflow='no-such-workflow')] ) == ([], []) cylc-flow-8.6.4/tests/unit/scripts/test_scan.py0000664000175000017500000000550515202510242021757 0ustar alastairalastair# 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 . import pytest from pathlib import Path from cylc.flow.scripts.scan import ( ScanOptions, get_pipe, _format_plain, _construct_tree, FLOW_STATES, BAD_CONTACT_FILE_MSG ) def test_no_connection(): """Ensure scan uses the filesystem where possible.""" pipe = get_pipe(ScanOptions(states=FLOW_STATES), _format_plain) assert 'graphql_query' not in repr(pipe) def test_ping_connection(): """Ensure scan always connects to the flow when requested via --ping.""" pipe = get_pipe(ScanOptions(states=FLOW_STATES, ping=True), _format_plain) assert 'graphql_query' in repr(pipe) def test_good_contact_info() -> None: """Check correct reporting of workflow contact information.""" port = 8888 host = "wizard" name = "blargh/run1" pid = 12345 res = _format_plain( { "name": name, "contact": Path(f"/path/to/{name}"), "CYLC_WORKFLOW_HOST": host, "CYLC_WORKFLOW_PORT": port, "CYLC_WORKFLOW_PID": pid, }, None ) assert name in res assert f"{host}:{port} {pid}" in res def test_bad_contact_info(caplog: pytest.LogCaptureFixture) -> None: """Check correct reporting of bad workflow contact information. Missing contact keys should result in a warning. """ name = "blargh/run1" _format_plain( { "name": name, "contact": Path(f"/path/to/{name}"), }, None ) assert BAD_CONTACT_FILE_MSG.format(flow_name=name) in caplog.text def test_bad_contact_info_tree(caplog: pytest.LogCaptureFixture) -> None: """Check correct reporting of bad workflow contact information. Missing contact keys should result in a warning. """ name = "blargh/run1" flows = [{ "name": name, "contact": Path(f"/path/to/{name}"), }] tree = {} _construct_tree(flows, tree, _format_plain, None, None) # Error during tree formatting: reports only the last name component. assert ( BAD_CONTACT_FILE_MSG.format(flow_name=f"{Path(name).name}") in caplog.text ) cylc-flow-8.6.4/tests/unit/scripts/test_reinstall.py0000664000175000017500000000434015202510242023024 0ustar alastairalastair# 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 . from textwrap import dedent from ansimarkup import parse as cparse import pytest from cylc.flow.scripts.reinstall import format_reinstall_output from cylc.flow.terminal import DIM @pytest.mark.parametrize('verbose', [True, False]) def test_format_reinstall_output(verbose, monkeypatch: pytest.MonkeyPatch): """It should: - colorize the output - remove the itemized changes summary if not in verbose mode - remove the "cannot delete non-empty directory" message """ output = dedent(""" *deleting del. Cloud.jpg >f+++++++++ send cloud.jpg .f...p..... send foo >fcsTp..... send bar cannot delete non-empty directory: scarf >f+++++++++ send meow.txt cL+++++++++ send garage -> foo """).strip() expected = [ f"<{DIM}>*deleting del. Cloud.jpg", f"<{DIM}>>f+++++++++ send cloud.jpg", f"<{DIM}>.f...p..... send foo", f"<{DIM}>>fcsTp..... send bar", f"<{DIM}>>f+++++++++ send meow.txt", f"<{DIM}>cL+++++++++ send garage -> foo", ] if verbose: monkeypatch.setattr('cylc.flow.flags.verbosity', 1) else: # itemized changes summary should not be in output shift = len(f'<{DIM}> ') + 11 expected = [i[shift:] for i in expected] assert format_reinstall_output(output) == [cparse(i) for i in expected] cylc-flow-8.6.4/tests/unit/scripts/test_cylc.py0000664000175000017500000001126515202510242021765 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import os import sys from types import SimpleNamespace from typing import Callable from unittest.mock import Mock import pytest from cylc.flow.scripts.cylc import iter_commands, pythonpath_manip @pytest.fixture def mock_entry_points(monkeypatch: pytest.MonkeyPatch): """Mock a range of entry points.""" def _load_fail(*args, **kwargs): raise ModuleNotFoundError('foo') def _resolve_ok(*args, **kwargs): return Mock() def _require_ok(*args, **kwargs): return def _mocked_entry_points(include_bad: bool = False): commands = { # an entry point with all dependencies installed: 'good': SimpleNamespace( name='good', module='os.path', load=_resolve_ok, extras=[], dist=SimpleNamespace(name='a'), ), # an entry point with optional dependencies missing: 'missing': SimpleNamespace( name='missing', module='not.a.python.module', # force an import error load=_load_fail, extras=[], dist=SimpleNamespace(name='foo'), ), } if include_bad: # an entry point with non-optional dependencies unexpectedly # missing: commands['bad'] = SimpleNamespace( name='bad', module='not.a.python.module', load=_load_fail, require=_require_ok, extras=[], dist=SimpleNamespace(name='d'), ) monkeypatch.setattr('cylc.flow.scripts.cylc.COMMANDS', commands) return _mocked_entry_points def test_iter_commands(mock_entry_points): """Test listing commands works ok. It should exclude commands with missing optional dependencies. """ mock_entry_points() commands = list(iter_commands()) assert [i[0] for i in commands] == ['good'] def test_iter_commands_bad(mock_entry_points): """Test listing commands doesn't fail on import error.""" mock_entry_points(include_bad=True) list(iter_commands()) def test_execute_cmd( mock_entry_points, capsys: pytest.CaptureFixture, ): """It should fail with a warning for commands with missing dependencies.""" # (stop IDEs reporting code as unreachable in this test) execute_cmd: Callable from cylc.flow.scripts.cylc import execute_cmd mock_entry_points(include_bad=True) # the "good" entry point should exit 0 (exit with no args) assert execute_cmd('good') == 0 assert capsys.readouterr().err == '' # the "missing" entry point should exit 1 with a warning to stderr assert execute_cmd('missing') == 1 assert capsys.readouterr().err.strip() == ( '"cylc missing" requires "foo"\n\nModuleNotFoundError: foo' ) # the "bad" entry point should log an error assert execute_cmd('bad') == 1 stderr = capsys.readouterr().err.strip() assert '"cylc bad" requires "d"' in stderr assert 'ModuleNotFoundError: foo' in stderr def test_pythonpath_manip(monkeypatch): """pythonpath_manip removes items in PYTHONPATH from sys.path and adds items from CYLC_PYTHONPATH """ # Local CYLC_PYTHONPATH can mess with this test. monkeypatch.delenv('CYLC_PYTHONPATH', raising=False) monkeypatch.setenv('PYTHONPATH', '/remove1:/remove2') monkeypatch.setattr('sys.path', ['/leave-alone', '/remove1', '/remove2']) pythonpath_manip() # ... we don't change PYTHONPATH assert os.environ['PYTHONPATH'] == '/remove1:/remove2' # ... but we do remove PYTHONPATH items from sys.path, and don't remove # items there not in PYTHONPATH assert sys.path == ['/leave-alone'] # If CYLC_PYTHONPATH is set we retrieve its contents and # add them to the sys.path: monkeypatch.setenv('CYLC_PYTHONPATH', '/add1:/add2') pythonpath_manip() assert sys.path == ['/add1', '/add2', '/leave-alone'] cylc-flow-8.6.4/tests/unit/scripts/test_install.py0000664000175000017500000000426015202510242022476 0ustar alastairalastair# 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 . import os.path from pathlib import Path from typing import Optional import pytest from cylc.flow.scripts.install import get_source_location @pytest.mark.parametrize( 'path, expected', [ pytest.param( 'isla/nublar', '{cylc_src}/isla/nublar', id="implicit relative" ), pytest.param( './isla/nublar', '{cwd}/isla/nublar', id="explicit relative" ), pytest.param( '/welcome/to/jurassic/park', '/welcome/to/jurassic/park', id="absolute" ), pytest.param( None, '{cwd}', id="None" ), pytest.param( '.', '{cwd}', id="dot" ), pytest.param( '$GENNARO/coupon-day', '{env_var}/coupon-day', id="env var expanded" ), ] ) def test_get_source_location( path: Optional[str], expected: str, monkeypatch: pytest.MonkeyPatch ): # Setup mock_cylc_src = '/ingen/cylc-src' monkeypatch.setattr( 'cylc.flow.scripts.install.search_install_source_dirs', lambda x: Path(mock_cylc_src, x) ) mock_env_var = '/donald/gennaro' monkeypatch.setenv('GENNARO', mock_env_var) expected = expected.format( cwd=Path.cwd(), cylc_src=mock_cylc_src, env_var=mock_env_var, ) # Test assert get_source_location(path) == Path(expected) assert os.path.isabs(expected) cylc-flow-8.6.4/tests/unit/scripts/test_validate.py0000664000175000017500000000000015202510242022605 0ustar alastairalastaircylc-flow-8.6.4/tests/unit/scripts/test_hold.py0000664000175000017500000000347515202510242021765 0ustar alastairalastair# 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 . """Test logic in cylc-hold script.""" from optparse import Values import pytest from typing import Iterable, Optional, Tuple, Type from cylc.flow.exceptions import InputError from cylc.flow.option_parsers import Options from cylc.flow.scripts.hold import get_option_parser, _validate Opts = Options(get_option_parser()) @pytest.mark.parametrize( 'opts, task_globs, expected_err', [ (Opts(), ['*'], None), (Opts(hold_point_string='2'), [], None), ( Opts(hold_point_string='2'), ['*'], (InputError, "Cannot combine --after with Cylc/Task ID") ), ( Opts(), [], (InputError, "Must define Cycles/Tasks") ), ] ) def test_validate( opts: Values, task_globs: Iterable[str], expected_err: Optional[Tuple[Type[Exception], str]]): if expected_err: err, msg = expected_err with pytest.raises(err) as exc: _validate(opts, *task_globs) assert msg in str(exc.value) else: _validate(opts, *task_globs) cylc-flow-8.6.4/tests/unit/scripts/test_lint_checkers.py0000664000175000017500000000611315202510242023644 0ustar alastairalastair#!/usr/bin/env python3 # 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 . """Test check functions in the `cylc lint` CLI Utility.""" import doctest import json import pytest import re from cylc.flow.scripts import lint VARS = re.compile(r'\{(.*)\}') # Functions in Cylc Lint defined with "check_* CHECKERS = [ getattr(lint, i) for i in lint.__dir__() if i.startswith('check_')] # List of checks defined as checks by Cylc Lint ALL_CHECKS = [ *lint.MANUAL_DEPRECATIONS.values(), *lint.STYLE_CHECKS.values(), ] finder = doctest.DocTestFinder() @pytest.mark.parametrize( 'check', # Those checks that have custom checker functions # and a short message with variables to insert: [ pytest.param(c, id=c.get('function').__name__) for c in ALL_CHECKS if c.get('function') in CHECKERS ] ) def test_custom_checker_doctests(check): """All check functions have at least one failure doctest By forcing each check function to have valid doctests for the case that linting has failed we are able to check that the function outputs the correct information for formatting the short formats. """ doctests = finder.find(check['function'])[0] msg = f'{check["function"].__name__}: No failure examples in doctest' assert any(i.want for i in doctests.examples if i.want), msg @pytest.mark.parametrize( 'ref, check', # Those checks that have custom checker functions # and a short message with variables to insert: [ (c.get('function'), c) for c in ALL_CHECKS if c.get('function') in CHECKERS and VARS.findall(c['short']) ] ) def test_custom_checkers_short_formatters(ref, check): """If a check message has a format string assert that the checker function will return a dict to be used in ``check['short'].format(**kwargs)``, based on doctest output. ref is useful to allow us to identify the check, even though not used in the test. """ doctests = finder.find(check['function'])[0] # Filter doctest examples for cases where there is a json parsable # want. examples = [ eg for eg in [ json.loads(e.want.replace("'", '"')) for e in doctests.examples if e.want ] if eg ] # Formatting using the example output changes the check short text: for example in examples: assert check['short'].format(**example) != check['short'] cylc-flow-8.6.4/tests/unit/scripts/test_stop.py0000664000175000017500000000511415202510242022014 0ustar alastairalastair# 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 . """Test logic in cylc-stop script.""" import pytest from typing import TYPE_CHECKING, Optional, Tuple, Type from cylc.flow.exceptions import InputError from cylc.flow.option_parsers import Options from cylc.flow.scripts.stop import get_option_parser, _validate if TYPE_CHECKING: from optparse import Values Opts = Options(get_option_parser()) @pytest.mark.parametrize( 'options, stop_task, stop_cycle, globs, expected_err', [ ( Opts(), None, None, None, None, ), ( Opts(kill=True), None, '10', None, (InputError, "--kill is not compatible with stop-cycle") ), ( Opts(), '10/foo', '10', None, (InputError, "stop-task is not compatible with stop-cycle") ), ( Opts(kill=True, now=True), None, None, None, (InputError, "--kill is not compatible with --now") ), ( Opts(flow_num=2, max_polls=2), None, None, None, (InputError, "--flow is not compatible with --max-polls") ), ( Opts(flow_num=2), None, None, '*', (InputError, "--flow is not compatible with task IDs") ), ] ) def test_validate( options: 'Values', stop_task: str, stop_cycle: str, globs: str, expected_err: Optional[Tuple[Type[Exception], str]]): if expected_err: err, msg = expected_err with pytest.raises(err) as exc: _validate(options, stop_task, stop_cycle, globs) assert msg in str(exc.value) else: _validate(options, stop_task, stop_cycle, globs) cylc-flow-8.6.4/tests/unit/scripts/test_clean.py0000664000175000017500000000760415202510242022117 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from typing import Callable, List, Type, Union import pytest from cylc.flow.exceptions import InputError from cylc.flow.scripts.clean import ( CleanOptions, _main, parse_timeout, scan, run ) async def test_scan(tmp_run_dir): """It should scan the filesystem to expand partial IDs.""" # regular workflows pass straight through tmp_run_dir('foo') workflows, multi_mode = await scan(['foo'], False) assert workflows == ['foo'] assert multi_mode is False # hierarchies, however, get expanded tmp_run_dir('bar/run1') workflows, multi_mode = await scan(['bar'], False) assert workflows == ['bar/run1'] assert multi_mode is True # because an expansion has happened tmp_run_dir('bar/run2') workflows, multi_mode = await scan(['bar'], False) assert workflows == ['bar/run1', 'bar/run2'] assert multi_mode is True @pytest.fixture def mute(monkeypatch: pytest.MonkeyPatch) -> List[str]: """Stop cylc clean from doing anything and log all init_clean calls.""" items = [] def _clean(id_, *_): items.append(id_) monkeypatch.setattr('cylc.flow.scripts.clean.init_clean', _clean) monkeypatch.setattr('cylc.flow.scripts.clean.prompt', lambda x: None) return items async def test_multi(tmp_run_dir: Callable, mute: List[str]): """It supports cleaning multiple workflows.""" # cli opts opts = CleanOptions() # create three dummy workflows tmp_run_dir('bar/pub/beer') tmp_run_dir('baz/run1') tmp_run_dir('foo') # an explicit workflow ID goes straight through mute.clear() await run('foo', opts=opts) assert mute == ['foo'] # a partial hierarchical ID gets expanded to all workflows contained # in the hierarchy (note runs are a special case of hierarchical ID) mute.clear() await run('bar', opts=opts) assert mute == ['bar/pub/beer'] # test a mixture of explicit and partial IDs mute.clear() await run('bar', 'baz', 'foo', opts=opts) assert mute == ['bar/pub/beer', 'baz/run1', 'foo'] # test a glob mute.clear() await run('*', opts=opts) assert mute == ['bar/pub/beer', 'baz/run1', 'foo'] @pytest.mark.parametrize( 'timeout, expected', [('100', '100'), ('PT1M2S', '62'), ('', ''), ('oopsie', InputError), (' ', InputError)] ) def test_parse_timeout( timeout: str, expected: Union[str, Type[InputError]] ): """It should accept ISO 8601 format or number of seconds.""" opts = CleanOptions(remote_timeout=timeout) if expected is InputError: with pytest.raises(expected): parse_timeout(opts) else: parse_timeout(opts) assert opts.remote_timeout == expected @pytest.mark.parametrize( 'opts, expected_msg', [ ({'local_only': True, 'remote_only': True}, "mutually exclusive"), ({'remote_timeout': 'oops'}, "Invalid timeout"), ] ) def test_bad_user_input(opts: dict, expected_msg: str, mute): """It should raise an InputError for bad user input.""" with pytest.raises(InputError) as exc_info: _main(CleanOptions(**opts), 'blah') assert expected_msg in str(exc_info.value) cylc-flow-8.6.4/tests/unit/scripts/test_cat_log.py0000664000175000017500000000541715202510242022445 0ustar alastairalastair# 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 . from subprocess import Popen, PIPE from ansimarkup import parse as cparse from colorama import Style import pytest from cylc.flow.loggingutil import CylcLogFormatter from cylc.flow.scripts.cat_log import ( colorise_cat_log, ) @pytest.fixture def log_file(tmp_path): _log_file = tmp_path / 'log' with open(_log_file, 'w+') as fh: fh.write('DEBUG - 1\n') fh.write('INFO - 2\n') fh.write('WARNING - 3\n') fh.write('ERROR - 4\n') fh.write('CRITICAL - 5\n') return _log_file def test_colorise_cat_log_plain(log_file): """It should not colourise logs when color=False.""" # command for colorise_cat_log to colourise cat_proc = Popen( ['cat', str(log_file)], stdout=PIPE, ) colorise_cat_log(cat_proc, color=False) assert cat_proc.communicate()[0].decode().splitlines() == [ # there should not be any ansii color characters here 'DEBUG - 1', 'INFO - 2', 'WARNING - 3', 'ERROR - 4', 'CRITICAL - 5', ] def test_colorise_cat_log_colour(log_file): """It should colourise logs when color=True.""" # command for colorise_cat_log to colourise cat_proc = Popen( ['cat', str(log_file)], stdout=PIPE, ) out, err = colorise_cat_log(cat_proc, color=True, stdout=PIPE) # strip the line breaks (because tags can come before or after them) # strip the reset tags (because they might not be needed if redeclared) out = ''.join( line.replace(Style.RESET_ALL, '') for line in out.decode().splitlines() ) col = CylcLogFormatter.COLORS assert out == ( ''.join([ # strip the reset tags cparse(line).replace(Style.RESET_ALL, '') for line in [ col['DEBUG'].format('DEBUG - 1'), 'INFO - 2', col['WARNING'].format('WARNING - 3'), col['ERROR'].format('ERROR - 4'), col['CRITICAL'].format('CRITICAL - 5'), '' ] ]) ) cylc-flow-8.6.4/tests/unit/test_pipe_poller.py0000664000175000017500000000236215202510242021654 0ustar alastairalastair# 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 . from subprocess import Popen, PIPE from cylc.flow.pipe_poller import pipe_poller def test_pipe_poller_str(): proc = Popen(['echo', 'Hello World!'], stdout=PIPE, text=True) (stdout,) = pipe_poller(proc, proc.stdout) assert proc.returncode == 0 assert stdout == 'Hello World!\n' def test_pipe_poller_bytes(): proc = Popen(['echo', 'Hello World!'], stdout=PIPE, text=False) (stdout,) = pipe_poller(proc, proc.stdout) assert proc.returncode == 0 assert stdout == b'Hello World!\n' cylc-flow-8.6.4/tests/unit/job_runner_handlers/0000775000175000017500000000000015202510242021751 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/job_runner_handlers/test_pbs.py0000664000175000017500000001273515202510242024156 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.pbs import ( JOB_RUNNER_HANDLER, PBSHandler ) from cylc.flow.job_runner_mgr import JobRunnerManager VERY_LONG_STR = 'x' * 240 @pytest.mark.parametrize( 'job_conf,lines', [ pytest.param( { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', 'platform': { 'job runner': 'pbs', 'job name length maximum': 100, }, }, [ '#PBS -N axe.1.chop', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', ], id='basic', ), pytest.param( { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': VERY_LONG_STR, 'platform': { 'job runner': 'pbs', }, }, [ '#PBS -N ' f'None.{VERY_LONG_STR[: PBSHandler.JOB_NAME_LEN_MAX - 5]}', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', ], id='long-job-name', ), pytest.param( { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', 'platform': { 'job runner': 'pbs', 'job name length maximum': 6, }, }, [ '#PBS -N axe.1.', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', ], id='truncate-job-name', ), pytest.param( { 'directives': { '-q': 'forever', '-V': '', '-l mem': '256gb', }, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', 'platform': { 'job runner': 'pbs', 'job name length maximum': 100, }, }, [ '#PBS -N axe.1.chop', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', '#PBS -q forever', '#PBS -V', '#PBS -l mem=256gb', ], id='custom-directives', ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines def test_filter_poll_many_output(): """It should strip trailing junk from job IDs. Job IDs are assumed to be a series of numbers, optionally followed by a full-stop and some other letters and numbers which are not needed for job tracking purposes. Job IDs are not expected to start with letters e.g. `abc.456` is not supported. """ assert JOB_RUNNER_HANDLER.filter_poll_many_output(''' Job id Name User Time Use S Queue ---------------- ---------------- ---------------- -------- - ----- 12345.foo.bar.baz test-pbs xxxxxxx 0 Q reomq 23456.foo test-pbs xxxxxxx 0 Q romeq 34567 test-pbs xxxxxxx 1 Q romeq abc.456 test-pbs xxxxxxx 2 Q romeq abcdef test-pbs xxxxxxx 2 Q romeq ''') == ['12345', '23456', '34567'] def test_filter_submit_output(tmp_path): """See notes for test_filter_poll_many_output.""" status_file = tmp_path / 'submit_out' status_file.touch() def test(out): return JobRunnerManager._filter_submit_output( status_file, JOB_RUNNER_HANDLER, out, '', )[2] assert test(' 12345.foo.bar.baz') == '12345' assert test(' 12345.foo') == '12345' assert test(' 12345') == '12345' cylc-flow-8.6.4/tests/unit/job_runner_handlers/test_lsf.py0000664000175000017500000000460715202510242024155 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.lsf import JOB_RUNNER_HANDLER @pytest.mark.parametrize( 'job_conf,lines', [ ( # basic { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#BSUB -J axe.1.chop', '#BSUB -o cylc-run/chop/log/job/1/axe/01/job.out', '#BSUB -e cylc-run/chop/log/job/1/axe/01/job.err', '#BSUB -W 3', ], ), ( # some useful directives { 'directives': { '-q': 'forever', '-B': '', '-ar': '', }, 'execution_time_limit': 200, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#BSUB -J axe.1.chop', '#BSUB -o cylc-run/chop/log/job/1/axe/01/job.out', '#BSUB -e cylc-run/chop/log/job/1/axe/01/job.err', '#BSUB -W 4', '#BSUB -q forever', '#BSUB -B', '#BSUB -ar', ], ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines def test_get_submit_stdin(): outs = JOB_RUNNER_HANDLER.get_submit_stdin(__file__, None) assert outs[0].name == __file__ assert outs[1] is None cylc-flow-8.6.4/tests/unit/job_runner_handlers/test_moab.py0000664000175000017500000000440715202510242024305 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.moab import JOB_RUNNER_HANDLER @pytest.mark.parametrize( 'job_conf,lines', [ ( # basic { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#PBS -N axe.1.chop', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', ], ), ( # some useful directives { 'directives': { '-q': 'forever', '-V': '', '-l mem': '256gb', }, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#PBS -N axe.1.chop', '#PBS -o cylc-run/chop/log/job/1/axe/01/job.out', '#PBS -e cylc-run/chop/log/job/1/axe/01/job.err', '#PBS -l walltime=180', '#PBS -q forever', '#PBS -V', '#PBS -l mem=256gb', ], ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines cylc-flow-8.6.4/tests/unit/job_runner_handlers/test_slurm_packjob.py0000664000175000017500000000563015202510242026221 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.slurm_packjob import JOB_RUNNER_HANDLER @pytest.mark.parametrize( 'job_conf,lines', [ ( # heterogeneous job, old-style (packjob) { 'directives': { '-p': 'middle', 'packjob_0_--mem': '128gb', 'packjob_1_--mem': '256gb', }, 'execution_time_limit': 200, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#SBATCH --job-name=axe.1.chop', ( '#SBATCH --output=' 'cylc-run/chop/log/job/1/axe/01/job.out' ), ( '#SBATCH --error=' 'cylc-run/chop/log/job/1/axe/01/job.err' ), '#SBATCH --time=3:20', '#SBATCH -p=middle', '#SBATCH --mem=128gb', '#SBATCH packjob', '#SBATCH --mem=256gb', ], ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines @pytest.mark.parametrize( 'job_ids,cmd', [ [['1234567'], ['squeue', '-h', '-j', '1234567']], [ ['1234567', '709394', '30624700'], ['squeue', '-h', '-j', '1234567,709394,30624700'], ], ], ) def test_get_poll_many_cmd(job_ids: list, cmd: list): assert JOB_RUNNER_HANDLER.get_poll_many_cmd(job_ids) == cmd @pytest.mark.parametrize( 'out,job_ids', [ [ """HEADING 1234567 JOB PROPERTIES 709394 JOB PROPERTIES 30624700 JOB PROPERTIES """, ['1234567', '30624700', '709394'], ], [ """HEADING 1234567+0 JOB PROPERTIES (HETERO) 1234567+1 JOB PROPERTIES (HETERO) 709394 JOB PROPERTIES 30624700 JOB PROPERTIES """, ['1234567', '30624700', '709394'], ], ], ) def test_filter_poll_many_output(job_ids: list, out: str): assert sorted(JOB_RUNNER_HANDLER.filter_poll_many_output(out)) == job_ids cylc-flow-8.6.4/tests/unit/job_runner_handlers/test_slurm.py0000664000175000017500000001232415202510242024526 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.slurm import JOB_RUNNER_HANDLER @pytest.mark.parametrize( 'job_conf,lines', [ ( # basic { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#SBATCH --job-name=axe.1.chop', ( '#SBATCH --output=' 'cylc-run/chop/log/job/1/axe/01/job.out' ), ( '#SBATCH --error=' 'cylc-run/chop/log/job/1/axe/01/job.err' ), '#SBATCH --time=3:00', ], ), ( # task name with % character { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': ( 'cylc-run/chop/log/job/1/axe%40HEAD/01/job' ), 'workflow_name': 'chop', 'task_id': '1/axe%40HEAD', }, [ '#SBATCH --job-name=axe%40HEAD.1.chop', ( '#SBATCH --output' '=cylc-run/chop/log/job/1/axe%%40HEAD/01/job.out' ), ( '#SBATCH --error' '=cylc-run/chop/log/job/1/axe%%40HEAD/01/job.err' ), '#SBATCH --time=3:00', ], ), ( # some useful directives { 'directives': { '-p': 'middle', '--no-requeue': '', '--mem': '256gb', }, 'execution_time_limit': 200, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#SBATCH --job-name=axe.1.chop', ( '#SBATCH --output=' 'cylc-run/chop/log/job/1/axe/01/job.out' ), ( '#SBATCH --error=' 'cylc-run/chop/log/job/1/axe/01/job.err' ), '#SBATCH --time=3:20', '#SBATCH -p=middle', '#SBATCH --no-requeue', '#SBATCH --mem=256gb', ], ), ( # heterogeneous job { 'directives': { '-p': 'middle', 'hetjob_0_--mem': '128gb', 'hetjob_1_--mem': '256gb', }, 'execution_time_limit': 200, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '#SBATCH --job-name=axe.1.chop', ( '#SBATCH --output=' 'cylc-run/chop/log/job/1/axe/01/job.out' ), ( '#SBATCH --error=' 'cylc-run/chop/log/job/1/axe/01/job.err' ), '#SBATCH --time=3:20', '#SBATCH -p=middle', '#SBATCH --mem=128gb', '#SBATCH hetjob', '#SBATCH --mem=256gb', ], ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines @pytest.mark.parametrize( 'job_ids,cmd', [ [['1234567'], ['squeue', '-h', '-j', '1234567']], [ ['1234567', '709394', '30624700'], ['squeue', '-h', '-j', '1234567,709394,30624700'], ], ], ) def test_get_poll_many_cmd(job_ids: list, cmd: list): assert JOB_RUNNER_HANDLER.get_poll_many_cmd(job_ids) == cmd @pytest.mark.parametrize( 'out,job_ids', [ [ """HEADING 1234567 JOB PROPERTIES 709394 JOB PROPERTIES 30624700 JOB PROPERTIES """, ['1234567', '30624700', '709394'], ], [ """HEADING 1234567+0 JOB PROPERTIES (HETERO) 1234567+1 JOB PROPERTIES (HETERO) 709394 JOB PROPERTIES 30624700 JOB PROPERTIES """, ['1234567', '30624700', '709394'], ], ], ) def test_filter_poll_many_output(job_ids: list, out: str): assert sorted(JOB_RUNNER_HANDLER.filter_poll_many_output(out)) == job_ids cylc-flow-8.6.4/tests/unit/job_runner_handlers/test_loadleveler.py0000664000175000017500000000574115202510242025667 0ustar alastairalastair# 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 . import pytest from cylc.flow.job_runner_handlers.loadleveler import JOB_RUNNER_HANDLER from cylc.flow.job_runner_handlers.loadleveler import LoadlevelerHandler @pytest.mark.parametrize( 'job_conf,lines', [ ( # basic { 'directives': {}, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '# @ job_name = chop.axe.1', '# @ output = cylc-run/chop/log/job/1/axe/01/job.out', '# @ error = cylc-run/chop/log/job/1/axe/01/job.err', '# @ wall_clock_limit = 240,180', '# @ queue' ], ), ( # some useful directives { 'directives': { '-q': 'forever', '-V': '', '-l mem': '256gb', }, 'execution_time_limit': 180, 'job_file_path': 'cylc-run/chop/log/job/1/axe/01/job', 'workflow_name': 'chop', 'task_id': '1/axe', }, [ '# @ job_name = chop.axe.1', '# @ output = cylc-run/chop/log/job/1/axe/01/job.out', '# @ error = cylc-run/chop/log/job/1/axe/01/job.err', '# @ wall_clock_limit = 240,180', '# @ -q = forever', '# @ -V', '# @ -l mem = 256gb', '# @ queue' ], ), ], ) def test_format_directives(job_conf: dict, lines: list): assert JOB_RUNNER_HANDLER.format_directives(job_conf) == lines def test_filter_poll_many_output(): configuration = ''' Id Owner Submitted ST PRI Class Running On ---------------- ---------- ----------- -- --- -------- ---------- mars.498.0 brownap 5/20 11:31 R 100 silver mars mars.499.0 brownap 5/20 11:31 R 50 No_Class mars mars.501.0 brownap 5/20 11:31 I 50 silver ''' out = ['Id', '----------------', 'mars.498', 'mars.499', 'mars.501'] assert LoadlevelerHandler.filter_poll_many_output(configuration) == out cylc-flow-8.6.4/tests/unit/test_cylc_subproc.py0000664000175000017500000000771415202510242022037 0ustar alastairalastair# 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 . import unittest from cylc.flow.cylc_subproc import procopen from unittest.mock import call from testfixtures import compare from testfixtures.popen import PIPE, MockPopen # Method could be a function # pylint: disable=no-self-use class TestSubprocessSafe(unittest.TestCase): """Unit tests for the parameter procopen utility function""" def setUp(self): self.Popen = MockPopen() def test_sprocess_communicate_with_process(self): foo = ' foo' bar = ' bar' cmd = ["echo", "this is a command" + foo + bar] p = procopen(cmd, stdoutpipe=True) stdout, _ = p.communicate() compare(stdout, b"this is a command foo bar\n") def test_sprocess_communicate_with_input(self): command = "a command" Popen = MockPopen() Popen.set_command(command) # only static input used with simulated mockpopen # codacy mistakenly sees this as a call to popen process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True) # nosec err, out = process.communicate('foo') compare([ # only static input used with simulated mockpopen # codacy mistakenly sees this as a call to popen call.Popen(command, shell=True, stderr=-1, stdout=-1), # nosec call.Popen_instance.communicate('foo'), ], Popen.mock.method_calls) return err, out def test_sprocess_safe_read_from_stdout_and_stderr(self): command = "a command" Popen = MockPopen() # only static input used with simulated mockpopen # codacy mistakenly sees this as a call to popen Popen.set_command(command, stdout=b'foo', stderr=b'bar') process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True) # nosec compare(process.stdout.read(), b'foo') compare(process.stderr.read(), b'bar') compare([ call.Popen(command, shell=True, stderr=PIPE, # nosec stdout=PIPE), ], Popen.mock.method_calls) def test_sprocess_safe_write_to_stdin(self): command = "a command" Popen = MockPopen() Popen.set_command(command) # only static input used with simulated mockpopen # codacy mistakenly sees this as a call to popen process = Popen(command, stdin=PIPE, shell=True) # nosec process.stdin.write(command) process.stdin.close() compare([ # static input used with simulated mockpopen # codacy mistakenly sees this as a call to popen call.Popen(command, shell=True, stdin=PIPE), # nosec call.Popen_instance.stdin.write(command), call.Popen_instance.stdin.close(), ], Popen.mock.method_calls) def test_sprocess_safe_wait_and_return_code(self): command = "a command" Popen = MockPopen() Popen.set_command(command, returncode=3) process = Popen(command) compare(process.returncode, None) compare(process.wait(), 3) compare(process.returncode, 3) compare([ call.Popen(command), call.Popen_instance.wait(), ], Popen.mock.method_calls) cylc-flow-8.6.4/tests/unit/test_templatevars.py0000664000175000017500000001545115202510242022054 0ustar alastairalastair# 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 . from pathlib import Path import pytest from pytest import param import tempfile import unittest from cylc.flow import __version__ as cylc_version from cylc.flow.exceptions import ServiceFileError, InputError from cylc.flow.rundb import CylcWorkflowDAO from cylc.flow.templatevars import ( get_template_vars_from_db, load_template_vars ) from cylc.flow.workflow_files import WorkflowFiles class TestTemplatevars(unittest.TestCase): def test_load_template_vars_no_params(self): self.assertFalse(load_template_vars()) def test_load_template_vars_from_string(self): pairs = [ "name='John'", "type='Human'", "age='12'" ] expected = { "name": "John", "type": "Human", "age": "12" } self.assertEqual(expected, load_template_vars(template_vars=pairs)) def test_load_template_vars_from_file(self): with tempfile.NamedTemporaryFile() as tf: tf.write(""" name='John' type='Human' # a comment # type=Test age='12' """.encode()) tf.flush() expected = { "name": "John", "type": "Human", "age": "12" } self.assertEqual( expected, load_template_vars(template_vars=None, template_vars_file=tf.name)) def test_load_template_vars_from_string_and_file_1(self): """Text pair variables take precedence over file.""" pairs = [ "name='John'", "age='12'" ] with tempfile.NamedTemporaryFile() as tf: tf.write(""" name='Mariah' type='Human' # a comment # type=Test age='70' """.encode()) tf.flush() expected = { "name": "John", "type": "Human", "age": "12" } self.assertEqual( expected, load_template_vars(template_vars=pairs, template_vars_file=tf.name)) def test_load_template_vars_from_string_and_file_2(self): """Text pair variables take precedence over file.""" pairs = [ "str='str'", "int=12", "float=12.3", "bool=True", "none=None" ] expected = { 'str': 'str', 'int': 12, 'float': 12.3, 'bool': True, 'none': None } self.assertEqual(expected, load_template_vars(template_vars=pairs)) @pytest.fixture(scope='module') def _setup_db(tmp_path_factory): tmp_path: Path = tmp_path_factory.mktemp('test_get_old_tvars') logfolder = tmp_path / WorkflowFiles.LogDir.DIRNAME logfolder.mkdir() db_path = logfolder / WorkflowFiles.LogDir.DB with CylcWorkflowDAO(db_path, create_tables=True) as dao: dao.connect().execute( r''' INSERT INTO workflow_params VALUES (?, ?) ''', ("cylc_version", cylc_version), ) dao.connect().executemany( r''' INSERT INTO workflow_template_vars VALUES (?, ?) ''', [ ("FOO", "42"), ("BAR", "'hello world'"), ("BAZ", "'foo', 'bar', 48"), ("QUX", "['foo', 'bar', 21]"), ], ) dao.connect().commit() yield get_template_vars_from_db(tmp_path) @pytest.mark.parametrize( 'key, expect', ( ('FOO', 42), ('BAR', 'hello world'), ('BAZ', ('foo', 'bar', 48)), ('QUX', ['foo', 'bar', 21]) ) ) def test_get_old_tvars(key, expect, _setup_db): """It can extract a variety of items from a workflow database. """ assert _setup_db[key] == expect def test_get_old_tvars_fails_if_cylc_7_db(tmp_path): """get_template_vars_from_db fails with error if db file is not a valid Cylc 8 DB. """ dbfile = tmp_path / WorkflowFiles.LogDir.DIRNAME / WorkflowFiles.LogDir.DB dbfile.parent.mkdir() dbfile.touch() with pytest.raises(ServiceFileError, match='database is incompatible'): get_template_vars_from_db(tmp_path) @pytest.mark.parametrize( 'tvars_lists, expect', ( param( ['FOO=a,b,c'], {'FOO': ['a', 'b', 'c']}, id='basic' ), param( ['FOO=a,"b,b",c'], {'FOO': ['a', 'b,b', 'c']}, id='contains-quotes' ), ) ) def test_load_var_lists(tvars_lists, expect): """ load_template_vars should parse template vars lists and dump these into template vars. """ result = load_template_vars(templatevars_lists=tvars_lists) assert result == expect def test_load_var_lists_warns_overwriting(caplog): """It wars user if same tvar name used twice.""" load_template_vars(template_vars=["BAZ='HEY'", "BAZ='THERE'"]) assert 'BAZ' in caplog.records[-1].message load_template_vars(templatevars_lists=['FOO=a,b', 'FOO=b,c']) assert 'FOO' in caplog.records[-1].message def test_load_var_lists_fails(): """ load_template_vars should fail if the same var is set using -s and -z """ errmsg = r"FOO=a and FOO=\['a', 'b'\]" with pytest.raises(InputError, match=errmsg): load_template_vars( template_vars=['FOO="a"'], templatevars_lists=['FOO=a,b'] ) def test_load_vars_invalid_value_pairs(): with pytest.raises(InputError, match="FOO\n.*qux\n.*BAZ\n.*QUX"): load_template_vars( template_vars=['FOO', 'qux'], templatevars_lists=['BAZ', 'QUX'] ) def test_load_template_vars_ValueError_fm_file(monkeypatch): from io import StringIO handle = StringIO('Hello') monkeypatch.setattr('builtins.open', lambda *_: handle) with pytest.raises(InputError, match='Hello'): load_template_vars(template_vars_file='foo') cylc-flow-8.6.4/tests/unit/conftest.py0000664000175000017500000001713315202510242020132 0ustar alastairalastair# 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 . """Standard pytest fixtures for unit tests.""" from pathlib import Path from typing import ( Any, Callable, Optional, Union, ) from unittest.mock import ( Mock, create_autospec, ) import pytest from cylc.flow.cycling.iso8601 import init as iso8601_init from cylc.flow.cycling.loader import ( INTEGER_CYCLING_TYPE, ISO8601_CYCLING_TYPE, ) from cylc.flow.data_store_mgr import DataStoreMgr from cylc.flow.install import ( link_runN, unlink_runN, ) from cylc.flow.scheduler import Scheduler from cylc.flow.workflow_files import WorkflowFiles from cylc.flow.xtrigger_mgr import XtriggerManager # Type alias for monkeymock() MonkeyMock = Callable[..., Mock] @pytest.fixture def monkeymock(monkeypatch: pytest.MonkeyPatch): """Fixture that patches a function/attr with a Mock and returns that Mock. Args: pypath: The Python-style import path to be patched. **kwargs: Any kwargs to set on the Mock. Example: mock_clean = monkeymock('cylc.flow.workflow_files.clean') something() # calls workflow_files.clean assert mock_clean.called is True """ def _monkeymock(pypath: str, **kwargs: Any) -> Mock: _mock = Mock(**kwargs) monkeypatch.setattr(pypath, _mock) return _mock return _monkeymock def _tmp_run_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """Fixture that patches the cylc-run dir to the tests's {tmp_path}/cylc-run, and optionally creates a workflow run dir inside. Adds the runN symlink automatically if the workflow ID ends with /run__. Args: id_: Workflow name. installed: If True, make it look like the workflow was installed using cylc install (creates _cylc-install dir). named: If True and installed is True, the _cylc-install dir will be created in the parent to make it look like this is a named run. Example: run_dir = tmp_run_dir('foo') # Or: cylc_run_dir = tmp_run_dir() """ cylc_run_dir = tmp_path / 'cylc-run' cylc_run_dir.mkdir(exist_ok=True) monkeypatch.setattr('cylc.flow.pathutil._CYLC_RUN_DIR', cylc_run_dir) def __tmp_run_dir( id_: Optional[str] = None, installed: bool = False, named: bool = False ) -> Path: if not id_: return cylc_run_dir run_dir = cylc_run_dir.joinpath(id_) run_dir.mkdir(parents=True, exist_ok=True) (run_dir / WorkflowFiles.FLOW_FILE).touch(exist_ok=True) (run_dir / WorkflowFiles.Service.DIRNAME).mkdir(exist_ok=True) if run_dir.name.startswith('run'): unlink_runN(run_dir.parent) link_runN(run_dir) if installed: if named: if len(Path(id_).parts) < 2: raise ValueError("Named run requires two-level id_") (run_dir.parent / WorkflowFiles.Install.DIRNAME).mkdir( exist_ok=True) else: (run_dir / WorkflowFiles.Install.DIRNAME).mkdir(exist_ok=True) return run_dir return __tmp_run_dir @pytest.fixture def tmp_run_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): return _tmp_run_dir(tmp_path, monkeypatch) @pytest.fixture(scope='module') def mod_tmp_run_dir(tmp_path_factory: pytest.TempPathFactory): """Module-scoped version of tmp_run_dir()""" tmp_path = tmp_path_factory.getbasetemp() with pytest.MonkeyPatch.context() as mp: yield _tmp_run_dir(tmp_path, mp) def _tmp_src_dir(tmp_path: Path): """Fixture that creates a temporary workflow source dir. (Actually the fixture is below, this is the re-usable meat of it.) Args: path: Path of source dir relative to cylc-src/. Example: src_dir = tmp_src_dir('foo') """ def __tmp_src_dir(path: Union[Path, str]) -> Path: cylc_src_dir = tmp_path / 'cylc-src' cylc_src_dir.mkdir(exist_ok=True) src_dir = cylc_src_dir / path src_dir.mkdir(parents=True) (src_dir / WorkflowFiles.FLOW_FILE).touch() return src_dir return __tmp_src_dir @pytest.fixture def tmp_src_dir(tmp_path: Path): # This is the actual tmp_src_dir fixture return _tmp_src_dir(tmp_path) @pytest.fixture(scope='module') def mod_tmp_src_dir(tmp_path_factory: pytest.TempPathFactory): """Module-scoped version of tmp_src_dir()""" tmp_path = tmp_path_factory.getbasetemp() return _tmp_src_dir(tmp_path) @pytest.fixture def set_cycling_type(monkeypatch: pytest.MonkeyPatch): """Initialize the Cylc cycling type. Args: ctype: The cycling type (integer or iso8601). time_zone: If using ISO8601/datetime cycling type, you can specify a custom time zone to use. dump_format: If using ISO8601, specify custom dump format. """ def _set_cycling_type( ctype: str = INTEGER_CYCLING_TYPE, time_zone: Optional[str] = 'Z', dump_format: Optional[str] = None, ) -> None: class _DefaultCycler: TYPE = ctype monkeypatch.setattr( 'cylc.flow.cycling.loader.DefaultCycler', _DefaultCycler ) if ctype == ISO8601_CYCLING_TYPE: monkeypatch.setattr( 'cylc.flow.cycling.iso8601.WorkflowSpecifics', iso8601_init( time_zone=time_zone, custom_dump_format=dump_format ), ) return _set_cycling_type @pytest.fixture def xtrigger_mgr() -> XtriggerManager: """A fixture to build an XtriggerManager which uses a mocked proc_pool, and uses a mocked broadcast_mgr.""" workflow_name = "sample_workflow" user = "john-foo" schd = create_autospec(Scheduler, workflow=workflow_name, owner=user) schd.proc_pool = Mock(put_command=lambda *a, **k: True) schd.workflow_db_mgr = Mock(housekeep=lambda *a, **k: True) schd.broadcast_mgr = Mock(put_broadcast=lambda *a, **k: True) schd.data_store_mgr = DataStoreMgr(schd) return XtriggerManager(schd) @pytest.fixture() def prevent_symlinking(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( 'cylc.flow.pathutil.make_symlink_dir', lambda *_, **__: {} ) def _tmp_flow_config(tmp_run_dir: Callable): """Create a temporary flow config file for use in init'ing WorkflowConfig. Args: id_: Workflow name. config: The flow file content. Returns the path to the flow file. """ def __tmp_flow_config(id_: str, config: str) -> 'Path': run_dir: 'Path' = tmp_run_dir(id_) flow_file = run_dir / WorkflowFiles.FLOW_FILE flow_file.write_text(config) return flow_file return __tmp_flow_config @pytest.fixture def tmp_flow_config(tmp_run_dir: Callable): return _tmp_flow_config(tmp_run_dir) @pytest.fixture(scope='module') def mod_tmp_flow_config(mod_tmp_run_dir: Callable): return _tmp_flow_config(mod_tmp_run_dir) cylc-flow-8.6.4/tests/unit/test_workflow_db_mgr.py0000664000175000017500000000535715202510242022535 0ustar alastairalastair# 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 . from pathlib import Path from typing import ( List, Set, ) from unittest.mock import Mock import pytest from pytest import param from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.flow_mgr import FlowNums from cylc.flow.id import Tokens from cylc.flow.task_proxy import TaskProxy from cylc.flow.taskdef import TaskDef from cylc.flow.util import serialise_set from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager @pytest.mark.parametrize('flow_nums, expected_removed', [ param(set(), {1, 2, 5}, id='all'), param({1}, {1}, id='subset'), param({1, 2, 5}, {1, 2, 5}, id='complete-set'), param({1, 3, 5}, {1, 5}, id='intersect'), param({3, 4}, set(), id='disjoint'), ]) def test_remove_task_from_flows( tmp_path: Path, flow_nums: FlowNums, expected_removed: FlowNums ): db_flows: List[FlowNums] = [ {1, 2}, {5}, set(), # FLOW_NONE ] expected_remaining = { serialise_set(flow - expected_removed) for flow in db_flows } db_mgr = WorkflowDatabaseManager(tmp_path) schd_tokens = Tokens('~asterix/gaul') tdef = TaskDef('a', rtcfg={}, start_point=None, initial_point=None) with db_mgr.get_pri_dao() as dao: db_mgr.pri_dao = dao db_mgr.pub_dao = Mock() for flow in db_flows: itask = TaskProxy( schd_tokens, tdef, IntegerPoint('1'), flow_nums=flow ) db_mgr.put_insert_task_states(itask) db_mgr.put_insert_task_outputs(itask) db_mgr.process_queued_ops() removed_fnums = db_mgr.remove_task_from_flows('1', 'a', flow_nums) assert removed_fnums == expected_removed db_mgr.process_queued_ops() for table in ('task_states', 'task_outputs'): remaining_fnums: Set[str] = { fnums_str for fnums_str, *_ in dao.connect().execute( f'SELECT flow_nums FROM {table}' ) } assert remaining_fnums == expected_remaining cylc-flow-8.6.4/tests/unit/test_scheduler_cli.py0000664000175000017500000002270515202510242022152 0ustar alastairalastair# 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 . from contextlib import contextmanager from secrets import token_hex import sqlite3 import pytest from cylc.flow.exceptions import HostSelectException, ServiceFileError from cylc.flow.scheduler_cli import ( RunOptions, _distribute, _version_check, ) from .conftest import MonkeyMock @pytest.fixture def stopped_workflow_db(tmp_path): """Returns a workflow DB with the `cylc_version` set to the provided string. def test_x(stopped_workflow_db): db_file = stopped_workflow_db(version) """ def _stopped_workflow_db(version): db_file = tmp_path / 'db' conn = sqlite3.connect(db_file) conn.execute(''' CREATE TABLE workflow_params(key TEXT, value TEXT, PRIMARY KEY(key)) ''') conn.execute( ''' INSERT INTO workflow_params VALUES (?, ?) ''', ('cylc_version', version) ) conn.commit() conn.close() return db_file return _stopped_workflow_db @pytest.fixture def set_cylc_version(monkeypatch): """Set the cylc.flow.__version__ attribute. def test_x(set_cylc_version): set_cylc_version('1.2.3') """ def _set_cylc_version(version): monkeypatch.setattr( 'cylc.flow.scheduler_cli.__version__', version, ) return _set_cylc_version @pytest.fixture def answer(monkeypatch): """Answer a `cylc play` CLI prompt. def test_x(answer): answer(users_response) It also adds an assert on the number of times the prompt interface was called. 0 if response is None, else 1. """ @contextmanager def _answer(response): calls = 0 def prompt(*args, **kwargs): nonlocal calls calls += 1 return response monkeypatch.setattr( 'cylc.flow.scheduler_cli.prompt', prompt, ) yield expected_calls = 1 if response is None: expected_calls = 0 assert calls == expected_calls return _answer @pytest.fixture def interactive(monkeypatch): monkeypatch.setattr( 'cylc.flow.scheduler_cli.is_terminal', lambda: True, ) @pytest.fixture def non_interactive(monkeypatch): monkeypatch.setattr( 'cylc.flow.scripts.reinstall.is_terminal', lambda: False, ) @pytest.mark.parametrize( 'before, after, downgrade, response, outcome', [ # no change ('8.0.0', '8.0.0', False, None, True), # upgrading ('8.0rc4.dev', '8.0.0', False, None, True), ('8.0.0', '8.0.1', False, None, True), ('8.0.0', '8.1.0', False, False, False), ('8.0.0', '8.1.0', False, True, True), ('8.0.0', '9.0.0', False, False, False), ('8.0.0', '9.0.0', False, True, True), # downgrading ('8.1.1', '8.1.0', False, None, False), ('8.1.1', '8.1.0', True, None, True), ('8.1.0', '8.0.0', False, None, False), ('8.1.0', '8.0.0', True, None, True), ('8.1.0', '8.0rc4.dev', True, None, True), ('9.1.0', '8.0.0', False, None, False), ('9.1.0', '8.0.0', True, None, True), # truncated versions ('8.1.1', '8', False, None, False), ('9.1.1', '8', True, None, True), ], ) def test_version_check_interactive( stopped_workflow_db, set_cylc_version, interactive, answer, before, after, response, downgrade, outcome, ): """It should check compatibility with the Cylc version of the prior run. When workflows are restarted we need to perform some checks to make sure it is safe and sensible to restart with this version of Cylc. Pytest Params: before: The Cylc version the workflow ran with previously. after: The version of Cylc being used to restart the workflow. downgrade: The --downgrade option of `cylc play`. response: The user's response the any CLI prompts. If `None` it will assert that no prompts were raised. outcome: The response of _version_check, True means safe to restart. """ db_file = stopped_workflow_db(before) set_cylc_version(after) with answer(response): assert ( _version_check( db_file, RunOptions(downgrade=downgrade) ) is outcome ) def test_version_check_interactive_upgrade( stopped_workflow_db, set_cylc_version, interactive, answer, ): """If a user interactively upgrades, it should set the upgrade option.""" db_file = stopped_workflow_db('8.0.0') set_cylc_version('8.1.0') opts = RunOptions() assert opts.upgrade is False with answer(True): assert _version_check(db_file, opts) is True assert opts.upgrade is True def test_version_check_non_interactive( stopped_workflow_db, set_cylc_version, non_interactive, ): """It should not prompt in non-interactive mode. * The --upgrade argument should permit upgrade. * The --downgrade argument should permit downgrade. """ # upgrade db_file = stopped_workflow_db('8.0.0') set_cylc_version('8.1.0') assert _version_check(db_file, RunOptions()) is False assert ( _version_check(db_file, RunOptions(upgrade=True)) is True ) # CLI --upgrade # downgrade db_file.unlink() db_file = stopped_workflow_db('8.1.0') set_cylc_version('8.0.0') assert _version_check(db_file, RunOptions()) is False assert ( _version_check(db_file, RunOptions(downgrade=True)) is True ) # CLI --downgrade def test_version_check_incompat(tmp_path): """It should fail for a corrupted or invalid database file.""" db_file = tmp_path / 'db' # invalid DB file db_file.touch() with pytest.raises(ServiceFileError): _version_check(db_file, RunOptions()) def test_version_check_no_db(tmp_path): """It should pass if there is no DB file (e.g. on workflow first start).""" db_file = tmp_path / 'db' # non-existent file assert _version_check(db_file, RunOptions()) @pytest.mark.parametrize( 'cli_colour, is_terminal, distribute_colour', [ ('never', True, '--color=never'), ('auto', True, '--color=always'), ('always', True, '--color=always'), ('never', False, '--color=never'), ('auto', False, '--color=never'), ('always', False, '--color=never'), ] ) def test_distribute_colour( monkeymock, cli_colour, is_terminal, distribute_colour, ): """It should start detached workflows with the correct --colour option. The is_terminal test will fail for detached scheduler processes which means that the colour formatting will be stripped for startup. This includes the Cylc header logo and any warnings/errors raised during config parsing. In order to preserver colour formatting we must set the `--colour` arg to `always` when we want the detached process to start in colour mode. See https://github.com/cylc/cylc-flow/issues/5159 """ _is_terminal = monkeymock('cylc.flow.scheduler_cli.is_terminal') _is_terminal.return_value = is_terminal _cylc_server_cmd = monkeymock('cylc.flow.scheduler_cli.cylc_server_cmd') _cylc_server_cmd.return_value = 0 opts = RunOptions(host='myhost', color=cli_colour) with pytest.raises(SystemExit) as excinfo: _distribute('foo', 'foo/run1', opts) assert excinfo.value.code == 0 assert distribute_colour in _cylc_server_cmd.call_args[0][0] def test_distribute_upgrade( monkeymock: MonkeyMock, monkeypatch: pytest.MonkeyPatch ): """It should start detached workflows with the --upgrade option if the user has interactively chosen to upgrade (typed 'y' at prompt). """ monkeypatch.setattr( 'sys.argv', ['cylc', 'play', 'foo'] # no upgrade option here ) _cylc_server_cmd = monkeymock('cylc.flow.scheduler_cli.cylc_server_cmd') _cylc_server_cmd.return_value = 0 opts = RunOptions( host='myhost', upgrade=True, # added by interactive upgrade ) with pytest.raises(SystemExit) as excinfo: _distribute('foo', 'foo/run1', opts) assert excinfo.value.code == 0 assert '--upgrade' in _cylc_server_cmd.call_args[0][0] def test_distribute_invalid_host( mock_glbl_cfg, caplog: pytest.LogCaptureFixture ): """It handles a socket error when the host is invalid.""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = non_exist_{token_hex(4)} ''' ) with pytest.raises(HostSelectException): _distribute('foo', 'foo/run1', RunOptions()) cylc-flow-8.6.4/tests/unit/cfgspec/0000775000175000017500000000000015202510242017340 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/cfgspec/test_workflow.py0000664000175000017500000000541515202510242022630 0ustar alastairalastair# 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 . import logging import pytest from typing import Any, Dict, Optional from cylc.flow import CYLC_LOG from cylc.flow.cfgspec.workflow import warn_about_depr_platform from cylc.flow.exceptions import PlatformLookupError @pytest.mark.parametrize( 'runtime_cfg, fail_expected, expected_warning', [ pytest.param( { 'foo': {'script': 'true'} }, False, None, id="No platform setting" ), pytest.param( { 'foo': {'platform': 'fine'} }, False, None, id="Valid platform" ), pytest.param( { 'foo': {'platform': 'fine'}, 'bar': {'platform': '`not good`'} }, True, None, id="Invalid subshell notation" ), pytest.param( { 'foo': {'platform': 'fine'}, 'bar': { 'platform': '$(fine)', 'job': {'batch system': 'pbs'} } }, True, None, id="Platform/host conflict" ), pytest.param( { 'foo': {'platform': 'fine'}, 'bar': { 'job': {'batch system': 'pbs'} } }, False, "please replace with [runtime][bar]platform", id="Deprecated settings" ) ] ) def test_warn_about_depr_platform( runtime_cfg: Dict[str, Any], fail_expected: bool, expected_warning: Optional[str], caplog: pytest.LogCaptureFixture): """Test warn_about_depr_platform()""" caplog.set_level(logging.WARNING, CYLC_LOG) cfg = {'runtime': runtime_cfg} if fail_expected: with pytest.raises(PlatformLookupError): warn_about_depr_platform(cfg) else: warn_about_depr_platform(cfg) if expected_warning: assert expected_warning in caplog.text else: assert caplog.record_tuples == [] cylc-flow-8.6.4/tests/unit/cfgspec/test_globalcfg.py0000664000175000017500000001455415202510242022702 0ustar alastairalastair# 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 . """Tests for the Cylc GlobalConfig object.""" from typing import TYPE_CHECKING, Callable import pytest from cylc.flow.cfgspec.globalcfg import GlobalConfig, SPEC from cylc.flow.parsec.exceptions import ValidationError from cylc.flow.parsec.validate import cylc_config_validate if TYPE_CHECKING: from pathlib import Path TEST_CONF = ''' [platforms] [[foo]] hosts = of_morgoth [platform groups] [[BAR]] platforms = mario, sonic [task events] # Checking that config items that aren't platforms or platform groups # are not output. ''' @pytest.fixture def mock_global_config(tmp_path: 'Path', monkeypatch: pytest.MonkeyPatch): """Create a mock GlobalConfig object, given the global.cylc contents as a string.""" def _mock_global_config(cfg: str) -> GlobalConfig: glblcfg = GlobalConfig(SPEC, validator=cylc_config_validate) conf_path = tmp_path / GlobalConfig.CONF_BASENAME conf_path.write_text(cfg) monkeypatch.setenv("CYLC_CONF_PATH", str(conf_path.parent)) glblcfg.loadcfg(conf_path) return glblcfg return _mock_global_config def test_dump_platform_names(capsys, mock_global_config): """It dumps lists of platform names, nothing else.""" glblcfg: GlobalConfig = mock_global_config(TEST_CONF) glblcfg.dump_platform_names(glblcfg) stdout, _ = capsys.readouterr() expected = 'localhost\nfoo\nBAR\n' assert stdout == expected def test_dump_platform_details(capsys, mock_global_config): """It dumps lists of platform spec.""" glblcfg: GlobalConfig = mock_global_config(TEST_CONF) glblcfg.dump_platform_details(glblcfg) out, _ = capsys.readouterr() expected = ( '[platforms]\n [[foo]]\n hosts = of_morgoth\n' '[platform groups]\n [[BAR]]\n platforms = mario, sonic\n' ) assert expected == out def test_expand_commas(tmp_path: 'Path', mock_global_config: Callable): """It should expand comma separated platform and install target definitions.""" glblcfg: GlobalConfig = mock_global_config(''' [install] [[symlink dirs]] [[[foo, bar]]] run = /x [[[foo]]] share = /y [[[bar]]] share = /z [platforms] [[foo]] [[[meta]]] x = 1 [["bar"]] # double quoted name [[[meta]]] x = 2 [[baz, bar, pub]] # baz before bar to test order is handled correctly [[[meta]]] x = 3 [['pub']] # single quoted name [[[meta]]] x = 4 ''') glblcfg._expand_commas() # ensure the definition order is preserved assert glblcfg.get(['platforms']).keys() == [ 'localhost', 'foo', 'bar', 'baz', 'pub', ] # ensure platform sections are correctly deep-merged assert glblcfg.get(['platforms', 'foo', 'meta', 'x']) == '1' assert glblcfg.get(['platforms', 'bar', 'meta', 'x']) == '3' assert glblcfg.get(['platforms', 'baz', 'meta', 'x']) == '3' assert glblcfg.get(['platforms', 'pub', 'meta', 'x']) == '4' # ensure install target sections are correctly merged: assert glblcfg.get(["install", "symlink dirs", "foo", "run"]) == "/x" assert glblcfg.get(["install", "symlink dirs", "foo", "share"]) == "/y" assert glblcfg.get(["install", "symlink dirs", "bar", "run"]) == "/x" assert glblcfg.get(["install", "symlink dirs", "bar", "share"]) == "/z" @pytest.mark.parametrize( 'src_dir, err_expected', [ pytest.param( '/theoden/rohan', False, id="Abs path ok" ), pytest.param( 'theoden/rohan', True, id="Rel path bad" ), pytest.param( '~theoden/rohan', False, id="Starts with usr - ok" ), pytest.param( '$THEODEN/rohan', False, id="Starts with env var - ok" ), pytest.param( 'rohan/$THEODEN', True, id="Rel path with env var not at start - bad" ), ] ) def test_source_dir_validation( src_dir: str, err_expected: bool, tmp_path: 'Path', mock_global_config: Callable ): glblcfg: GlobalConfig = mock_global_config(f''' [install] source dirs = /denethor/gondor, {src_dir} ''') if err_expected: with pytest.raises(ValidationError) as excinfo: glblcfg.load() assert "must be an absolute path" in str(excinfo.value) else: glblcfg.load() def test_platform_ssh_forward_variables(mock_global_config): glblcfg: GlobalConfig = mock_global_config(''' [platforms] [[foo]] ssh forward environment variables = "FOO", "BAR" ''') assert glblcfg.get( ['platforms', 'foo', 'ssh forward environment variables'] ) == ["FOO", "BAR"] def test_reload( mock_global_config, tmp_path: 'Path', monkeypatch: pytest.MonkeyPatch ): # Load a config glblcfg: GlobalConfig = mock_global_config(''' [platforms] [[foo]] [[[meta]]] x = 1 ''') # Update the global config file and reload conf_path = tmp_path / GlobalConfig.CONF_BASENAME conf_path.write_text(''' [platforms] [[foo]] [[[meta]]] x = 2 ''') glblcfg.load() # Mock the global config singleton monkeypatch.setattr(GlobalConfig, "get_inst", lambda *a, **k: glblcfg) assert glblcfg.get(['platforms', 'foo', 'meta', 'x']) == '2' from cylc.flow.platforms import get_platform platform = get_platform("foo") assert platform['meta']['x'] == "2" cylc-flow-8.6.4/tests/unit/jinja2_filters/0000775000175000017500000000000015202510242020633 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/jinja2_filters/test_duration_as.py0000664000175000017500000000246315202510242024561 0ustar alastairalastair# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. # Copyright (C) 2008-2019 NIWA # # 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 . from cylc.flow.jinja.filters.duration_as import duration_as import pytest @pytest.mark.parametrize( 'duration,fmt,result', [ pytest.param('PT1H', 's', 3600, id='PT1H->s'), pytest.param('PT1H', 'm', 60, id='PT1H->m'), pytest.param('PT1H', 'h', 1, id='PT1H->h'), pytest.param('PT1H', 'd', 1 / 24, id='PT1H->d'), pytest.param('PT1H', 'w', 1 / (24 * 7), id='PT1H->w'), pytest.param('P7D', 'd', 7, id='P7D->d'), pytest.param('P7D', 's', 604800, id='P7D->s'), ] ) def test_all(duration, fmt, result): assert duration_as(duration, fmt) == result cylc-flow-8.6.4/tests/unit/test_platforms.py0000664000175000017500000004346515202510242021362 0ustar alastairalastair# 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 . # # Tests for the platform lookup. from typing import ( Any, Dict, List, Optional, ) import pytest from cylc.flow.exceptions import ( GlobalConfigError, PlatformLookupError, ) from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults from cylc.flow.platforms import ( _platform_name_from_job_info, _validate_single_host, generic_items_match, get_install_target_from_platform, get_install_target_to_platforms_map, get_platform, get_platform_deprecated_settings, is_platform_definition_subshell, platform_from_name, ) from cylc.flow.run_modes import JOBLESS_MODES PLATFORMS = { 'desktop[0-9]{2}|laptop[0-9]{2}': { 'job runner': 'background' }, 'sugar': { 'hosts': 'localhost', 'job runner': 'slurm', }, 'hpc-no-logs': { 'hosts': ['hpc1', 'hpc2'], 'job runner': 'pbs', 'retrieve job logs': False }, 'hpc-logs': { 'hosts': ['hpc1', 'hpc2'], 'job runner': 'pbs', 'retrieve job logs': True }, 'hpc1-bg': { 'hosts': 'hpc1', 'job runner': 'background', }, 'hpc2-bg': { 'hosts': 'hpc2', 'job runner': 'background' }, 'localhost': { 'hosts': 'localhost', 'job runner': 'background' } } PLATFORMS_NO_UNIQUE = { 'sugar': { 'hosts': 'localhost', 'job runner': 'slurm' }, 'pepper': { 'hosts': ['hpc1', 'hpc2'], 'job runner': 'slurm' }, } PLATFORMS_WITH_RE = { 'hpc.*': {'hosts': 'hpc1', 'job runner': 'background'}, 'h.*': {'hosts': 'hpc3'}, r'vld\d{2,3}, anselm\d{4}': {}, 'nu.*': { 'job runner': 'slurm', 'hosts': ['localhost'] }, 'localhost': { 'hosts': 'localhost', 'job runner': 'background' } } PLATFORMS_TREK = { 'enterprise': { 'hosts': ['kirk', 'picard'], 'install target': 'picard', 'name': 'enterprise' }, 'voyager': { 'hosts': ['janeway'], 'install target': 'janeway', 'name': 'voyager' }, 'stargazer': { 'hosts': ['picard'], 'install target': 'picard', 'name': 'stargazer' } } PLATFORMS_INVALID = { 'enterprise': { 'hosts': ['kirk', 'picard'], 'install target': 'picard', 'job runner': 'background' # requires one host }, 'voyager': { 'hosts': ['janeway', 'seven-of-nine'], 'install target': 'janeway', 'job runner': 'at' # requires one host } } # ---------------------------------------------------------------------------- # Tests of platform_from_name # ---------------------------------------------------------------------------- @pytest.mark.parametrize( "PLATFORMS, platform, expected", [ (PLATFORMS_WITH_RE, "nutmeg", { "job runner": "slurm", "name": "nutmeg", "hosts": ['localhost'] }), (PLATFORMS_WITH_RE, "vld798", ["vld798"]), (PLATFORMS_WITH_RE, "vld56", ["vld56"]), ( PLATFORMS_NO_UNIQUE, "sugar", { "hosts": "localhost", "job runner": "slurm", "name": "sugar" }, ), ( PLATFORMS, None, { "hosts": "localhost", "name": "localhost", "job runner": "background" }, ), (PLATFORMS, "laptop22", { "job runner": "background", "name": "laptop22", "hosts": ["laptop22"] }), ( PLATFORMS, "hpc1-bg", { "hosts": "hpc1", "job runner": "background", "name": "hpc1-bg" }, ), (PLATFORMS_WITH_RE, "hpc2", {"hosts": "hpc3", "name": "hpc2"}), ], ) def test_basic(PLATFORMS, platform, expected): # n.b. The name field of the platform is set in the Globalconfig object # if the name is 'localhost', so we don't test for it here. platform = platform_from_name(platform_name=platform, platforms=PLATFORMS) if isinstance(expected, dict): assert platform == expected else: assert platform["hosts"] == expected def test_platform_not_there(): with pytest.raises(PlatformLookupError): platform_from_name('moooo', PLATFORMS) @pytest.mark.parametrize( 'platform', [ {i: j} for i, j in PLATFORMS_INVALID.items() ] ) def test_invalid_platforms(platform): with pytest.raises(GlobalConfigError): _validate_single_host(platform) def test_similar_but_not_exact_match(): with pytest.raises(PlatformLookupError): platform_from_name('vld1', PLATFORMS_WITH_RE) # ---------------------------------------------------------------------------- # Tests of platform_name_from_job_info # ---------------------------------------------------------------------------- # Basic tests that we can select sensible platforms @pytest.mark.parametrize( 'job, remote, returns', [ # Can we return a sensible platform for desktop 42 ( {}, {'host': 'desktop42'}, 'desktop42' ), # Basic test where the user hasn't submitted anything and the task # returns to default, i.e. localhost. ( {'batch system': 'background'}, {'retrieve job logs retry delays': None}, 'localhost' ), # Check that when the user asks for batch system = slurm alone # they get system = sugar ( {'batch system': 'slurm'}, {'host': ''}, 'sugar' ), # Check that when users asks for hpc1 and pbs they get a platform # with hpc1 in its list of hosts ( {'batch system': 'pbs'}, {'host': 'hpc1'}, 'hpc-logs' ), # When the user asks for hpc1 without specifying pbs user gets platform # hpc bg1 ( {'batch system': 'background'}, {'host': 'hpc1'}, 'hpc1-bg' ), # Check that None as a value is handled correctly ( {'batch system': None}, {'host': 'hpc1-bg'}, 'hpc1-bg' ), ( # Check that failure to set any items will return localhost {'batch system': None}, {'host': None}, 'localhost' ), ( # Check that all generic items are matched {'batch system': 'pbs'}, {'host': 'hpc1', 'retrieve job logs': False}, 'hpc-no-logs' ), ] ) def test_platform_name_from_job_info_basic(job, remote, returns): assert _platform_name_from_job_info(PLATFORMS, job, remote) == returns def test_platform_name_from_job_info_evaluated_hostname(): result = _platform_name_from_job_info( PLATFORMS, {'batch system': 'background'}, {'host': '$(cat tiddles)'}, evaluated_host='hpc2', ) assert result == 'hpc2-bg' def test_platform_name_from_job_info_ordered_dict_comparison(): """Check that we are only comparing set items in OrderedDictWithDefaults. """ job = {'batch system': 'background', 'Made up key': 'Zaphod'} remote = {'host': 'hpc1', 'Made up key': 'Arthur'} # Set up a fake OrderedDictWith a fake unset default. platform = OrderedDictWithDefaults() platform.defaults_ = {k: None for k in PLATFORMS['hpc1-bg'].keys()} platform.defaults_['Made up key'] = {} platform.update(PLATFORMS['hpc1-bg']) platforms = {'hpc1-bg': platform, 'dobbie': PLATFORMS['sugar']} assert _platform_name_from_job_info(platforms, job, remote) == 'hpc1-bg' # Cases where the error ought to be raised because no matching platform should # be found. @pytest.mark.parametrize( 'job, remote', [ # Check for error when the user asks for slurm on host desktop01 ( {'batch system': 'slurm'}, {'host': 'desktop01'}, ), # ('hpc1', 'slurm', 'error'), ( {'batch system': 'slurm'}, {'host': 'hpc1'}, ), # Localhost doesn't support pbs ( {'batch system': 'pbs'}, {}, ), ] ) def test_reverse_PlatformLookupError(job, remote): with pytest.raises(PlatformLookupError): _platform_name_from_job_info(PLATFORMS, job, remote) # An example of a global config with two Spice systems available @pytest.mark.parametrize( 'job, remote, returns', [ ( {'batch system': 'slurm'}, {'host': 'sugar'}, 'sugar' ), ( {'batch system': 'slurm'}, {}, 'sugar' ), ( {'batch system': 'slurm'}, {'host': 'pepper'}, 'pepper' ) ] ) def test_platform_name_from_job_info_two_spices( job, remote, returns ): platforms = { 'sugar': { 'hosts': ['sugar', 'localhost'], 'job runner': 'slurm', }, 'pepper': { 'job runner': 'slurm', 'hosts': 'pepper' }, } assert _platform_name_from_job_info(platforms, job, remote) == returns # An example of two platforms with the same hosts and job runner settings # but some other setting different @pytest.mark.parametrize( 'job, remote, returns', [ ( { 'job runner': 'background', 'job runner command template': '', 'shell': '/bin/fish' }, { 'host': 'desktop01', 'owner': '', 'workflow definition directory': '', 'retrieve job logs': '', 'retrieve job logs max size': '', 'retrieve job logs retry delays': 'None' }, 'my-platform-with-fish' ), ] ) def test_platform_name_from_job_info_similar_platforms( job, remote, returns ): platforms = { 'my-platform-with-bash': { 'hosts': 'desktop01', 'shell': '/bin/bash', 'job runner': 'background' }, # An extra platform to check that we only pick up the first match 'my-platform-with-fish-not-this-one': { 'hosts': 'desktop01', 'shell': '/bin/fish', 'job runner': 'background' }, 'my-platform-with-fish': { 'hosts': 'desktop01', 'shell': '/bin/fish', 'job runner': 'background' }, } assert _platform_name_from_job_info(platforms, job, remote) == returns # ----------------------------------------------------------------------------- # Tests for getting install target info @pytest.mark.parametrize( 'platform, expected', [ ({'name': 'rick', 'install target': 'desktop'}, 'desktop'), ({'name': 'morty', 'install target': ''}, 'morty') ] ) def test_get_install_target_from_platform(platform, expected): """Test that get_install_target_from_platform works as expected.""" assert get_install_target_from_platform(platform) == expected @pytest.mark.parametrize( 'platform_names, expected_map', [ ( ['enterprise', 'stargazer'], { 'picard': [ PLATFORMS_TREK['enterprise'], PLATFORMS_TREK['stargazer'] ] }, ), ( ['enterprise', 'voyager', 'enterprise'], { 'picard': [ PLATFORMS_TREK['enterprise'] ], 'janeway': [ PLATFORMS_TREK['voyager'] ] }, ), ( ['enterprise', 'starkiller'], # ignores non-existent platform { 'picard': [ PLATFORMS_TREK['enterprise'] ], }, ), ] ) def test_get_install_target_to_platforms_map( platform_names: list[str], expected_map: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ): """Test that get_install_target_to_platforms_map works as expected.""" monkeypatch.setattr('cylc.flow.platforms.platform_from_name', lambda x: platform_from_name(x, PLATFORMS_TREK)) result = get_install_target_to_platforms_map(platform_names) # Sort the maps: for _map in (result, expected_map): for install_target in _map: _map[install_target] = sorted( _map[install_target], key=lambda k: k['name'] ) assert result == expected_map @pytest.mark.parametrize('mode', sorted(JOBLESS_MODES)) def test_platform_from_name__jobless_modes(mode): result = platform_from_name(mode) assert result['name'] == 'localhost' @pytest.mark.parametrize('mode', sorted(JOBLESS_MODES)) def test_get_install_target_to_platforms_map__jobless_modes(mode): result = get_install_target_to_platforms_map([mode]) assert list(result) == ['localhost'] assert len(result['localhost']) == 1 assert result['localhost'][0]['hosts'] == ['localhost'] assert result['localhost'][0]['install target'] == 'localhost' @pytest.mark.parametrize( 'platform, job, remote, expect', [ ( # Default, no old settings. {'ship': 'Enterprise'}, {}, {}, True ), ( {'captain': 'Kirk'}, {'captain': 'Picard'}, {}, False ), ( {'captain': 'Sisko'}, {}, {'captain': 'Janeway'}, False ), ( {'captain': 'Picard', 'ship': 'Enterprise'}, {'captain': 'Picard'}, {'ship': 'Enterprise'}, True ), ( {'captain': 'Picard', 'ship': 'Enterprise'}, {'captain': 'Picard'}, {'ship': 'Defiant'}, False ), ( {'captain': 'Picard', 'ship': 'Enterprise'}, {'captain': 'Picard'}, {}, True ) ] ) def test_generic_items_match(platform, job, remote, expect): assert generic_items_match(platform, job, remote) == expect @pytest.mark.parametrize( 'task_conf, expected', [ pytest.param( { 'remote': { 'host': 'cylcdevbox', 'retrieve job logs': True }, 'job': { 'batch system': 'pbs', 'batch submit command template': 'meow' } }, [ '[runtime][task][job]batch submit command template = meow', '[runtime][task][remote]retrieve job logs = True', '[runtime][task][remote]host = cylcdevbox', '[runtime][task][job]batch system = pbs' ], id="All are deprecated settings" ), pytest.param( { 'remote': {'host': 'localhost'}, 'job': { 'batch system': 'pbs', 'batch submit command template': None } }, ['[runtime][task][job]batch system = pbs'], id="Exclusions are excluded" ), pytest.param( { 'environment filter': { 'include': ['frodo', 'sam'] } }, [], id="No deprecated settings" ) ] ) def test_get_platform_deprecated_settings( task_conf: Dict[str, Any], expected: List[str] ): output = get_platform_deprecated_settings(task_conf, task_name='task') assert set(output) == set(expected) @pytest.mark.parametrize( 'plat_val, expected, err_msg', [('normal', False, None), ('$(yes)', True, None), ('`echo ${chamber}`', None, "backticks are not supported")] ) def test_is_platform_definition_subshell( plat_val: str, expected: Optional[bool], err_msg: Optional[str]): if err_msg: with pytest.raises(PlatformLookupError) as exc: is_platform_definition_subshell(plat_val) assert err_msg in str(exc.value) else: assert is_platform_definition_subshell(plat_val) is expected def test_get_platform_from_OrderedDictWithDefaults(mock_glbl_cfg): """Get platform works with OrderedDictWithDefaults. Most tests use dictionaries to check platforms functionality. This one was added to catch an issue where the behaviour of dict.get != OrderedDictWithDefaults.get. See - https://github.com/cylc/cylc-flow/issues/4979 """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[skarloey]] hosts = foo, bar job runner = slurm ''' ) task_conf = OrderedDictWithDefaults() task_conf.defaults_ = OrderedDictWithDefaults([ ('job', OrderedDictWithDefaults([ ('batch system', 'slurm') ])), ('remote', OrderedDictWithDefaults([ ('host', 'foo') ])), ]) result = get_platform(task_conf)['name'] assert result == 'skarloey' cylc-flow-8.6.4/tests/unit/test_job_runner_mgr.py0000664000175000017500000000551215202510242022352 0ustar alastairalastair# 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 . from cylc.flow.job_runner_mgr import ( JobRunnerManager, JOB_FILES_REMOVED_MESSAGE) jrm = JobRunnerManager() SAMPLE_STATUS = """ ignore me, I have no = sign CYLC_JOB_RUNNER_NAME=pbs CYLC_JOB_ID=2361713 CYLC_JOB_RUNNER_SUBMIT_TIME=2025-01-28T14:46:04Z CYLC_JOB_PID=2361713 CYLC_JOB_INIT_TIME=2025-01-28T14:46:05Z CYLC_MESSAGE=2025-01-28T14:46:05Z|INFO|sleep 31 CYLC_JOB_RUNNER_EXIT_POLLED=2025-01-28T14:46:08Z CYLC_JOB_EXIT=SUCCEEDED CYLC_JOB_EXIT_TIME=2025-01-28T14:46:38Z """ def test__job_poll_status_files(tmp_path): """Good Path: A valid job.status files exists""" (tmp_path / 'sub').mkdir() (tmp_path / 'sub' / 'job.status').write_text(SAMPLE_STATUS) ctx = jrm._jobs_poll_status_files(str(tmp_path), 'sub') assert ctx.job_runner_name == 'pbs' assert ctx.job_id == '2361713' assert ctx.job_runner_exit_polled == 1 assert ctx.pid == '2361713' assert ctx.time_submit_exit == '2025-01-28T14:46:04Z' assert ctx.time_run == '2025-01-28T14:46:05Z' assert ctx.time_run_exit == '2025-01-28T14:46:38Z' assert ctx.run_status == 0 assert ctx.messages == ['2025-01-28T14:46:05Z|INFO|sleep 31'] def test__job_poll_status_files_task_failed(tmp_path): """Good Path: A valid job.status files exists""" (tmp_path / 'sub').mkdir() (tmp_path / 'sub' / 'job.status').write_text("CYLC_JOB_EXIT=FOO") ctx = jrm._jobs_poll_status_files(str(tmp_path), 'sub') assert ctx.run_status == 1 assert ctx.run_signal == 'FOO' def test__job_poll_status_files_deleted_logdir(): """The log dir has been deleted whilst the task is still active. Return the context with the message that the task has failed. """ ctx = jrm._jobs_poll_status_files('foo', 'bar') assert ctx.run_signal == JOB_FILES_REMOVED_MESSAGE assert ctx.run_status == 1 assert ctx.job_runner_exit_polled == 1 def test__job_poll_status_files_ioerror(tmp_path, capsys): """There is no readable file. """ (tmp_path / 'sub').mkdir() jrm._jobs_poll_status_files(str(tmp_path), 'sub') cap = capsys.readouterr() assert '[Errno 2] No such file or directory' in cap.err cylc-flow-8.6.4/tests/unit/test_param_expand.py0000664000175000017500000004107515202510242022005 0ustar alastairalastair# 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 . import unittest import pytest from pytest import param from cylc.flow.exceptions import ParamExpandError from cylc.flow.param_expand import NameExpander, GraphExpander class TestParamExpand(unittest.TestCase): """Unit tests for the parameter expansion module.""" def setUp(self): """Create some parameters and templates for use in tests.""" params_map = {'a': [-3, -1], 'i': [0, 1], 'j': [0, 1, 2], 'k': [0, 1]} # k has template is deliberately bad templates = { 'a': '_a%(a)d', 'i': '_i%(i)d', 'j': '_j%(j)d', 'k': '_k%(z)d'} self.name_expander = NameExpander((params_map, templates)) self.graph_expander = GraphExpander((params_map, templates)) def test_name_one_param(self): """Test name expansion and returned value for a single parameter.""" self.assertEqual( self.name_expander.expand('foo'), [('foo_j0', {'j': 0}), ('foo_j1', {'j': 1}), ('foo_j2', {'j': 2})] ) def test_name_two_params(self): """Test name expansion and returned values for two parameters.""" self.assertEqual( self.name_expander.expand('foo'), [('foo_i0_j0', {'i': 0, 'j': 0}), ('foo_i0_j1', {'i': 0, 'j': 1}), ('foo_i0_j2', {'i': 0, 'j': 2}), ('foo_i1_j0', {'i': 1, 'j': 0}), ('foo_i1_j1', {'i': 1, 'j': 1}), ('foo_i1_j2', {'i': 1, 'j': 2})] ) def test_name_two_names(self): """Test name expansion for two names.""" self.assertEqual( self.name_expander.expand('foo, bar'), [('foo_i0', {'i': 0}), ('foo_i1', {'i': 1}), ('bar_j0', {'j': 0}), ('bar_j1', {'j': 1}), ('bar_j2', {'j': 2})] ) def test_name_specific_val_1(self): """Test singling out a specific value, in name expansion.""" self.assertEqual( self.name_expander.expand('foo'), [('foo_i0', {'i': 0})] ) def test_name_specific_val_2(self): """Test specific value in the first parameter of a pair.""" self.assertEqual( self.name_expander.expand('foo'), [('foo_i0_j0', {'i': 0, 'j': 0}), ('foo_i0_j1', {'i': 0, 'j': 1}), ('foo_i0_j2', {'i': 0, 'j': 2})] ) def test_name_specific_val_3(self): """Test specific value in the second parameter of a pair.""" self.assertEqual( self.name_expander.expand('foo'), [('foo_i0_j1', {'i': 0, 'j': 1}), ('foo_i1_j1', {'i': 1, 'j': 1})] ) def test_name_fail_bare_value(self): """Test foo<0,j> fails.""" # It should be foo. self.assertRaises(ParamExpandError, self.name_expander.expand, 'foo<0,j>') def test_name_fail_undefined_param(self): """Test that an undefined parameter gets failed.""" # m is not defined. self.assertRaises(ParamExpandError, self.name_expander.expand, 'foo') def test_name_fail_param_value_too_high(self): """Test that an out-of-range parameter gets failed.""" # i stops at 3. self.assertRaises(ParamExpandError, self.name_expander.expand, 'foo') def test_name_multiple(self): """Test expansion of two names, with one and two parameters.""" self.assertEqual( self.name_expander.expand('foo, bar'), [('foo_i0', {'i': 0}), ('foo_i1', {'i': 1}), ('bar_i0_j0', {'i': 0, 'j': 0}), ('bar_i0_j1', {'i': 0, 'j': 1}), ('bar_i0_j2', {'i': 0, 'j': 2}), ('bar_i1_j0', {'i': 1, 'j': 0}), ('bar_i1_j1', {'i': 1, 'j': 1}), ('bar_i1_j2', {'i': 1, 'j': 2})] ) def test_graph_expand_1(self): """Test graph expansion with two parameters each side of an arrow.""" self.assertEqual( self.graph_expander.expand("bar=>baz"), set(["bar_i0_j1=>baz_i0_j1", "bar_i1_j2=>baz_i1_j2", "bar_i0_j2=>baz_i0_j2", "bar_i1_j1=>baz_i1_j1", "bar_i1_j0=>baz_i1_j0", "bar_i0_j0=>baz_i0_j0"]) ) def test_graph_expand_2(self): """Test graph expansion to 'branch and merge' a workflow.""" self.assertEqual( self.graph_expander.expand("pre=>bar=>baz=>post"), set(["pre=>bar_i0=>baz_i0_j1=>post", "pre=>bar_i1=>baz_i1_j2=>post", "pre=>bar_i0=>baz_i0_j2=>post", "pre=>bar_i1=>baz_i1_j1=>post", "pre=>bar_i1=>baz_i1_j0=>post", "pre=>bar_i0=>baz_i0_j0=>post"]) ) def test_graph_expand_3(self): """Test graph expansion -ve integers.""" self.assertEqual( self.graph_expander.expand("bar"), set(["bar_a-1", "bar_a-3"])) def test_graph_expand_offset_1(self): """Test graph expansion with a -ve offset.""" self.assertEqual( self.graph_expander.expand("bar=>baz"), set(["bar_i-32768_j0=>baz_i0_j0", "bar_i-32768_j1=>baz_i0_j1", "bar_i-32768_j2=>baz_i0_j2", "bar_i0_j0=>baz_i1_j0", "bar_i0_j1=>baz_i1_j1", "bar_i0_j2=>baz_i1_j2"]) ) def test_graph_expand_offset_2(self): """Test graph expansion with a +ve offset.""" self.assertEqual( self.graph_expander.expand("baz=>baz"), set(["baz_i0=>baz_i1", "baz_i1=>baz_i-32768"]) ) def test_graph_expand_specific(self): """Test graph expansion with a specific value.""" self.assertEqual( self.graph_expander.expand("bar=>baz"), set(["bar_i1_j0=>baz_i0_j0", "bar_i1_j1=>baz_i0_j1", "bar_i1_j2=>baz_i0_j2", "bar_i1_j0=>baz_i1_j0", "bar_i1_j1=>baz_i1_j1", "bar_i1_j2=>baz_i1_j2"]) ) def test_graph_fail_bare_value(self): """Test that a bare parameter value fails in the graph.""" self.assertRaises(ParamExpandError, self.graph_expander.expand, 'foo<0,j>=>bar') def test_graph_fail_undefined_param(self): """Test that an undefined parameter value fails in the graph.""" self.assertRaises(ParamExpandError, self.graph_expander.expand, 'foo=>bar') def test_graph_fail_param_value_too_high(self): """Test that an out-of-range parameter value fails in the graph.""" self.assertRaises(ParamExpandError, self.graph_expander.expand, 'foo') def test_template_fail_missing_param(self): """Test a template string specifying a non-existent parameter.""" self.assertRaises( ParamExpandError, self.name_expander.expand, 'foo') self.assertRaises( ParamExpandError, self.graph_expander.expand, 'foo') @staticmethod def _param_expand_params(): """Test data for test_parameter_graph_mixing_offset_and_conditional. params_map, templates, expanded_str, expanded_values params_map : map of parameters used in the graph expression templates : parameters template expanded_str : graph string, using params/template expanded_values: values expected to exist after params expanded """ return ( # original case from #2608 ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "foo & baz => foo", [ 'foo_-32768 & baz => foo_cat', 'foo_cat & baz => foo_dog' ] ), # cases from comments from #2608 # see cylc/cylc-flow/pull/3452#issuecomment-670782800 ( # single element, so bar does not exist {'m': ["cat"]}, {'m': '_%(m)s'}, "foo & bar & baz => qux", [ "foo & bar_-32768 & baz => qux" ] ), # cases from comments from #2608 # see cylc/cylc-flow/pull/3452#issuecomment-670776749 ( {'m': ["1", "2"]}, {'m': '_%(m)s'}, "foo => bar => baz", [ "foo_-32768 => bar_1 => baz", "foo_1 => bar_2 => baz" ] ), # cases from comments from #2608 # see cylc/cylc-flow/pull/3452#discussion_r430967867 ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "baz & foo & pub => foo", [ "baz & foo_-32768 & pub => foo_cat", "baz & foo_cat & pub => foo_dog" ] ), ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "bar & foo & pub & qux => foo", [ "bar & foo_-32768 & pub_-32768 & qux => foo_cat", "bar & foo_cat & pub_cat & qux => foo_dog" ] ), # GraphParser strips spaces! ( {'m': ["cat"]}, {'m': '_%(m)s'}, "foo&bar&baz=>qux", [ "foo&bar_-32768&baz=>qux" ] ), ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "foo&bar&baz=>qux", [ "foo&bar_-32768&baz=>qux", "foo&bar_cat&baz=>qux" ] ), # must support & and | in graph expressions ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "foo|bar|baz=>qux", [ "foo|bar_-32768|baz=>qux", "foo|bar_cat|baz=>qux" ] ), ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "foo&bar|baz=>qux", [ "foo&bar_-32768|baz=>qux", "foo&bar_cat|baz=>qux" ] ), ( {'m': ["cat", "dog"]}, {'m': '_%(m)s'}, "foo&bar|baz=>qux", [ "foo&bar_-32768|baz=>qux", "foo&bar_cat|baz=>qux" ] ), ( {'m': ["cat"]}, {'m': '_%(m)s'}, "foo => bar => baz", [ "foo=>bar_-32768=>baz" ] ) ) def test_parameter_graph_mixing_offset_and_conditional(self): """Test for bug reported in issue #2608 on GitHub.""" for test_case in self._param_expand_params(): params_map, templates, expanded_str, expanded_values = \ test_case graph_expander = GraphExpander((params_map, templates)) # Ignore white spaces. expanded = [expanded.replace(' ', '') for expanded in graph_expander.expand(expanded_str)] self.assertEqual( len(expanded_values), len(expanded), f"Invalid length for expected {expanded_values} and " f"{expanded}") # When testing, we don't really care for white spaces,as they # are removed in the GraphParser anyway. That's why we have # ''.replace(' ', ''). for expected in expanded_values: self.assertTrue( expected.replace(' ', '') in expanded, f"Expected value {expected.replace(' ', '')} " f"not in {expanded}") class myParam(): def __init__( self, raw_str, parameter_values=None, templates=None, raises=None, id_=None, expect=None, ): """Ease of reading wrapper for pytest.param Args: expect: Output of expand_parent_params() raw_str: The parent_params input string. parameter_values """ parameter_values = parameter_values if parameter_values else {} templates = templates if templates else {} self.raises = raises self.expect = expect self.raw_str = raw_str self.parameter_values = parameter_values self.templates = templates self.parameters = ((parameter_values, templates)) self.name_expander = NameExpander(self.parameters) self.id_ = 'raises:' + id_ if raises else id_ def get(self): return param(self, id=self.id_) @pytest.mark.parametrize( "param", ( myParam( expect=(None, 'no_params_here'), raw_str='no_params_here', id_='basic' ).get(), myParam( expect=({'bar': 1}, 'bar1'), raw_str='', parameter_values={'bar': 1}, templates={'bar': 'bar%(bar)s'}, id_='one-valid_-param' ).get(), myParam( expect=({'bar': 1}, 'foo_bar1_baz'), raw_str='foobaz', parameter_values={'bar': 1}, templates={'bar': '_bar%(bar)s_'}, id_='one-valid_-param' ).get(), myParam( raw_str='foobaz', parameter_values={'qux': 2}, templates={'bar': '_bar%(bar)s_'}, raises=(ParamExpandError, 'parameter \'bar\' undefined'), id_='one-invalid_-param' ).get(), myParam( expect=({'bar': 1, 'baz': 42}, 'foo_bar1_baz42'), raw_str='foo', parameter_values={'bar': 1, 'baz': 42}, templates={'bar': '_bar%(bar)s', 'baz': '_baz%(baz)s'}, id_='two-valid_-param' ).get(), myParam( expect=({'bar': 1, 'baz': 42}, 'foo_bar1qux_baz42'), raw_str='fooqux', parameter_values={'bar': 1, 'baz': 42}, templates={'bar': '_bar%(bar)s', 'baz': '_baz%(baz)s'}, id_='two-valid_-param-sep-brackets', ).get(), myParam( raw_str='foobaz', raises=(ParamExpandError, '^parameter offsets illegal here'), id_='offsets-illegal' ).get(), myParam( expect=({'bar': 1}, 'foo_bar1_baz'), raw_str='foobaz', parameter_values={'bar': [1, 2]}, templates={'bar': '_bar%(bar)s_'}, id_='value-set' ).get(), myParam( raw_str='foobaz', parameter_values={'bar': [1, 2]}, raises=(ParamExpandError, '^illegal'), id_='illegal-value' ).get(), myParam( expect=({'bar': 1}, 'foo_bar1_baz'), raw_str='foobaz', raises=(ParamExpandError, '^parameter \'bar\' undefined'), id_='parameter-undefined' ).get(), ) ) def test_expand_parent_params(param): if not param.raises: # Good Path tests: result = param.name_expander.expand_parent_params( param.raw_str, param.parameter_values, 'Errortext') assert result == param.expect else: # Bad path tests: with pytest.raises(param.raises[0], match=param.raises[1]): param.name_expander.expand_parent_params( param.raw_str, param.parameter_values, 'Errortext') cylc-flow-8.6.4/tests/unit/test_loggingutil.py0000664000175000017500000002231715202510242021670 0ustar alastairalastair# 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 . import logging import re import sys from io import TextIOWrapper from pathlib import Path from time import sleep from typing import Callable, cast from unittest import mock import pytest from pytest import param from cylc.flow import LOG from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.cfgspec.globalcfg import GlobalConfig from cylc.flow.loggingutil import ( CylcLogFormatter, RotatingLogFileHandler, get_reload_start_number, get_sorted_logs_by_time, patch_log_level, set_timestamps, ) @pytest.fixture def rotating_log_file_handler(tmp_path: Path): """Fixture to create a RotatingLogFileHandler for testing.""" log_file = tmp_path / "log" log_file.touch() handler = cast('RotatingLogFileHandler', None) orig_stream = cast('TextIOWrapper', None) def inner( *args, level: int = logging.INFO, **kwargs ) -> RotatingLogFileHandler: nonlocal handler, orig_stream handler = RotatingLogFileHandler(log_file, *args, **kwargs) orig_stream = handler.stream # next line is important as pytest can have a "Bad file descriptor" # due to a FileHandler with default "a" (pytest tries to r/w). handler.mode = "a+" # enable the logger LOG.setLevel(level) LOG.addHandler(handler) return handler yield inner # clean up LOG.setLevel(logging.INFO) LOG.removeHandler(handler) @mock.patch("cylc.flow.loggingutil.glbl_cfg") def test_value_error_raises_system_exit( mocked_glbl_cfg, rotating_log_file_handler ): """Test that a ValueError when writing to a log stream won't result in multiple exceptions (what could lead to infinite loop in some occasions. Instead, it **must** raise a SystemExit""" # mock objects used when creating the file handler mocked = mock.MagicMock() mocked_glbl_cfg.return_value = mocked mocked.get.return_value = 100 file_handler = rotating_log_file_handler(level=logging.INFO) # Disable raising uncaught exceptions in logging, due to file # handler using stdin.fileno. See the following links for more. # https://github.com/pytest-dev/pytest/issues/2276 & # https://github.com/pytest-dev/pytest/issues/1585 logging.raiseExceptions = False # first message will initialize the stream and the handler LOG.info("What could go") # here we change the stream of the handler file_handler.stream = mock.MagicMock() file_handler.stream.seek = mock.MagicMock() # in case where file_handler.stream.seek.side_effect = ValueError with pytest.raises(SystemExit): # next call will call the emit method and use the mocked stream LOG.info("wrong?!") # clean up logging.raiseExceptions = True @pytest.mark.parametrize( 'dev_info, expect', [ param( True, ( '%(asctime)s %(levelname)-2s - [%(module)s:%(lineno)d] - ' '%(message)s' ), id='dev_info=True' ), param( False, '%(asctime)s %(levelname)-2s - %(message)s', id='dev_info=False' ) ] ) def test_CylcLogFormatter__init__dev_info(dev_info, expect): """dev_info switch changes the logging format string.""" formatter = CylcLogFormatter(dev_info=dev_info) assert formatter._fmt == expect def test_update_log_archive(tmp_run_dir: Callable): """Test log archive performs as expected""" run_dir = tmp_run_dir('some_workflow') log_dir = Path(run_dir / 'log' / 'scheduler') log_dir.mkdir(exist_ok=True, parents=True) log_file = log_dir.joinpath('log') log_file.touch() log_object = RotatingLogFileHandler( log_file, no_detach=False, restart_num=0 ) for i in range(1, 5): (log_dir / f'{i:02d}-start-{i:02d}.log').touch() log_object.update_log_archive(2) assert list((log_dir.iterdir())).sort() == [ Path(log_dir / 'log'), Path(log_dir / '03-start-03.log'), Path(log_dir / '04-start-04.log')].sort() def test_get_sorted_logs_by_time(tmp_run_dir: Callable): run_dir = tmp_run_dir('some_workflow') config_log_dir = Path(run_dir / 'log' / 'config') config_log_dir.mkdir(exist_ok=True, parents=True) for file in ['01-start-01.cylc', '02-start-01.cylc', '03-restart-02.cylc', '04-reload-02.cylc']: (config_log_dir / file).touch() # Sleep required to ensure modification times are sufficiently # different for sort sleep(0.1) loggies = get_sorted_logs_by_time(config_log_dir, "*.cylc") assert loggies == [f'{config_log_dir}/01-start-01.cylc', f'{config_log_dir}/02-start-01.cylc', f'{config_log_dir}/03-restart-02.cylc', f'{config_log_dir}/04-reload-02.cylc'] def test_get_reload_number(tmp_run_dir: Callable): run_dir = tmp_run_dir('some_reloaded_workflow') config_log_dir = Path(run_dir / 'log' / 'config') config_log_dir.mkdir(exist_ok=True, parents=True) for file in [ '01-start-01.cylc', '02-reload-01.cylc', '03-restart-02.cylc', '04-restart-02.cylc' ]: (config_log_dir / file).touch() # Sleeps required to ensure modification times are sufficiently # different for sort sleep(0.1) config_logs = get_sorted_logs_by_time(config_log_dir, "*.cylc") assert get_reload_start_number(config_logs) == '02' def test_get_reload_number_no_logs(tmp_run_dir: Callable): run_dir = tmp_run_dir('another_reloaded_workflow') config_log_dir = Path(run_dir / 'log' / 'config') config_log_dir.mkdir(exist_ok=True, parents=True) config_logs = get_sorted_logs_by_time(config_log_dir, "*.cylc") assert get_reload_start_number(config_logs) == '01' def test_set_timestamps(capsys): """The enable and disable timstamp methods do what they say""" # Setup log handler log_handler = logging.StreamHandler(sys.stderr) log_handler.setFormatter(CylcLogFormatter()) LOG.addHandler(log_handler) # Log some messages with timestamps on or off: LOG.warning('foo') set_timestamps(LOG, False) LOG.warning('bar') set_timestamps(LOG, True) LOG.warning('baz') # Check 1st and 3rd error have something timestamp-like: errors = capsys.readouterr().err.split('\n') assert re.match('^[0-9]{4}', errors[0]) assert re.match('^WARNING - bar', errors[1]) assert re.match('^[0-9]{4}', errors[2]) LOG.removeHandler(log_handler) def test_log_emit_and_glbl_cfg( monkeypatch: pytest.MonkeyPatch, rotating_log_file_handler ): """Test that log calls do not access the global config object. Doing so can have the side effect of expanding the global config object so should be avoided - see https://github.com/cylc/cylc-flow/issues/6244 """ rotating_log_file_handler(level=logging.DEBUG) mock_cfg = mock.Mock(spec=GlobalConfig) monkeypatch.setattr( 'cylc.flow.cfgspec.globalcfg.GlobalConfig', mock.Mock( spec=GlobalConfig, get_inst=lambda *a, **k: mock_cfg ) ) # Check mocking is correct: glbl_cfg().get(['kinesis']) assert mock_cfg.get.call_args_list == [mock.call(['kinesis'])] mock_cfg.reset_mock() # Check log emit does not access global config object: LOG.debug("Entering zero gravity") assert mock_cfg.get.call_args_list == [] def test_patch_log_level(caplog: pytest.LogCaptureFixture): """Test patch_log_level temporarily changes the log level.""" caplog.set_level(logging.DEBUG) logger = logging.getLogger("forest") logger.setLevel(logging.ERROR) logger.info("nope") assert not caplog.records with patch_log_level(logger, logging.INFO): LOG.info("yep") assert len(caplog.records) == 1 logger.info("nope") assert len(caplog.records) == 1 def test_patch_log_level__reset(caplog: pytest.LogCaptureFixture): """Test patch_log_level resets the log level correctly after use, not affected by the parent logger level - see https://github.com/cylc/cylc-flow/pull/6327 """ caplog.set_level(logging.ERROR) logger = logging.getLogger("woods") assert logger.level == logging.NOTSET with patch_log_level(logger, logging.INFO): logger.info("emitted but not captured, as caplog is at ERROR level") assert not caplog.records caplog.set_level(logging.INFO) logger.info("yep") assert len(caplog.records) == 1 assert logger.level == logging.NOTSET cylc-flow-8.6.4/tests/unit/test_pbs_multi_cluster.py0000664000175000017500000000427615202510242023107 0ustar alastairalastair# 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 . import unittest from cylc.flow.job_runner_handlers.pbs_multi_cluster import ( PBSMulticlusterHandler, ) def get_test_filter_poll_many_output(): return [ ("header1\nheader2\n123.localhost", ["123.localhost@localhost"]), ( """header1 header2 12.localhost job123 09.jd-01.foo.bar""", [ "12.localhost@localhost", "job123", # unchanged "09.jd-01.foo.bar@jd-01.foo.bar", ] ), ] def get_test_manip_job_id(): return [ ("1.localhost", "1.localhost@localhost"), ("10000.jd-01", "10000.jd-01@jd-01"), (" 1077 ", "1077") # unchanged ] class TestPBSMultiCluster(unittest.TestCase): def test_filter_poll_many_output(self): """Basic tests for filter_poll_many_output.""" for out, expected in get_test_filter_poll_many_output(): job_ids = PBSMulticlusterHandler.filter_poll_many_output(out) self.assertEqual(expected, job_ids) def test_manip_job_id(self): """Basic tests for manip_job_id.""" for job_id, expected in get_test_manip_job_id(): mod_job_id = PBSMulticlusterHandler.manip_job_id(job_id) self.assertEqual(expected, mod_job_id) def test_export_handler(self): import cylc.flow.job_runner_handlers.pbs_multi_cluster as m self.assertTrue(hasattr(m, 'JOB_RUNNER_HANDLER')) cylc-flow-8.6.4/tests/unit/README.md0000664000175000017500000000204715202510242017210 0ustar alastairalastair# Unit Tests This directory contains Cylc unit tests. ## How To Run These Tests ```console $ pytest tests/u $ pytest tests/u -n 5 # run up to 5 tests in parallel $ pytest tests/u --dist=no -n0 # turn off xdist (allows --pdb etc) ``` ## What Are Unit Tests Unit tests test the smallest possible units of functionality, typically methods or functions. The interaction of components is mitigated by mocking input objects. ## Guidelines Don't write integration tests here: * If your test requires any of the fixtures in the integration tests then it is an integration test. * If your test sees logic flow through multiple modules it's not a unit test. * If you are constructing computationally expensive objects it's unlikely to be a unit test. ## Doctests Doctests are Python interactive shell examples embedded in the docstrings of functions/methods, that can provide some level of unit testing. E.g. ```python def factorial(n): """ Example: >>> factorial(7) 5040 """ ``` To run doctests: ```console $ pytest cylc/flow ``` cylc-flow-8.6.4/tests/unit/test_flow_mgr.py0000664000175000017500000000564515202510242021165 0ustar alastairalastair# 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 . """Unit tests for FlowManager.""" import pytest import datetime import logging from cylc.flow.flow_mgr import FlowMgr from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager from cylc.flow import CYLC_LOG FAKE_NOW = datetime.datetime(2020, 12, 25, 17, 5, 55) FAKE_NOW_ISO = FAKE_NOW.isoformat() @pytest.fixture def patch_datetime_now(monkeypatch): class mydatetime: @classmethod def now(cls, tz=None): return FAKE_NOW monkeypatch.setattr(datetime, 'datetime', mydatetime) def test_all( patch_datetime_now, caplog: pytest.LogCaptureFixture, ): """Test flow number management.""" db_mgr = WorkflowDatabaseManager() flow_mgr = FlowMgr(db_mgr) caplog.set_level(logging.DEBUG, CYLC_LOG) meta = "the quick brown fox" assert flow_mgr.get_flow(None, meta) == 1 msg1 = f"flow: 1 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg1}" in caplog.messages # automatic: expect 2 meta = "jumped over the lazy dog" assert flow_mgr.get_flow(None, meta) == 2 msg2 = f"flow: 2 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg2}" in caplog.messages # give flow 2: not a new flow meta = "jumped over the moon" assert flow_mgr.get_flow(2, meta) == 2 msg3 = f"flow: 2 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg3}" not in caplog.messages assert ( f"Ignoring flow metadata \"{meta}\": 2 is not a new flow" in caplog.messages ) # give flow 4: new flow meta = "jumped over the cheese" assert flow_mgr.get_flow(4, meta) == 4 msg4 = f"flow: 4 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg4}" in caplog.messages # automatic: expect 3 meta = "jumped over the log" assert flow_mgr.get_flow(None, meta) == 3 msg5 = f"flow: 3 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg5}" in caplog.messages # automatic: expect 5 (skip over 4) meta = "crawled under the log" assert flow_mgr.get_flow(None, meta) == 5 msg6 = f"flow: 5 ({meta}) {FAKE_NOW_ISO}" assert f"New {msg6}" in caplog.messages flow_mgr._log() assert ( "Flows:\n" f"{msg1}\n" f"{msg2}\n" f"{msg4}\n" f"{msg5}\n" f"{msg6}" ) in caplog.messages cylc-flow-8.6.4/tests/unit/test_indep_task_queues.py0000664000175000017500000000564715202510242023063 0ustar alastairalastair# 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 . # # Tests for the task queue manager module from collections import Counter from unittest.mock import Mock import pytest from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_queues.independent import IndepQueueManager MEMBERS = {"a", "b", "c", "d", "e", "f"} ACTIVE = Counter(["a", "a", "d"]) ALL_TASK_NAMES = [ "o1", "o2", "o3", "o4", "o5", "o6", "o7", "s1", "s2", "s3", "s4", "s5", "b1", "b2", "b3", "b4", "b5", "foo" ] DESCENDANTS = { "root": ALL_TASK_NAMES + ["BIG", "SML", "OTH", "foo"], "BIG": ["b1", "b2", "b3", "b4", "b5"], "SML": ["s1", "s2", "s3", "s4", "s5"], "OTH": ["o1", "o2", "o3", "o4", "o5", "o6", "o7"] } QCONFIG = { "default": { "limit": 6, "members": [] # (auto: all task names) }, "big": { "members": ["BIG", "foo"], "limit": 2 }, "sml": { "members": ["SML", "foo"], "limit": 3 } } READY_TASK_NAMES = ["b3", "s4", "o2", "s3", "b4", "o3", "o4", "o5", "o6", "o7"] @pytest.mark.parametrize( "active, expected_released, expected_foo_groups", [ ( Counter(["b1", "b2", "s1", "o1"]), ["s4", "o2", "s3", "o3", "o4", "o5", "o6"], ["sml"] ) ] ) def test_queue_and_release( active, expected_released, expected_foo_groups ): """Test task queue and release.""" # configure the queue queue_mgr = IndepQueueManager(QCONFIG, ALL_TASK_NAMES, DESCENDANTS) # add newly ready tasks to the queue for name in READY_TASK_NAMES: itask = Mock(spec=TaskProxy) itask.tdef.name = name itask.state.is_held = False queue_mgr.push_task(itask) # release tasks, given current active task counter released = queue_mgr.release_tasks(active) assert sorted(r.tdef.name for r in released) == sorted(expected_released) # check that adopted orphans end up in the default queue orphans = ["orphan1", "orphan2"] queue_mgr.adopt_tasks(orphans) for orphan in orphans: assert orphan in queue_mgr.queues["default"].members # check second assignment overrides first for group in expected_foo_groups: assert "foo" in queue_mgr.queues[group].members cylc-flow-8.6.4/tests/unit/test_install.py0000664000175000017500000004516015202510242021013 0ustar alastairalastair# 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 . import os import shutil from pathlib import Path from typing import ( Any, Callable, Dict, Optional, Tuple, Type, Union, ) import pytest from cylc.flow.exceptions import ( InputError, WorkflowFilesError, ) from cylc.flow.workflow_files import ( WorkflowFiles, get_workflow_source_dir, ) from cylc.flow.install import ( NESTED_DIRS_MSG, check_nested_dirs, get_rsync_rund_cmd, get_run_dir_info, get_source_workflow_name, install_workflow, parse_cli_sym_dirs, reinstall_workflow, search_install_source_dirs, validate_source_dir, ) NonCallableFixture = Any # global.cylc[install]scan depth for these tests: MAX_SCAN_DEPTH = 3 @pytest.fixture def glbl_cfg_max_scan_depth(mock_glbl_cfg: Callable) -> None: mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', f''' [install] max depth = {MAX_SCAN_DEPTH} ''' ) @pytest.mark.parametrize( 'workflow_name, err_expected', [ ('foo/' * (MAX_SCAN_DEPTH - 1), False), ('foo/' * MAX_SCAN_DEPTH, True) # /run1 takes it beyond max depth ] ) def test_install_workflow__max_depth( workflow_name: str, err_expected: bool, tmp_run_dir: Callable, tmp_src_dir: Callable, glbl_cfg_max_scan_depth: NonCallableFixture, prevent_symlinking, ): """Test that trying to install beyond max depth fails.""" src_dir = tmp_src_dir('bar') if err_expected: with pytest.raises(WorkflowFilesError) as exc_info: install_workflow(src_dir, workflow_name) assert "would exceed global.cylc[install]max depth" in str( exc_info.value ) else: install_workflow(src_dir, workflow_name) @pytest.mark.parametrize( 'flow_file, expected_exc', [ (WorkflowFiles.FLOW_FILE, WorkflowFilesError), (WorkflowFiles.SUITE_RC, WorkflowFilesError), (None, None) ] ) def test_install_workflow__next_to_flow_file( flow_file: Optional[str], expected_exc: Optional[Type[Exception]], tmp_run_dir: Callable, tmp_src_dir: Callable, prevent_symlinking, ): """Test that you can't install into a dir that contains a workflow file.""" # Setup cylc_run_dir: Path = tmp_run_dir() workflow_dir = cylc_run_dir / 'faden' workflow_dir.mkdir() src_dir: Path = tmp_src_dir('faden') if flow_file: (workflow_dir / flow_file).touch() # Test if expected_exc: with pytest.raises(expected_exc) as exc_info: install_workflow(src_dir, 'faden') assert "Nested run directories not allowed" in str(exc_info.value) else: install_workflow(src_dir, 'faden') def test_install_workflow__symlink_target_exists( tmp_path: Path, tmp_src_dir: Callable, tmp_run_dir: Callable, mock_glbl_cfg: Callable, ): """Test that you can't install workflow when run dir symlink dir target already exists.""" id_ = 'smeagol' src_dir: Path = tmp_src_dir(id_) sym_run = tmp_path / 'sym-run' sym_log = tmp_path / 'sym-log' mock_glbl_cfg( 'cylc.flow.pathutil.glbl_cfg', f''' [install] [[symlink dirs]] [[[localhost]]] run = {sym_run} log = {sym_log} ''' ) msg = "Symlink dir target already exists: .*{}" # Test: (sym_run / 'cylc-run' / id_ / 'run1').mkdir(parents=True) with pytest.raises(WorkflowFilesError, match=msg.format(sym_run)): install_workflow(src_dir) shutil.rmtree(sym_run) ( sym_log / 'cylc-run' / id_ / 'run1' / WorkflowFiles.LogDir.DIRNAME ).mkdir(parents=True) with pytest.raises(WorkflowFilesError, match=msg.format(sym_log)): install_workflow(src_dir) def test_check_nested_dirs( tmp_run_dir: Callable, glbl_cfg_max_scan_depth: NonCallableFixture ): """Test that check_nested_dirs() raises when a parent dir is a workflow directory.""" cylc_run_dir: Path = tmp_run_dir() test_dir = cylc_run_dir.joinpath('a/' * (MAX_SCAN_DEPTH + 3)) # note we check beyond max scan depth (because we're checking upwards) test_dir.mkdir(parents=True) # Parents are not run dirs - ok: check_nested_dirs(test_dir) # Parent contains a run dir but that run dir is not direct ancestor # of our test dir - ok: tmp_run_dir('a/Z') check_nested_dirs(test_dir) # Now make run dir out of parent - not ok: tmp_run_dir('a') with pytest.raises(WorkflowFilesError) as exc: check_nested_dirs(test_dir) assert str(exc.value) == NESTED_DIRS_MSG.format( dir_type='run', dest=test_dir, existing=(cylc_run_dir / 'a') ) @pytest.mark.parametrize( 'named_run', [True, False] ) @pytest.mark.parametrize( 'test_install_path, existing_install_path', [ pytest.param( f'{"child/" * (MAX_SCAN_DEPTH + 3)}', '', id="Check parents (beyond max scan depth)" ), pytest.param( '', f'{"child/" * MAX_SCAN_DEPTH}', id="Check children up to max scan depth" ) ] ) def test_check_nested_dirs_install_dirs( tmp_run_dir: Callable, glbl_cfg_max_scan_depth: NonCallableFixture, test_install_path: str, existing_install_path: str, named_run: bool ): """Test that check nested dirs looks both up and down a tree for WorkflowFiles.Install.DIRNAME. Params: test_install_path: Path relative to ~/cylc-run/thing where we are trying to install a workflow. existing_install_path: Path relative to ~/cylc-run/thing where there is an existing install dir. named_run: Whether the workflow we are trying to install has named/numbered run. """ cylc_run_dir: Path = tmp_run_dir() existing_install: Path = tmp_run_dir( f'thing/{existing_install_path}/run1', installed=True, named=True ).parent test_install_dir = cylc_run_dir / 'thing' / test_install_path test_run_dir = test_install_dir / 'run1' if named_run else test_install_dir with pytest.raises(WorkflowFilesError) as exc: check_nested_dirs(test_run_dir, test_install_dir) assert str(exc.value) == NESTED_DIRS_MSG.format( dir_type='install', dest=test_run_dir, existing=existing_install ) def test_get_workflow_source_dir_numbered_run(tmp_path): """Test get_workflow_source_dir returns correct source for numbered run""" cylc_install_dir = ( tmp_path / "cylc-run" / "flow-name" / "_cylc-install") cylc_install_dir.mkdir(parents=True) run_dir = (tmp_path / "cylc-run" / "flow-name" / "run1") run_dir.mkdir() source_dir = (tmp_path / "cylc-source" / "flow-name") source_dir.mkdir(parents=True) assert get_workflow_source_dir(run_dir) == (None, None) (cylc_install_dir / "source").symlink_to(source_dir) assert get_workflow_source_dir(run_dir) == ( str(source_dir), cylc_install_dir / "source") def test_get_workflow_source_dir_named_run(tmp_path): """Test get_workflow_source_dir returns correct source for named run""" cylc_install_dir = ( tmp_path / "cylc-run" / "flow-name" / "_cylc-install") cylc_install_dir.mkdir(parents=True) source_dir = (tmp_path / "cylc-source" / "flow-name") source_dir.mkdir(parents=True) (cylc_install_dir / "source").symlink_to(source_dir) assert get_workflow_source_dir( cylc_install_dir.parent) == ( str(source_dir), cylc_install_dir / "source") def test_reinstall_workflow(tmp_path, capsys): cylc_install_dir = ( tmp_path / "cylc-run" / "flow-name" / "_cylc-install") cylc_install_dir.mkdir(parents=True) source_dir = (tmp_path / "cylc-source" / "flow-name") source_dir.mkdir(parents=True) (source_dir / "flow.cylc").touch() (cylc_install_dir / "source").symlink_to(source_dir) run_dir = cylc_install_dir.parent reinstall_workflow(source_dir, "flow-name", run_dir) assert capsys.readouterr().out == ( f"REINSTALLED flow-name from {source_dir}\n") @pytest.mark.parametrize( 'filename, expected_err', [('flow.cylc', None), ('suite.rc', None), ('fluff.txt', (WorkflowFilesError, "Could not find workflow 'baa/baa'"))] ) def test_search_install_source_dirs( filename: str, expected_err: Optional[Tuple[Type[Exception], str]], tmp_path: Path, mock_glbl_cfg: Callable): """Test search_install_source_dirs(). Params: filename: A file to insert into one of the source dirs. expected_err: Exception and message expected to be raised. """ horse_dir = tmp_path / 'horse' horse_dir.mkdir() sheep_dir = tmp_path / 'sheep' source_dir = sheep_dir / 'baa' / 'baa' source_dir.mkdir(parents=True) source_dir_file = source_dir / filename source_dir_file.touch() mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', f''' [install] source dirs = {horse_dir}, {sheep_dir} ''' ) if expected_err: err, msg = expected_err with pytest.raises(err) as exc: search_install_source_dirs('baa/baa') assert msg in str(exc.value) else: ret = search_install_source_dirs('baa/baa') assert ret == source_dir assert ret.is_absolute() def test_search_install_source_dirs_empty(mock_glbl_cfg: Callable): """Test search_install_source_dirs() when no source dirs configured.""" mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', ''' [install] source dirs = ''' ) with pytest.raises(WorkflowFilesError) as exc: search_install_source_dirs('foo') assert str(exc.value) == ( "Cannot find workflow as 'global.cylc[install]source dirs' " "does not contain any paths") @pytest.mark.parametrize( 'path, expected', [ ('~/isla/nublar/dennis/nedry', 'dennis/nedry'), ('~/isla/sorna/paul/kirby', 'paul/kirby'), ('~/mos/eisley/owen/skywalker', 'skywalker') ] ) def test_get_source_workflow_name( path: str, expected: str, mock_glbl_cfg: Callable ): mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', ''' [install] source dirs = ~/isla/nublar, ${HOME}/isla/sorna ''' ) assert get_source_workflow_name( Path(path).expanduser().resolve()) == expected def test_get_rsync_rund_cmd( tmp_src_dir: Callable, tmp_run_dir: Callable ): """Test rsync command for cylc install/reinstall excludes cylc dirs. """ src_dir = tmp_src_dir('foo') cylc_run_dir: Path = tmp_run_dir('rsync_flow', installed=True, named=False) for wdir in [ WorkflowFiles.WORK_DIR, WorkflowFiles.SHARE_DIR, WorkflowFiles.LogDir.DIRNAME, ]: cylc_run_dir.joinpath(wdir).mkdir(exist_ok=True) actual_cmd = get_rsync_rund_cmd(src_dir, cylc_run_dir) assert actual_cmd == [ 'rsync', '-a', '--checksum', '--out-format=%o %n%L', '--no-t', '--exclude=/log', '--exclude=/work', '--exclude=/share', '--exclude=/_cylc-install', '--exclude=/.service', f'{src_dir}/', f'{cylc_run_dir}/'] @pytest.mark.parametrize( 'args, expected_relink, expected_run_num, expected_run_dir', [ ( ['{cylc_run}/numbered', None, False], True, 1, '{cylc_run}/numbered/run1' ), ( ['{cylc_run}/named', 'dukat', False], False, None, '{cylc_run}/named/dukat' ), ( ['{cylc_run}/unnamed', None, True], False, None, '{cylc_run}/unnamed' ), ] ) def test_get_run_dir_info( args: list, expected_relink: bool, expected_run_num: Optional[int], expected_run_dir: Union[Path, str], tmp_run_dir: Callable ): """Test get_run_dir_info(). Params: args: Input args to function. expected_*: Expected return values. """ # Setup cylc_run_dir: Path = tmp_run_dir() sub = lambda x: Path(x.format(cylc_run=cylc_run_dir)) # noqa: E731 args[0] = sub(args[0]) expected_run_dir = sub(expected_run_dir) # Test assert get_run_dir_info(*args) == ( expected_relink, expected_run_num, expected_run_dir ) assert expected_run_dir.is_absolute() def test_get_run_dir_info__increment_run_num(tmp_run_dir: Callable): """Test that get_run_dir_info() increments run number and unlinks runN.""" # Setup cylc_run_dir: Path = tmp_run_dir() run_dir: Path = tmp_run_dir('gowron/run1') runN = run_dir.parent / WorkflowFiles.RUN_N assert os.path.lexists(runN) # Test assert get_run_dir_info(cylc_run_dir / 'gowron', None, False) == ( True, 2, cylc_run_dir / 'gowron' / 'run2' ) assert not os.path.lexists(runN) def test_get_run_dir_info__fail(tmp_run_dir: Callable): # Test that you can't install named runs when numbered runs exist cylc_run_dir: Path = tmp_run_dir() run_dir: Path = tmp_run_dir('martok/run1') with pytest.raises(WorkflowFilesError) as excinfo: get_run_dir_info(run_dir.parent, 'targ', False) assert "contains installed numbered runs" in str(excinfo.value) # Test that you can install numbered run in an empty dir base_dir = cylc_run_dir / 'odo' base_dir.mkdir() get_run_dir_info(base_dir, None, False) # But not when named runs exist tmp_run_dir('odo/meter') with pytest.raises(WorkflowFilesError) as excinfo: get_run_dir_info(base_dir, None, False) assert "contains an installed workflow" @pytest.mark.parametrize( 'symlink_dirs, err_msg, expected', [ ('log=$shortbread, share= $bourbon,share/cycle= $digestive, ', "There is an error in --symlink-dirs option:", None ), ('log=$shortbread share= $bourbon share/cycle= $digestive ', "There is an error in --symlink-dirs option:" " log=$shortbread share= $bourbon share/cycle= $digestive . " "Try entering option in the form --symlink-dirs=" "'log=$DIR, share=$DIR2, ...'", None ), ('run=$NICE, log= $Garibaldi, share/cycle=$RichTea', None, {'localhost': { 'run': '$NICE', 'log': '$Garibaldi', 'share/cycle': '$RichTea' }} ), ('some_other_dir=$bourbon', 'some_other_dir not a valid entry for --symlink-dirs', {'some_other_dir': '£bourbon'} ), ] ) def test_parse_cli_sym_dirs( symlink_dirs: str, err_msg: str, expected: Dict[str, Dict[str, Any]] ): """Test parse_cli_sym_dirs returns dict or correctly raises errors on cli symlink dir options""" if err_msg is not None: with pytest.raises(InputError) as exc: parse_cli_sym_dirs(symlink_dirs) assert err_msg in str(exc) else: actual = parse_cli_sym_dirs(symlink_dirs) assert actual == expected def test_validate_source_dir(tmp_run_dir: Callable, tmp_src_dir: Callable): cylc_run_dir: Path = tmp_run_dir() src_dir: Path = tmp_src_dir('ludlow') validate_source_dir(src_dir, 'ludlow') # Test that src dir must have flow file (src_dir / WorkflowFiles.FLOW_FILE).unlink() with pytest.raises(WorkflowFilesError): validate_source_dir(src_dir, 'ludlow') # Test that reserved dirnames not allowed in src dir src_dir = tmp_src_dir('roland') (src_dir / 'log').mkdir() with pytest.raises(WorkflowFilesError) as exc_info: validate_source_dir(src_dir, 'roland') assert "exists in source directory" in str(exc_info.value) # Test that src dir is allowed to be inside ~/cylc-run src_dir = cylc_run_dir / 'dieter' src_dir.mkdir() (src_dir / WorkflowFiles.FLOW_FILE).touch() validate_source_dir(src_dir, 'dieter') # Test that src dir is not allowed to be an installed dir. src_dir = cylc_run_dir / 'ajay' src_dir.mkdir() (src_dir / WorkflowFiles.Install.DIRNAME).mkdir() (src_dir / WorkflowFiles.FLOW_FILE).touch() with pytest.raises(WorkflowFilesError) as exc_info: validate_source_dir(src_dir, 'ajay') assert "exists in source directory" in str(exc_info.value) def test_install_workflow_failif_name_name(tmp_src_dir, tmp_run_dir): """If a run_name is given validate_workflow_name is called on the workflow and the run name in combination. """ src_dir: Path = tmp_src_dir('ludlow') # It only has a workflow name: with pytest.raises(WorkflowFilesError, match='can only contain'): install_workflow(src_dir, workflow_name='foo?') # It only has a run name: with pytest.raises(WorkflowFilesError, match='can only contain'): install_workflow(src_dir, run_name='foo?') # It has a legal workflow name, but an invalid run name: with pytest.raises(WorkflowFilesError, match='can only contain'): install_workflow(src_dir, workflow_name='foo', run_name='bar?') def test_install_workflow_failif_reserved_name(tmp_src_dir, tmp_run_dir): """Reserved names cause install validation failure. n.b. manually defined to avoid test dependency on workflow_files. """ src_dir = tmp_src_dir('ludlow') is_reserved = '(that filename is reserved)' reserved_names = { 'share', 'log', 'runN', 'suite.rc', 'work', '_cylc-install', 'flow.cylc', # .service fails because starting a workflow/run can't start with "." # And that check fails first. # '.service' } install_workflow(src_dir, workflow_name='ok', run_name='also_ok') for name in reserved_names: with pytest.raises(WorkflowFilesError, match=is_reserved): install_workflow(src_dir, workflow_name='ok', run_name=name) with pytest.raises(WorkflowFilesError, match=is_reserved): install_workflow(src_dir, workflow_name=name) with pytest.raises(WorkflowFilesError, match=is_reserved): install_workflow(src_dir, workflow_name=name, run_name='ok') cylc-flow-8.6.4/tests/unit/test_graph_parser.py0000664000175000017500000006770415202510242022032 0ustar alastairalastair# 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 . """Unit tests for the GraphParser.""" import logging from typing import Dict, List import pytest from itertools import product from pytest import param from types import SimpleNamespace from cylc.flow import CYLC_LOG from cylc.flow.exceptions import GraphParseError, ParamExpandError from cylc.flow.graph_parser import GraphParser from cylc.flow.task_outputs import ( TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED ) @pytest.mark.parametrize( 'graph', [ 't1 => & t2', 't1 => t2 &', '& t1 => t2', 't1 & => t2', 't1 => => t2' ] ) def test_parse_graph_fails_null_task_name(graph): """Test fail null task names.""" with pytest.raises(GraphParseError) as cm: GraphParser().parse_graph(graph) assert "Null task name in graph:" in str(cm.value) @pytest.mark.parametrize('seq', ('&', '|', '=>')) @pytest.mark.parametrize( 'graph, expected_err', [ [ "{0} b", "Leading {0}" ], [ "a {0}", "Dangling {0}" ], [ "{0} b {0} c", "Leading {0}" ], [ "a {0} b {0}", "Dangling {0}" ] ] ) def test_graph_syntax_errors_2(seq, graph, expected_err): """Test various graph syntax errors.""" graph = graph.format(seq) expected_err = expected_err.format(seq) with pytest.raises(GraphParseError) as cm: GraphParser().parse_graph(graph) assert ( expected_err in str(cm.value) ) @pytest.mark.parametrize( 'graph, expected_err', [ ( "a b => c", "Bad graph node format" ), ( "a => b c", "Bad graph node format" ), ( "!foo => bar", "Suicide markers must be on the right of a trigger:" ), ( "( foo & bar => baz", 'Mismatched parentheses in: "(foo&bar"' ), ( "a => b & c)", 'Mismatched parentheses in: "b&c)"' ), ( "(a => b & c)", 'Mismatched parentheses in: "(a"' ), ( "(a => b[+P1]", 'Mismatched parentheses in: "(a"' ), ( """(a | b & c) => d foo => bar (a | b & c) => !d""", "can't trigger both d and !d" ), ( "a => b | c", "Illegal OR on right side" ), ( "foo && bar => baz", "The graph AND operator is '&'" ), ( "foo || bar => baz", "The graph OR operator is '|'" ), param( # See https://github.com/cylc/cylc-flow/issues/5844 "foo => bar[1649]", 'Invalid cycle point offsets only on right', id='no-cycle-point-RHS' ), # See https://github.com/cylc/cylc-flow/issues/6523 # For the next 4 tests: # NB "foo:succeeded" now explicit on the right (this used to test # inferred "foo:succeeded" from "=> foo", which we no longer infer). param( # Yes I know it's circular, but it's here to # demonstrate that the test below is broken: "foo:finished => foo:succeeded", 'Output foo:succeeded can\'t be both required and optional', id='finish-implies-success-optional' ), param( "foo[-P1]:finish => foo:succeeded", 'Output foo:succeeded can\'t be both required and optional', id='finish-implies-success-optional-offset' ), param( "foo[-P1]:succeeded | foo[-P1]:failed => bar", # order of outputs varies in the error message 'must both be optional if both are used', id='succeed-or-failed-mustbe-optional' ), param( "foo[-P1]:succeeded? | foo[-P1]:failed? => foo:succeeded", 'Output foo:succeeded can\'t be both required and optional', id='succeed-or-failed-implies-success-optional' ), ] ) def test_graph_syntax_errors(graph, expected_err): """Test various graph syntax errors.""" with pytest.raises(GraphParseError) as cm: GraphParser().parse_graph(graph) assert expected_err in str(cm.value) def test_parse_graph_simple(): """Test parsing graphs.""" # added white spaces and comments to show that these change nothing gp = GraphParser() gp.parse_graph('a => b\n \n# this is a comment\n') original = gp.original triggers = gp.triggers families = gp.family_map assert ( original == {'a': {'': ''}, 'b': {'a:succeeded': 'a:succeeded'}} ) assert ( triggers == { 'a': {'': ([], False)}, 'b': {'a:succeeded': (['a:succeeded'], False)} } ) assert not families @pytest.mark.parametrize( 'graph, expect', [ param( 'a => b\n=> c', SimpleNamespace( original={ 'c': {'b:succeeded': 'b:succeeded'}, 'b': {'a:succeeded': 'a:succeeded'}, 'a': {'': ''} }, triggers={ 'c': {'b:succeeded': (['b:succeeded'], False)}, 'b': {'a:succeeded': (['a:succeeded'], False)}, 'a': {'': ([], False)} }, families={} ), id='line break on =>' ), param( 'a & b\n& c', SimpleNamespace( original={'b': {'': ''}, 'a': {'': ''}, 'c': {'': ''}}, triggers={ 'b': {'': ([], False)}, 'a': {'': ([], False)}, 'c': {'': ([], False)} }, families={} ), id='line break on &' ), param( 'a | b\n| c', SimpleNamespace( original={'b': {'': ''}, 'c': {'': ''}, 'a': {'': ''}}, triggers={ 'b': {'': ([], False)}, 'c': {'': ([], False)}, 'a': {'': ([], False)} }, families={} ), id='line break on |' ) ] ) def test_parse_graph_simple_with_break_line_01(graph, expect): """Test parsing graphs.""" parser = GraphParser() parser.parse_graph(graph) assert parser.original == expect.original assert parser.triggers == expect.triggers assert not parser.family_map def test_parse_graph_simple_with_break_line_02(): """Test parsing graphs.""" gp = GraphParser() gp.parse_graph( 'a => b\n' '=> c =>\n' 'd' ) original = gp.original triggers = gp.triggers families = gp.family_map assert original['a'] == {'': ''} assert original['b'] == {'a:succeeded': 'a:succeeded'} assert original['c'] == {'b:succeeded': 'b:succeeded'} assert original['d'] == {'c:succeeded': 'c:succeeded'} assert triggers['a'] == {'': ([], False)} assert triggers['b'] == {'a:succeeded': (['a:succeeded'], False)} assert triggers['c'] == {'b:succeeded': (['b:succeeded'], False)} assert triggers['d'] == {'c:succeeded': (['c:succeeded'], False)} assert not families def test_parse_graph_with_parameters(): """Test parsing graphs with parameters.""" parameterized_parser = GraphParser( None, ({'city': ['la_paz']}, {'city': '_%(city)s'})) parameterized_parser.parse_graph('a => b') original = parameterized_parser.original triggers = parameterized_parser.triggers families = parameterized_parser.family_map assert ( original == {'a': {'': ''}, 'b_la_paz': {'a:succeeded': 'a:succeeded'}} ) assert ( triggers == { 'a': {'': ([], False)}, 'b_la_paz': {'a:succeeded': (['a:succeeded'], False)} } ) assert not families def test_parse_graph_with_invalid_parameters(): """Test parsing graphs with invalid parameters.""" parameterized_parser = GraphParser( None, ({'city': ['la_paz']}, {'city': '_%(city)s'})) with pytest.raises(ParamExpandError): # no state in the parameters list parameterized_parser.parse_graph('a => b') def test_inter_workflow_dependence_simple(): """Test invalid inter-workflow dependence""" gp = GraphParser() gp.parse_graph( """ a => b c => d """ ) assert ( gp.original == { 'a': {'': ''}, 'b': {'a:succeeded': 'a:succeeded'}, 'c': {'': ''}, 'd': {'c:succeeded': 'c:succeeded'} } ) assert ( gp.triggers == { 'a': {'': ([], False)}, 'c': {'': ([], False)}, 'b': {'a:succeeded': (['a:succeeded'], False)}, 'd': {'c:succeeded': (['c:succeeded'], False)} } ) assert ( gp.workflow_state_polling_tasks == { 'a': ( 'WORKFLOW', 'TASK', 'failed', '' ), # Default to "succeeded" is done in config module. 'c': ( 'WORKFLOW', 'TASK', None, '' ) } ) assert not gp.family_map def test_line_continuation(): """Test syntax-driven line continuation.""" graph1 = "a => b => c" graph2 = """a => b => c""" graph3 = """a => b => c""" gp1 = GraphParser() gp1.parse_graph(graph1) gp2 = GraphParser() gp2.parse_graph(graph2) gp3 = GraphParser() gp3.parse_graph(graph3) res = { 'a': {'': ([], False)}, 'c': {'b:succeeded': (['b:succeeded'], False)}, 'b': {'a:succeeded': (['a:succeeded'], False)} } assert res == gp1.triggers assert gp1.triggers == gp2.triggers assert gp1.triggers == gp3.triggers graph = """foo => bar a => b =>""" gp = GraphParser() pytest.raises(GraphParseError, gp.parse_graph, graph) graph = """ => a => b foo => bar""" gp = GraphParser() pytest.raises(GraphParseError, gp.parse_graph, graph) @pytest.mark.parametrize( 'graph1, graph2', [ [ "foo => bar", # default trigger "foo:succeed => bar" ], [ "foo => bar", # default trigger "foo:succeeded => bar" ], [ "foo => bar", # repeat trigger """foo => bar foo => bar""" ], [ "foo:finished => bar", # finish trigger "(foo:succeed? | foo:fail?) => bar" ], [ """ bar foo => bar:succeed => baz # ignore qualifier on RHS """, """ foo => bar bar:succeed => baz """ ], [ """ foo => bar[1649] => baz """, """ foo => bar[1649] bar[1649] => baz """ ], ] ) def test_trigger_equivalence(graph1, graph2): gp1 = GraphParser() gp1.parse_graph(graph1) gp2 = GraphParser() gp2.parse_graph(graph2) assert gp1.triggers == gp2.triggers @pytest.mark.parametrize( 'fam_map, fam_graph, member_graph', [ [ {'FAM': ['m1', 'm2'], 'BAM': ['b1', 'b2']}, "FAM:succeed-all => BAM", """(m1 & m2) => b1 (m1 & m2) => b2""" ], [ {'FAM': ['m1', 'm2']}, "pre => FAM", """pre => m1 pre => m2""" ], [ {'FAM': ['m1', 'm2']}, "FAM:succeed-all => post", "(m1 & m2) => post" ], [ {'FAM': ['m1', 'm2']}, "FAM:succeed-any => post", "(m1 | m2) => post", ], [ {'FAM': ['m1', 'm2'], 'BAM': ['b1', 'b2']}, "FAM:fail-any => BAM", """(m1:fail | m2:fail) => b1 (m1:fail | m2:fail) => b2""" ], [ {'FAM': ['m1', 'm2']}, "FAM:finish-all => post", "((m1? | m1:fail?) & (m2? | m2:fail?)) => post" ] ] ) def test_family_trigger_equivalence(fam_map, fam_graph, member_graph): """Test family trigger semantics.""" gp1 = GraphParser(fam_map) gp1.parse_graph(fam_graph) gp2 = GraphParser() gp2.parse_graph(member_graph) assert gp1.triggers == gp2.triggers def test_parameter_expand(): """Test graph parameter expansion.""" fam_map = { 'FAM_m0': ['fa_m0', 'fb_m0'], 'FAM_m1': ['fa_m1', 'fb_m1'], } params = {'m': ['0', '1'], 'n': ['0', '1']} templates = {'m': '_m%(m)s', 'n': '_n%(n)s'} gp1 = GraphParser(fam_map, (params, templates)) gp1.parse_graph(""" pre => foo => bar bar => baz # specific case bar => bar # inter-chunk """) gp2 = GraphParser() gp2.parse_graph(""" pre => foo_m0_n0 => bar_n0 pre => foo_m0_n1 => bar_n1 pre => foo_m1_n0 => bar_n0 pre => foo_m1_n1 => bar_n1 bar_n0 => baz bar_n0 => bar_n1 """) assert gp1.triggers == gp2.triggers def test_parameter_specific(): """Test graph parameter expansion with a specific value.""" params = {'i': ['0', '1'], 'j': ['0', '1', '2']} templates = {'i': '_i%(i)s', 'j': '_j%(j)s'} gp1 = GraphParser(family_map=None, parameters=(params, templates)) gp1.parse_graph("bar => baz\nfoo => qux") gp2 = GraphParser() gp2.parse_graph(""" foo_i1_j0 => qux foo_i1_j1 => qux foo_i1_j2 => qux bar_i0_j0 => baz_i1_j0 bar_i0_j1 => baz_i1_j1 bar_i0_j2 => baz_i1_j2""") assert gp1.triggers == gp2.triggers def test_parameter_offset(): """Test graph parameter expansion with an offset.""" params = {'i': ['0', '1'], 'j': ['0', '1', '2']} templates = {'i': '_i%(i)s', 'j': '_j%(j)s'} gp1 = GraphParser(family_map=None, parameters=(params, templates)) gp1.parse_graph("bar => baz") gp2 = GraphParser() gp2.parse_graph(""" bar_i0_j0 => baz_i1_j0 bar_i0_j1 => baz_i1_j1 bar_i0_j2 => baz_i1_j2""") assert gp1.triggers == gp2.triggers def test_conditional(): """Test generation of conditional triggers.""" gp1 = GraphParser() gp1.parse_graph("(foo:start | bar) => baz") res = { 'baz': { '(foo:started|bar:succeeded)': ( ['foo:started', 'bar:succeeded'], False) }, 'foo': { '': ([], False) }, 'bar': { '': ([], False) } } assert res == gp1.triggers == res @pytest.mark.parametrize( 'graph', [ "foo[-P1Y] => bar", "foo:fail => bar", "foo:fail[-P1Y] => bar", "foo[-P1Y]:fail => bar", "foo[-P1Y]:fail => bar", "foo:fail[-P1Y] => bar", "foo:fail[-P1Y] => bar", ":fail[-P1Y] => bar", "[-P1Y] => bar", "[-P1Y]:fail => bar", "bar => foo:fail[-P1Y]", "foo[-P1Y]baz => bar" ] ) def test_bad_node_syntax(graph): """Test that badly formatted graph nodes are detected. The correct format is: NAME()([CYCLE-POINT-OFFSET])(:TRIGGER-TYPE)") """ params = {'m': ['0', '1'], 'n': ['0', '1']} templates = {'m': '_m%(m)s', 'n': '_n%(n)s'} gp = GraphParser(parameters=(params, templates)) with pytest.raises(GraphParseError) as cm: gp.parse_graph(graph) assert "Bad graph node format" in str(cm.value) def test_spaces_between_tasks_fails(): """Test that is rejected (i.e. no & or | in between)""" gp = GraphParser() pytest.raises( GraphParseError, gp.parse_graph, "foo bar=> baz") pytest.raises( GraphParseError, gp.parse_graph, "foo&bar=> ba z") pytest.raises( GraphParseError, gp.parse_graph, "foo 123=> bar") pytest.raises( GraphParseError, gp.parse_graph, "foo - 123 baz=> bar") def test_spaces_between_parameters_fails(): """Test that are rejected (i.e. no comma)""" gp = GraphParser() pytest.raises( GraphParseError, gp.parse_graph, " => baz") pytest.raises( GraphParseError, gp.parse_graph, " => baz") pytest.raises( GraphParseError, gp.parse_graph, " => baz") def test_spaces_between_parameters_passes(): """Test that works, with spaces around the -+ signs""" params = {'m': ['0', '1', '2']} templates = {'m': '_m%(m)s'} gp = GraphParser(parameters=(params, templates)) gp.parse_graph(" => ") gp.parse_graph(" => ") gp.parse_graph(" => ") gp.parse_graph(" => ") gp.parse_graph(" => ") gp.parse_graph(" => ") def test_spaces_in_trigger_fails(): """Test that 'task:a- b' are rejected""" gp = GraphParser() pytest.raises( GraphParseError, gp.parse_graph, "FOO:custom -trigger => baz") pytest.raises( GraphParseError, gp.parse_graph, "FOO:custom- trigger => baz") pytest.raises( GraphParseError, gp.parse_graph, "FOO:custom - trigger => baz") def test_parameter_graph_mixing_offset_and_conditional(): """Test for bug reported in issue #2608 on GitHub: https://github.com/cylc/cylc-flow/issues/2608""" params = {'m': ["cat", "dog"]} templates = {'m': '_%(m)s'} gp = GraphParser(parameters=(params, templates)) gp.parse_graph("foo & baz => foo") triggers = { 'foo_cat': { '': ( [], False ), 'baz:succeeded': ( ['baz:succeeded'], False ) }, 'foo_dog': { 'foo_cat:succeeded': ( ['foo_cat:succeeded'], False ), 'baz:succeeded': ( ['baz:succeeded'], False ) }, 'baz': { '': ([], False) } } assert gp.triggers == triggers def test_param_expand_graph_parser(): """Test to validate that the graph parser removes out-of-edge nodes: https://github.com/cylc/cylc-flow/pull/3452#issuecomment-677165000""" params = {'m': ["cat"]} templates = {'m': '_%(m)s'} gp = GraphParser(parameters=(params, templates)) gp.parse_graph("foo => bar => baz") triggers = { 'foo': { '': ([], False) } } assert gp.triggers == triggers @pytest.mark.parametrize( 'expect', ('&', '|', '=>') ) def test_parse_graph_fails_with_continuation_at_last_line(expect): """Fails if last line contains a continuation char. """ parser = GraphParser() with pytest.raises(GraphParseError) as raised: parser.parse_graph(f't1 => t2 {expect}') assert isinstance(raised.value, GraphParseError) assert f'Dangling {expect}' in raised.value.args[0] @pytest.mark.parametrize( 'before, after', product(['&', '|', '=>'], repeat=2) ) def test_parse_graph_fails_with_too_many_continuations(before, after): """Fails if one line ends with continuation char and the next line _also_ starts with one. """ parser = GraphParser() with pytest.raises(GraphParseError) as raised: parser.parse_graph(f'foo & bar {before}\n{after}baz') assert isinstance(raised.value, GraphParseError) assert 'Consecutive lines end and start' in raised.value.args[0] def test_task_optional_outputs(): """Test optional outputs are correctly parsed from graph. This checks "task_output_opt" dict which holds output optionality inferred from the graph. Note since https://github.com/cylc/cylc-flow/pull/6999 we no longer infer optionality from *implicit* outputs on RHS of triggers, i.e. "a => b" does not imply b:succeeded is a required output. """ OPTIONAL = True REQUIRED = False gp = GraphParser() gp.parse_graph( """ a1 => b1 # does not imply b1:succeeded ... a2:succeed => b2:succeeded a3:succeed => b3:succeed c1? => d1? c2:succeed? => d2? c3:succeed? => d3:succeed? x:fail? => y foo:finish => bar """ ) for i in range(1, 4): for task in (f'a{i}', f'b{i}'): if task != "b1": assert ( gp.task_output_opt[(task, TASK_OUTPUT_SUCCEEDED)] == (REQUIRED, False, True) ) for task in (f'c{i}', f'd{i}'): assert ( gp.task_output_opt[(task, TASK_OUTPUT_SUCCEEDED)] == (OPTIONAL, True, True) ) assert ( gp.task_output_opt[('x', TASK_OUTPUT_FAILED)] == (OPTIONAL, True, True) ) assert ( gp.task_output_opt[('foo', TASK_OUTPUT_SUCCEEDED)] == (OPTIONAL, True, True) ) assert ( gp.task_output_opt[('foo', TASK_OUTPUT_FAILED)] == (OPTIONAL, True, True) ) @pytest.mark.parametrize( 'qual, task_output', [ ('start', TASK_OUTPUT_STARTED), ('succeed', TASK_OUTPUT_SUCCEEDED), ('fail', TASK_OUTPUT_FAILED), ('submit', TASK_OUTPUT_SUBMITTED), ] ) def test_family_optional_outputs(qual, task_output): """Test member output optionality inferred from family triggers.""" fam_map = { 'FAM': ['f1', 'f2'], 'BAM': ['b1', 'b2'], } gp = GraphParser(fam_map) gp.parse_graph( f""" # required FAM:{qual}-all => foo # optional member f2:{task_output}? # required BAM:{qual}-any => bar """ ) # -all for member in ['f1', 'f2']: optional = (member == 'f2') assert gp.task_output_opt[(member, task_output)][0] == optional # -any optional = False for member in ['b1', 'b2']: assert gp.task_output_opt[(member, task_output)][0] == optional def test_cannot_be_required(): """Is should not allow :expired or :submit-failed to be required. See proposal point 4: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ gp = GraphParser({}) # outputs can be optional gp.parse_graph('a:expired? => b') gp.parse_graph('a:submit-failed? => b') # but cannot be required with pytest.raises(GraphParseError, match='must be optional'): gp.parse_graph('a:expired => b') with pytest.raises(GraphParseError, match='must be optional'): gp.parse_graph('a:submit-failed => b') @pytest.mark.parametrize( 'graph, error', [ [ """FAM:succeed-all => foo FAM:fail-all => foo""", ("must both be optional if both are used (via family trigger" " defaults") ], [ """FAM:succeed-all => foo FAM:succeed-any? => bar""", ("can't default to both optional and required (via family trigger" " defaults)") ], [ "FAM:blargh-all => foo", # LHS "Illegal family trigger" ], [ "foo => FAM:blargh-all", # RHS "Illegal family trigger" ], [ "FAM => foo", # bare family on LHS "Family trigger required: FAM => foo" ], [ "FAM:expire-all => foo", "must be optional" ], ] ) def test_family_trigger_errors(graph, error): """Test errors via bad family triggers and member output optionality.""" fam_map = { 'FAM': ['f1', 'f2'] } gp = GraphParser(fam_map) with pytest.raises(GraphParseError) as cm: gp.parse_graph(graph) assert error in str(cm.value) @pytest.mark.parametrize( 'graph, c8error', [ [ """a:x => b a:x? => c""", "Output a:x can't be both required and optional", ], [ """a? => c a => b""", "Output a:succeeded can't be both required and optional", ], [ """a => c a:fail => b""", ("must both be optional if both are used"), ], [ """a:fail? => b a => c""", ("must both be optional if both are used"), ], [ "a:finish? => b", "Pseudo-output a:finished can't be optional", ], [ "a:expire => b", "must be optional", ], ] ) def test_task_optional_output_errors_order( graph, c8error, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch ): """Test optional output errors are raised as expected.""" gp = GraphParser() with pytest.raises(GraphParseError) as cm: gp.parse_graph(graph) assert c8error in str(cm.value) # In Cylc 7 back compat mode these graphs should all pass with no warnings. monkeypatch.setattr('cylc.flow.flags.cylc7_back_compat', True) caplog.set_level(logging.WARNING, CYLC_LOG) gp = GraphParser() gp.parse_graph(graph) # No warnings logged: assert not caplog.messages # After graph parsing all Cylc 7 back compat outputs should be optional. # (Success outputs are set to required later, in taskdef processing.) for (optional, _, _) in gp.task_output_opt.values(): assert optional @pytest.mark.parametrize( 'ftrig', GraphParser.fam_to_mem_trigger_map.keys() ) def test_fail_family_triggers_on_tasks(ftrig): gp = GraphParser() with pytest.raises(GraphParseError) as cm: gp.parse_graph(f"foo:{ftrig} => bar") assert ( str(cm.value).startswith( "family trigger on non-family namespace" ) ) @pytest.mark.parametrize( 'graph, expected_triggers', [ param( 'a => b & c', {'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']}, id="simple" ), param( 'a => (b & c)', {'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']}, id="simple w/ parentheses" ), param( 'a => (b & (c & d))', { 'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded'], 'd': ['a:succeeded'], }, id="more parentheses" ), ] ) def test_RHS_AND(graph: str, expected_triggers: Dict[str, List[str]]): """Test '&' operator on right hand side of trigger expression.""" gp = GraphParser() gp.parse_graph(graph) triggers = { task: list(trigs.keys()) for task, trigs in gp.triggers.items() } assert triggers == expected_triggers @pytest.mark.parametrize( 'args, err', ( # No error if offset in NON-terminal RHS: param((('a', 'b[-P42M]'), {}, set(), set()), None), # Check the left hand side if this has a non-terminal RHS: param( (('a &', 'b[-P42M]'), {}, set(), set()), 'Null task name in graph' ), ), ) def test_proc_dep_pair(args, err): """ Unit tests for _proc_dep_pair. """ gp = GraphParser() if err: with pytest.raises(GraphParseError, match=err): gp._proc_dep_pair(*args) else: assert gp._proc_dep_pair(*args) is None cylc-flow-8.6.4/tests/unit/test_task_remote_cmd.py0000664000175000017500000000522215202510242022500 0ustar alastairalastair# 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 . """Tests for remote initialisation.""" from pathlib import Path from unittest.mock import patch from pytest import CaptureFixture from cylc.flow.workflow_files import WorkflowFiles from cylc.flow.task_remote_cmd import remote_init def test_existing_key_raises_error(tmp_path: Path, capsys: CaptureFixture): """Test .service directory that contains existing incorrect key, results in REMOTE INIT FAILED""" rundir = tmp_path / 'some_rund' srvdir = rundir / WorkflowFiles.Service.DIRNAME srvdir.mkdir(parents=True) (srvdir / 'client_wrong.key').touch() remote_init('test_install_target', str(rundir)) assert capsys.readouterr().out == ( "REMOTE INIT FAILED\nUnexpected authentication key" " \"client_wrong.key\" exists. Check global.cylc install target is" " configured correctly for this platform.\n") @patch('os.path.expandvars') def test_unexpandable_symlink_env_var_returns_failed( mocked_expandvars, capsys): """Test unexpandable symlinks return REMOTE INIT FAILED""" mocked_expandvars.side_effect = ['some/rund/path', '$blah'] remote_init('test_install_target', 'some_rund', 'run=$blah') assert capsys.readouterr().out == ( "REMOTE INIT FAILED\nError occurred when symlinking." " $blah contains an invalid environment variable.\n") def test_existing_client_key_dir_raises_error( tmp_path: Path, capsys: CaptureFixture): """Test .service directory that contains existing incorrect key, results in REMOTE INIT FAILED """ rundir = tmp_path / 'some_rund' keydir = rundir / WorkflowFiles.Service.DIRNAME / "client_public_keys" keydir.mkdir(parents=True) remote_init('test_install_target', rundir) assert capsys.readouterr().out == ( f"REMOTE INIT FAILED\nUnexpected key directory exists: {keydir}" " Check global.cylc install target is configured correctly for this" " platform.\n") cylc-flow-8.6.4/tests/unit/test_syntax_generator.py0000664000175000017500000000355615202510242022744 0ustar alastairalastair# 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 . import importlib from pathlib import Path # Fake up the module. path = Path(__file__).parent.parent.parent / 'etc/bin/syntax_generator.py' loader = importlib.machinery.SourceFileLoader( 'syntaxgenerator', str(path) ) syntax = loader.load_module() def test_get_keywords_from_workflow_cfg(): """It gets a list of config items. Not a thorough check, but ensure type is sensible and some unlikely-to change items are present. """ result = syntax.get_keywords_from_workflow_cfg() assert isinstance(result, list) assert 'meta' in result assert 'scheduling' in result assert 'cycle point format' in result def test_update_cylc_lang_new_section(tmp_path): test_file = tmp_path / 'testfile' test_file.write_text(( "\n" "some stuff" "" )) syntax.update_cylc_lang( ['gamma', 'nu'], test_file, '#kword#{word}#kword# - ' ) assert test_file.read_text() == ( "\n#kword#gamma#kword#" " - #kword#nu#kword# - " "" ) cylc-flow-8.6.4/tests/unit/test_task_id.py0000664000175000017500000000463215202510242020762 0ustar alastairalastair# 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 . import unittest from cylc.flow.task_id import TaskID class TestTaskId(unittest.TestCase): def test_get(self): self.assertEqual("a.1", TaskID.get("a", 1)) self.assertEqual("a._1", TaskID.get("a", "_1")) self.assertEqual( "WTASK.20101010T101010", TaskID.get("WTASK", "20101010T101010")) def test_split(self): self.assertEqual(["a", '1'], TaskID.split("a.1")) self.assertEqual(["a", '_1'], TaskID.split("a._1")) self.assertEqual( ["WTAS", '20101010T101010'], TaskID.split("WTAS.20101010T101010")) def test_is_valid_name(self): for name in [ "abc", "123", "____", "_", "a_b", "a_1", "1_b", "ABC" ]: self.assertTrue(TaskID.is_valid_name(name)) for name in [ "a.1", None, "%abc", "", " " ]: self.assertFalse(TaskID.is_valid_name(name)) def test_is_valid_id(self): for id1 in [ "a.1", "_.098098439535$#%#@!#~" ]: self.assertTrue(TaskID.is_valid_id(id1)) for id2 in [ "abc", "123", "____", "_", "a_b", "a_1", "1_b", "ABC", "a.A A" ]: self.assertFalse(TaskID.is_valid_id(id2)) def test_is_valid_id_2(self): # TBD: a.A A is invalid for valid_id, but valid for valid_id_2? # TBD: a/a.a is OK? for id1 in [ "a.1", "_.098098439535$#%#@!#~", "a/1", "_/098098439535$#%#@!#~", "a.A A", "a/a.a" ]: self.assertTrue(TaskID.is_valid_id_2(id1)) for id2 in [ "abc", "123", "____", "_", "a_b", "a_1", "1_b", "ABC" ]: self.assertFalse(TaskID.is_valid_id_2(id2)) cylc-flow-8.6.4/tests/unit/test_remote.py0000664000175000017500000000724115202510242020636 0ustar alastairalastair# 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 . """Test the cylc.flow.remote module.""" import os import pytest from cylc.flow.remote import ( run_cmd, construct_rsync_over_ssh_cmd, construct_ssh_cmd ) import cylc.flow def test_run_cmd_stdin_str(): """Test passing stdin as a string.""" proc = run_cmd( ['sed', 's/foo/bar/'], stdin_str='1foo2', capture_process=True ) assert [s.strip() for s in proc.communicate()] == [ '1bar2', '' ] def test_run_cmd_stdin_file(tmp_path): """Test passing stdin as a file.""" tmp_path = tmp_path / 'stdin' with tmp_path.open('w+') as tmp_file: tmp_file.write('1foo2') tmp_file = tmp_path.open('rb') proc = run_cmd( ['sed', 's/foo/bar/'], stdin=tmp_file, capture_process=True ) assert [s.strip() for s in proc.communicate()] == [ '1bar2', '' ] def test_construct_rsync_over_ssh_cmd(): """Function against known good output. """ cmd, host = construct_rsync_over_ssh_cmd( '/foo', '/bar', { 'rsync command': 'rsync command', 'hosts': ['miklegard'], 'ssh command': 'strange_ssh', 'selection': {'method': 'definition order'}, 'name': 'testplat' } ) assert host == 'miklegard' assert cmd == [ 'rsync', 'command', '--delete', '--rsh=strange_ssh', '--include=/.service/', '--include=/.service/server.key', '-a', '--checksum', '--out-format=%o %n%L', '--no-t', '--exclude=/log', '--exclude=/share', '--exclude=/work', '--include=/ana/***', '--include=/app/***', '--include=/bin/***', '--include=/etc/***', '--include=/lib/***', '--exclude=*', '/foo/', 'miklegard:/bar/', ] def test_construct_ssh_cmd_forward_env(monkeypatch: pytest.MonkeyPatch): """ Test for 'ssh forward environment variables' """ # Clear CYLC_* env vars as these will show up in the command for env_var in os.environ: if env_var.startswith('CYLC'): monkeypatch.delenv(env_var) host = 'example.com' config = { 'ssh command': 'ssh', 'use login shell': None, 'cylc path': None, 'ssh forward environment variables': ['FOO', 'BAZ'], } # Variable isn't set, no change to command expect = [ 'ssh', host, 'env', f'CYLC_VERSION={cylc.flow.__version__}', 'cylc', 'play', ] cmd = construct_ssh_cmd(['play'], config, host) assert cmd == expect # Variable is set, appears in `env` list monkeypatch.setenv('FOO', 'BAR') expect = [ 'ssh', host, 'env', f'CYLC_VERSION={cylc.flow.__version__}', 'FOO=BAR', 'cylc', 'play', ] cmd = construct_ssh_cmd(['play'], config, host) assert cmd == expect cylc-flow-8.6.4/tests/unit/test_task_message.py0000664000175000017500000000257015202510242022011 0ustar alastairalastair# 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 . from socket import gaierror import pytest from cylc.flow.task_message import send_messages def test_send_messages_err( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture ): """If an error occurs while initializing the client, it should be printed. """ exc_msg = 'Relic malfunction detected' def mock_get_client(*a, **k): raise gaierror(-2, exc_msg) monkeypatch.setattr('cylc.flow.task_message.get_client', mock_get_client) send_messages( 'arasaka', '1/v/01', [['INFO', 'silverhand']], '2077-01-01T00:00:00Z' ) assert f"gaierror: [Errno -2] {exc_msg}" in capsys.readouterr().err cylc-flow-8.6.4/tests/unit/test_command_validation.py0000664000175000017500000000662515202510242023200 0ustar alastairalastair# 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 . import re import pytest from cylc.flow.command_validation import ( ERR_OPT_FLOW_COMBINE, ERR_OPT_FLOW_VAL_INT_NEW_NONE, flow_opts, is_tasks, ) from cylc.flow.cycling.loader import ISO8601_CYCLING_TYPE from cylc.flow.exceptions import InputError from cylc.flow.flow_mgr import FLOW_NEW, FLOW_NONE from cylc.flow.id import TaskTokens @pytest.mark.parametrize('flow_strs, expected_msg', [ ([FLOW_NEW, '1'], ERR_OPT_FLOW_COMBINE.format(FLOW_NEW)), ([FLOW_NONE, '1'], ERR_OPT_FLOW_COMBINE.format(FLOW_NONE)), ([FLOW_NONE, FLOW_NEW], ERR_OPT_FLOW_COMBINE.format(FLOW_NONE)), (['a'], ERR_OPT_FLOW_VAL_INT_NEW_NONE), (['1', 'a'], ERR_OPT_FLOW_VAL_INT_NEW_NONE), ]) async def test_trigger_invalid(flow_strs, expected_msg): """Ensure invalid flow values are rejected during command validation.""" with pytest.raises(InputError) as exc_info: flow_opts(flow_strs, False) assert str(exc_info.value) == expected_msg async def test_is_tasks(set_cycling_type): set_cycling_type(ISO8601_CYCLING_TYPE) # tokens should be parsed assert is_tasks({'20000101T0000Z/a', '20010101T0000Z/b'}) == { TaskTokens('20000101T0000Z', 'a'), TaskTokens('20010101T0000Z', 'b'), } # cycle points should be standardised unless they are globs assert is_tasks({'2000/a', '20010101/b', '*/c', '[23]000/d'}) == { TaskTokens('20000101T0000Z', 'a'), TaskTokens('20010101T0000Z', 'b'), TaskTokens('*', 'c'), TaskTokens('[23]000', 'd'), } # the namespace should default to "root" unless provided assert is_tasks({'*', '2000'}) == { TaskTokens('*', 'root'), TaskTokens('20000101T0000Z', 'root'), } # invalid IDs result in errors with pytest.raises(InputError, match='Invalid ID: //, ///, ////'): is_tasks({'//', '///', '////', '2000/a'}) # last ID is valid # invalid cycle points result in errors with pytest.raises( InputError, match=re.escape('Invalid cycle point: (42)/answer, 2000Z, abc'), ): is_tasks({'(42)/answer', '2000Z', 'abc', '2000/a'}) # last ID is valid # job IDs reuslt in errors with pytest.raises( InputError, match=re.escape('This command does not take job IDs: */b/02, 1/a/01'), ): is_tasks({'1/a/01', '*/b/02'}) # combinations of errors are reported with pytest.raises( InputError, match=( re.escape('Invalid ID: ///, ////') + re.escape('\nInvalid cycle point: 200Z, abc') + re.escape('\nThis command does not take job IDs: */b/02, 1/a/01') ), ): is_tasks({'///', '////', '200Z', 'abc', '1/a/01', '*/b/02'}) cylc-flow-8.6.4/tests/unit/test_wallclock.py0000664000175000017500000001015715202510242021316 0ustar alastairalastair# 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 . from datetime import datetime import pytest from pytest import param from metomi.isodatetime.data import CALENDAR from cylc.flow.wallclock import ( get_current_time_string, get_time_string, get_unix_time_from_time_string, ) @pytest.mark.parametrize( 'time_str,time_sec', [ ('2016-09-08T09:09:00+01', 1473322140), ('2016-09-08T08:09:00Z', 1473322140), ('2016-09-07T20:09:00-12', 1473322140), ] ) def test_get_unix_time_from_time_string(time_str, time_sec): assert get_unix_time_from_time_string(time_str) == time_sec @pytest.mark.parametrize( 'time_str,time_sec', [ ('2016-09-08T09:09:00+01', 1473322140), ('2016-09-08T08:09:00Z', 1473322140), ('2016-09-07T20:09:00-12', 1473322140), ('2016-08-31T18:09:00+01', 1472663340), ] ) def test_get_unix_time_from_time_string_360(time_str, time_sec): mode = CALENDAR.mode CALENDAR.set_mode(CALENDAR.MODE_360) try: assert get_unix_time_from_time_string(time_str) == time_sec finally: CALENDAR.set_mode(mode) @pytest.mark.parametrize( 'value,error', [ (None, TypeError), (42, TypeError) ] ) def test_get_unix_time_from_time_string_error(value, error): with pytest.raises(error): get_unix_time_from_time_string(value) def test_get_current_time_string(set_timezone): """It reacts to local time zone changes. https://github.com/cylc/cylc-flow/issues/6701 """ set_timezone() res = get_current_time_string() assert res[-6:] == '+19:17' @pytest.mark.parametrize( 'arg, kwargs, expect', ( param( datetime(2000, 12, 13, 15, 30, 12, 123456), {}, '2000-12-14T10:47:12+19:17', id='good', ), param( datetime(2000, 12, 13, 15, 30, 12, 123456), {'date_time_is_local': True}, '2000-12-13T15:30:12+19:17', id='dt_is_local', ), param( datetime(2000, 12, 13, 15, 30, 12, 123456), { 'custom_time_zone_info': { 'hours': 0, 'minutes': -20, 'string_basic': 'XXX+00:20', }, 'use_basic_format': True }, '20001213T151012XXX+00:20', id='custom_time_zone_info_string_basic', ), param( datetime(2000, 12, 13, 15, 30, 12, 123456), { 'custom_time_zone_info': { 'hours': 0, 'minutes': -20, 'string_extended': ':UK/Exeter', }, 'use_basic_format': False }, '2000-12-13T15:10:12:UK/Exeter', id='custom_time_zone_info_string_extended', ), param( datetime(2000, 12, 13, 15, 30, 12, 123456), { 'custom_time_zone_info': { 'hours': 0, 'minutes': -20, 'string_extended': ':UK/Exeter', }, 'use_basic_format': False, 'date_time_is_local': True, }, '2000-12-12T19:53:12:UK/Exeter', id='date_time_is_local', ), ), ) def test_get_time_string(set_timezone, arg, kwargs, expect): set_timezone() assert get_time_string(arg, **kwargs) == expect cylc-flow-8.6.4/tests/unit/test_job_file.py0000664000175000017500000004606715202510242021125 0ustar alastairalastair# 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 . # # Tests for functions contained in cylc.flow.job_file. # TODO remove the unittest dependency - it should not be necessary. from contextlib import suppress import io import os from pathlib import Path import pytest from tempfile import NamedTemporaryFile from textwrap import dedent from cylc.flow import ( __version__, __file__ as cylc_flow_file, ) from cylc.flow.job_file import ( JobFileWriter, ) from cylc.flow.platforms import platform_from_name @pytest.mark.parametrize( 'in_value, out_value', [('~foo/bar bar', '~foo/"bar bar"'), ('~/bar bar', '~/"bar bar"'), ('~/a', '~/"a"'), ('test', '"test"'), ('~', '~'), ('~a', '~a'), ('foo%s', '"foo%s"'), ('foo%(i)d', '"foo3"')] ) def test_get_variable_value_definition(in_value, out_value): """Test the value for single/tilde variables are correctly quoted, and parameter environment templates are handled""" param_dict = {'i': 3} res = JobFileWriter._get_variable_value_definition(in_value, param_dict) assert out_value == res @pytest.fixture def fixture_get_platform(): """ Allows pytest to cache default platform dictionary. Args: custom_settings (dict): settings that you wish to override. Returns: platforms dictionary. """ def inner_func(custom_settings=None): platform = platform_from_name() if custom_settings is not None: platform.update(custom_settings) return platform yield inner_func def test_write(fixture_get_platform): """Test write function outputs jobscript file correctly.""" with NamedTemporaryFile() as local_job_file_path: local_job_file_path = local_job_file_path.name platform = fixture_get_platform( { "job runner command template": "woof", } ) job_conf = { "platform": platform, "task_id": "1/baa", "workflow_name": "farm_noises", "work_d": "farm_noises/work_d", "uuid_str": "neigh", 'environment': {'cow': '~/moo', 'sheep': '~baa/baa', 'duck': '~quack'}, "job_d": "1/baa/01", "try_num": 1, "flow_nums": {1}, # "job_runner_name": "background", "param_var": {"duck": "quack", "mouse": "squeak"}, "execution_time_limit": "moo", "namespace_hierarchy": ["root", "baa", "moo"], "dependencies": ['moo', 'neigh', 'quack'], "init-script": "This is the init script", "env-script": "This is the env script", "err-script": "This is the err script", "pre-script": "This is the pre script", "script": "This is the script", "post-script": "This is the post script", "exit-script": "This is the exit script", } JobFileWriter().write(local_job_file_path, job_conf) assert (os.path.exists(local_job_file_path)) size_of_file = os.stat(local_job_file_path).st_size # This test only needs to check that the file is created and is # non-empty as each section is covered by individual unit tests. assert size_of_file > 10 """Test the header is correctly written""" expected = ('#!/bin/bash -l\n#\n# ++++ THIS IS A CYLC JOB SCRIPT ' '++++\n# Workflow: farm_noises\n# Task: 1/baa\n# Job ' 'log directory: 1/baa/01\n# Job runner: ' 'background\n# Job runner command template: woof\n#' ' Execution time limit: moo') platform = fixture_get_platform( {"job runner command template": "woof"} ) job_conf = { "platform": platform, "job runner": "background", "execution_time_limit": "moo", "workflow_name": "farm_noises", "task_id": "1/baa", "job_d": "1/baa/01" } with io.StringIO() as fake_file: JobFileWriter()._write_header(fake_file, job_conf) assert fake_file.getvalue() == expected @pytest.mark.parametrize( 'job_conf,expected', [ ( # basic { "platform": { "job runner": "loadleveler", "job runner command template": "test_workflow", }, "directives": {"moo": "foo", "cluck": "bar"}, "workflow_name": "farm_noises", "task_id": "1/baa", "job_d": "1/test_task_id/01", "job_file_path": "directory/job", "execution_time_limit": 60 }, ('\n\n# DIRECTIVES:\n# @ job_name = farm_noises.baa.1' '\n# @ output = directory/job.out\n# @ error = directory/' 'job.err\n# @ wall_clock_limit = 120,60\n# @ moo = foo' '\n# @ cluck = bar\n# @ queue') ), ( # Check no directives is correctly written { "platform": { "job runner": "slurm", "job runner command template": "test_workflow" }, "directives": {}, "workflow_name": "farm_noises", "task_id": "1/baa", "job_d": "1/test_task_id/01", "job_file_path": "directory/job", "execution_time_limit": 60 }, ( '\n\n# DIRECTIVES:\n#SBATCH ' '--job-name=baa.1.farm_noises\n#SBATCH ' '--output=directory/job.out\n#SBATCH --error=directory/' 'job.err\n#SBATCH --time=1:00' ) ), ( # Check pbs max job name length { "platform": { "job runner": "pbs", "job runner command template": "test_workflow", "job name length maximum": 15 }, "directives": {}, "workflow_name": "farm_noises", "task_id": "1/baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "job_d": "1/test_task_id/01", "job_file_path": "directory/job", "execution_time_limit": 60 }, ('\n\n# DIRECTIVES:\n#PBS -N baaaaaaaaaaaaaa\n#PBS -o ' 'directory/job.out\n#PBS -e directory/job.err\n#PBS -l ' 'walltime=60') ), ( # Check sge directives are correctly written { "platform": { "job runner": "sge", "job runner command template": "test_workflow", }, "directives": {"-V": "", "-q": "queuename", "-l": "s_vmem=1G,s_cpu=60"}, "workflow_name": "farm_noises", "task_id": "1/baa", "job_d": "1/test_task_id/01", "job_file_path": "$HOME/directory/job", "execution_time_limit": 1000 }, ('\n\n# DIRECTIVES:\n#$ -N farm_noises.baa.1\n#$ -o directory/' 'job.out\n#$ -e directory/job.err\n#$ -l h_rt=0:16:40\n#$ -V\n#' '$ -q queuename\n#$ -l s_vmem=1G,s_cpu=60' ) ) ], ids=["1", "2", "3", "4"]) def test_write_directives(fixture_get_platform, job_conf: dict, expected: str): """"Test the directives section of job script file is correctly written""" with io.StringIO() as fake_file: JobFileWriter()._write_directives(fake_file, job_conf) assert fake_file.getvalue() == expected @pytest.mark.parametrize( "job_runner", ["at", "background", "loadleveler", "pbs", "sge", "slurm"]) def test_traps_for_each_job_runner(job_runner: str): """Test traps for each job runner""" platform = platform_from_name() platform.update({ "job runner": f"{job_runner}", }) job_conf = { "platform": platform, "directives": {}, "workflow_name": 'test_traps_for_each_job_runner', } with io.StringIO() as fake_file: JobFileWriter()._write_prelude(fake_file, job_conf) output = fake_file.getvalue() assert "CYLC_FAIL_SIGNALS='EXIT ERR TERM XCPU" in output @pytest.mark.parametrize( 'set_CYLC_ENV_NAME', [ pytest.param(True, id='CYLC_ENV_NAME=True'), pytest.param(False, id='CYLC_ENV_NAME=False'), ] ) def test_write_prelude(monkeypatch, fixture_get_platform, set_CYLC_ENV_NAME): """Test the prelude section of job script file is correctly written. """ if set_CYLC_ENV_NAME: monkeypatch.setenv('CYLC_ENV_NAME', 'myenv') else: with suppress(KeyError): monkeypatch.delenv('CYLC_ENV_NAME') monkeypatch.setattr('cylc.flow.flags.verbosity', 2) expected = ('\nCYLC_FAIL_SIGNALS=\'EXIT ERR TERM XCPU\'\n' 'CYLC_VACATION_SIGNALS=\'USR1\'\nexport PATH=moo/baa:$PATH' '\nexport CYLC_VERBOSE=true' '\nexport CYLC_DEBUG=true' f'\nexport CYLC_VERSION=\'{__version__}\'') if set_CYLC_ENV_NAME: expected += '\nexport CYLC_ENV_NAME=\'myenv\'' expected += '\nexport CYLC_WORKFLOW_ID="test_write_prelude"' expected += '\nexport CYLC_WORKFLOW_INITIAL_CYCLE_POINT=\'20200101T0000Z\'' job_conf = { "workflow_name": "test_write_prelude", "platform": fixture_get_platform({ "job runner": "loadleveler", "job runner command template": "test_workflow", "host": "localhost", "copyable environment variables": [ "CYLC_WORKFLOW_INITIAL_CYCLE_POINT" ], "cylc path": "moo/baa" }), "directives": {"restart": "yes"}, } monkeypatch.setenv("CYLC_WORKFLOW_INITIAL_CYCLE_POINT", "20200101T0000Z") monkeypatch.setenv("CYLC_WORKFLOW_NAME", "test_write_prelude") monkeypatch.setenv("CYLC_WORKFLOW_NAME_BASE", "test_write_prelude") with io.StringIO() as fake_file: # copyable environment variables JobFileWriter()._write_prelude(fake_file, job_conf) assert fake_file.getvalue() == expected def test_write_workflow_environment(fixture_get_platform, monkeypatch): """Test workflow environment is correctly written in jobscript""" # set some workflow environment conditions monkeypatch.setattr('cylc.flow.flags.verbosity', 2) workflow_env = {'CYLC_UTC': 'True', 'CYLC_CYCLING_MODE': 'integer', 'CYLC_WORKFLOW_NAME': 'blargh/quack', 'CYLC_WORKFLOW_NAME_BASE': 'quack'} job_file_writer = JobFileWriter() job_file_writer.set_workflow_env(workflow_env) # workflow env not correctly setting...check this expected = ('\n\ncylc__job__inst__cylc_env() {\n # CYLC WORKFLOW ' 'ENVIRONMENT:\n export CYLC_CYCLING_MODE="integer"\n ' ' export CYLC_UTC="True"' '\n export CYLC_WORKFLOW_NAME="blargh/quack"' '\n export CYLC_WORKFLOW_NAME_BASE="quack"' '\n export TZ="UTC"' '\n export CYLC_WORKFLOW_UUID="neigh"') job_conf = { "platform": fixture_get_platform({ "host": "localhost", }), "workflow_name": "farm_noises", "uuid_str": "neigh" } with io.StringIO() as fake_file: job_file_writer._write_workflow_environment(fake_file, job_conf) result = fake_file.getvalue() assert result == expected def test_write_script(): """Test script is correctly written in jobscript""" expected = ( "\n\ncylc__job__inst__init_script() {\n# INIT-SCRIPT:\n" "This is the init script\n}\n\ncylc__job__inst__env_script()" " {\n# ENV-SCRIPT:\nThis is the env script\n}\n\n" "cylc__job__inst__err_script() {\n# ERR-SCRIPT:\nThis is the err " "script\n}\n\ncylc__job__inst__pre_script() {\n# PRE-SCRIPT:\n" "This is the pre script\n}\n\ncylc__job__inst__script() {\n" "# SCRIPT:\nThis is the script\n}\n\ncylc__job__inst__post_script" "() {\n# POST-SCRIPT:\nThis is the post script\n}\n\n" "cylc__job__inst__exit_script() {\n# EXIT-SCRIPT:\n" "This is the exit script\n}") job_conf = { "init-script": "This is the init script", "env-script": "This is the env script", "err-script": "This is the err script", "pre-script": "This is the pre script", "script": "This is the script", "post-script": "This is the post script", "exit-script": "This is the exit script", "workflow_name": "test_write_script", } with io.StringIO() as fake_file: JobFileWriter()._write_script(fake_file, job_conf) assert fake_file.getvalue() == expected def test_no_script_section_with_comment_only_script(): """Test jobfilewriter does not generate script section when script is comment only""" expected = ("") job_conf = { "init-script": "", "env-script": "", "err-script": "", "pre-script": "#This is the pre script/n #moo /n#baa", "script": "", "post-script": "", "exit-script": "", "workflow_name": "test_no_script_section_with_comment_only_script" } with io.StringIO() as fake_file: JobFileWriter()._write_script(fake_file, job_conf) blah = fake_file.getvalue() print(blah) assert fake_file.getvalue() == expected def test_write_task_environment(): """Test task environment is correctly written in jobscript""" # set some task environment conditions expected = ('\n\n # CYLC TASK ENVIRONMENT:\n ' 'export CYLC_TASK_COMMS_METHOD=ssh\n ' 'export CYLC_TASK_JOB="1/moo/01"\n export ' 'CYLC_TASK_NAMESPACE_HIERARCHY="baa moo"\n export ' 'CYLC_TASK_TRY_NUMBER=1\n export ' 'CYLC_TASK_FLOW_NUMBERS=1\n export ' 'CYLC_TASK_PARAM_duck="quack"\n export ' 'CYLC_TASK_PARAM_mouse="squeak"\n ' 'CYLC_TASK_WORK_DIR_BASE=\'farm_noises/work_d\'\n}') job_conf = { "platform": {'communication method': 'ssh'}, "job_d": "1/moo/01", "namespace_hierarchy": ["baa", "moo"], "dependencies": ['moo', 'neigh', 'quack'], "try_num": 1, "flow_nums": {1}, "param_var": {"duck": "quack", "mouse": "squeak"}, "work_d": "farm_noises/work_d", "workflow_name": "test_write_task_environment", } with io.StringIO() as fake_file: JobFileWriter()._write_task_environment(fake_file, job_conf) assert fake_file.getvalue() == expected def test_write_runtime_environment(): """Test runtime environment is correctly written in jobscript""" expected = ( '\n\ncylc__job__inst__user_env() {\n # TASK RUNTIME ' 'ENVIRONMENT:\n export cow sheep duck\n' ' cow=~/"moo"\n sheep=~baa/"baa"\n ' 'duck=~quack\n}') job_conf = { 'environment': { 'cow': '~/moo', 'sheep': '~baa/baa', 'duck': '~quack' }, "workflow_name": "test_write_runtime_environment", } with io.StringIO() as fake_file: JobFileWriter()._write_runtime_environment(fake_file, job_conf) assert fake_file.getvalue() == expected def test_write_epilogue(): """Test epilogue is correctly written in jobscript""" expected = '\n' + dedent(''' CYLC_RUN_DIR="${CYLC_RUN_DIR:-$HOME/cylc-run}" . "${CYLC_RUN_DIR}/${CYLC_WORKFLOW_ID}/.service/etc/job.sh" cylc__job__main #EOF: 1/moo/01 ''') job_conf = {'job_d': "1/moo/01"} with io.StringIO() as fake_file: JobFileWriter()._write_epilogue(fake_file, job_conf) assert fake_file.getvalue() == expected def test_write_global_init_scripts(fixture_get_platform): """Test global init script is correctly written in jobscript""" job_conf = { "platform": fixture_get_platform({ "global init-script": ( 'export COW=moo\n' 'export PIG=oink\n' 'export DONKEY=HEEHAW\n' ) }) } expected = '\n' + dedent(''' # GLOBAL INIT-SCRIPT: export COW=moo export PIG=oink export DONKEY=HEEHAW ''') with io.StringIO() as fake_file: JobFileWriter()._write_global_init_script(fake_file, job_conf) assert fake_file.getvalue() == expected def test_homeless_platform(fixture_get_platform): """Ensure there are no uses of $HOME before the global init-script. This is to allow users to configure a $HOME on machines with no $HOME directory. """ job_conf = { "platform": fixture_get_platform({ 'global init-script': 'some-script' }), "task_id": "1/a", "workflow_name": "b", "work_d": "c/d", "uuid_str": "e", 'environment': {}, 'cow': '~/moo', "job_d": "1/a/01", "try_num": 1, "flow_nums": {1}, # "job_runner_name": "background", "param_var": {}, "execution_time_limit": None, "namespace_hierarchy": [], "dependencies": [], "init-script": "", "env-script": "", "err-script": "", "pre-script": "", "script": "", "post-script": "", "exit-script": "", } with NamedTemporaryFile() as local_job_file_path: local_job_file_path = local_job_file_path.name JobFileWriter().write(local_job_file_path, job_conf) with open(local_job_file_path, 'r') as local_job_file: job_script = local_job_file.read() # ensure that $HOME is not used before the global init-script for line in job_script.splitlines(): if line.startswith(' '): # ignore env/script functions which aren't run until later continue if line == '# GLOBAL INIT-SCRIPT:': # quit once we've hit the global init-script break if 'HOME' in line: # bail if $HOME is used raise Exception(f'$HOME found in {line}\n{job_script}') # also ensure there is no use of $HOME in the job.sh script with open(Path(cylc_flow_file).parent / 'etc/job.sh', 'r') as job_sh: job_sh_txt = job_sh.read() if 'HOME' in job_sh_txt: raise Exception('$HOME found in job.sh\n{job_sh_txt}') cylc-flow-8.6.4/tests/unit/run_modes/0000775000175000017500000000000015202510242017721 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/run_modes/__init__.py0000664000175000017500000000000015202510242022020 0ustar alastairalastaircylc-flow-8.6.4/tests/unit/run_modes/test_skip_units.py0000664000175000017500000001101015202510242023513 0ustar alastairalastair# 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 . """Unit tests for utilities supporting skip modes """ import logging import pytest from pytest import param, raises from types import SimpleNamespace from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.run_modes.skip import ( check_task_skip_config, process_outputs, skip_mode_validate, ) @pytest.mark.parametrize( 'conf', ( param({}, id='no-skip-config'), param({'skip': {'outputs': []}}, id='no-skip-outputs'), param({'skip': {'outputs': ['foo1', 'failed']}}, id='ok-skip-outputs'), ) ) def test_good_check_task_skip_config(conf): """It returns none if the problems this function checks are not present. """ tdef = SimpleNamespace(rtconfig=conf) tdef.name = 'foo' assert check_task_skip_config(tdef) is None def test_raises_check_task_skip_config(): """It raises an error if succeeded and failed are set. """ tdef = SimpleNamespace( rtconfig={'skip': {'outputs': ['foo1', 'failed', 'succeeded']}} ) tdef.name = 'foo' with raises(WorkflowConfigError, match='succeeded AND failed'): check_task_skip_config(tdef) @pytest.mark.parametrize( 'outputs, required, expect', ( param([], [], ['succeeded'], id='implicit-succeded'), param( ['succeeded'], ['succeeded'], ['succeeded'], id='explicit-succeded' ), param(['submitted'], [], ['succeeded'], id='only-1-submit'), param( ['foo', 'bar', 'baz', 'qux'], ['bar', 'qux'], ['bar', 'qux', 'succeeded'], id='required-only' ), param( ['foo', 'baz'], ['bar', 'qux'], ['succeeded'], id='no-required' ), param( ['failed'], [], ['failed'], id='explicit-failed' ), ) ) def test_process_outputs(outputs, required, expect): """Check that skip outputs: 1. Doesn't send submitted twice. 2. Sends every required output. 3. If failed is set send failed 4. If failed in not set send succeeded. """ # Create a mocked up task-proxy: rtconf = {'skip': {'outputs': outputs}} itask = SimpleNamespace( tdef=SimpleNamespace(rtconfig=rtconf), state=SimpleNamespace( outputs=SimpleNamespace( iter_required_messages=lambda *a, **k: iter(required), _message_to_trigger={v: v for v in required}, ) ), ) assert process_outputs(itask, rtconf) == {'submitted', 'started'}.union( expect ) def test_skip_mode_validate(caplog, log_filter): """It logs a message if we've set a task config to nonlive mode. (And not otherwise) Point 3 from the skip mode proposal https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md | If the run mode is set to simulation or skip in the workflow | configuration, then cylc validate and cylc lint should produce | warning (similar to development features in other languages / systems). Edit: Warning demoted to info to prevent the orange warning triangles popping up in the GUI every time a workflow with configured skip tasks is started. See: https://github.com/cylc/cylc-flow/pull/6854 """ caplog.set_level(logging.INFO) taskdefs = { f'{run_mode}_task': SimpleNamespace( rtconfig={'run mode': run_mode}, name=f'{run_mode}_task' ) for run_mode in ['live', 'skip'] } skip_mode_validate(taskdefs) assert len(caplog.records) == 1 assert log_filter( level=logging.INFO, exact_match=( "The following tasks are set to run in skip mode:\n" " * skip_task" ), log=caplog ) cylc-flow-8.6.4/tests/unit/run_modes/test_simulation_units.py0000664000175000017500000001217715202510242024750 0ustar alastairalastair# 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 . """Tests for utilities supporting simulation and skip modes """ import pytest from pytest import param from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.run_modes.simulation import ( disable_platforms, get_simulated_run_len, parse_fail_cycle_points, sim_task_failed, ) @pytest.mark.parametrize( 'execution_time_limit, speedup_factor, default_run_length', ( param(None, None, 'PT1H', id='default-run-length'), param(None, 10, 'PT1H', id='speedup-factor-alone'), param('PT1H', None, 'PT1H', id='execution-time-limit-alone'), param('P1D', 24, 'PT1M', id='speed-up-and-execution-tl'), ) ) def test_get_simulated_run_len( execution_time_limit, speedup_factor, default_run_length ): """Test the logic of the presence or absence of config items. Avoid testing the correct workign of DurationParser. """ rtc = { 'execution time limit': execution_time_limit, 'simulation': { 'speedup factor': speedup_factor, 'default run length': default_run_length, 'time limit buffer': 'PT0S', } } assert get_simulated_run_len(rtc) == 3600 @pytest.mark.parametrize( 'rtc, expect', ( ({'platform': 'skarloey'}, 'localhost'), ({'remote': {'host': 'rheneas'}}, 'localhost'), ({'job': {'batch system': 'loaf'}}, 'localhost'), ) ) def test_disable_platforms(rtc, expect): """A sampling of items FORBIDDEN_WITH_PLATFORMS are removed from a config passed to this method. """ disable_platforms(rtc) assert rtc['platform'] == expect subdicts = [v for v in rtc.values() if isinstance(v, dict)] for subdict in subdicts: for k, val in subdict.items(): if k != 'platform': assert val is None @pytest.mark.parametrize( 'args, cycling, fallback', ( param((['2', '4'], ['']), 'integer', False, id='int.valid'), param((['garbage'], []), 'integer', True, id='int.invalid'), param((['20200101T0000Z'], []), 'iso8601', False, id='iso.valid'), param((['garbage'], []), 'iso8601', True, id='iso.invalid'), ), ) def test_parse_fail_cycle_points( caplog, set_cycling_type, args, cycling, fallback ): """Tests for parse_fail_cycle points. """ set_cycling_type(cycling) if fallback: expect = args[1] check_log = True else: expect = args[0] check_log = False if cycling == 'integer': assert parse_fail_cycle_points(*args) == [ IntegerPoint(i) for i in expect ] else: assert parse_fail_cycle_points(*args) == [ ISO8601Point(i) for i in expect ] if check_log: assert "Incompatible" in caplog.messages[0] assert cycling in caplog.messages[0].lower() @pytest.mark.parametrize( 'conf, point, try_, expect', ( param( {'fail cycle points': [], 'fail try 1 only': True}, ISO8601Point('1'), 1, False, id='defaults' ), param( {'fail cycle points': None, 'fail try 1 only': False}, ISO8601Point('1066'), 1, True, id='fail-all' ), param( { 'fail cycle points': [ ISO8601Point('1066'), ISO8601Point('1067')], 'fail try 1 only': False }, ISO8601Point('1067'), 1, True, id='point-in-failCP' ), param( { 'fail cycle points': [ ISO8601Point('1066'), ISO8601Point('1067')], 'fail try 1 only': True }, ISO8601Point('1000'), 1, False, id='point-notin-failCP' ), param( {'fail cycle points': None, 'fail try 1 only': True}, ISO8601Point('1066'), 2, False, id='succeed-attempt2' ), param( {'fail cycle points': None, 'fail try 1 only': False}, ISO8601Point('1066'), 7, True, id='fail-attempt7' ), ) ) def test_sim_task_failed( conf, point, try_, expect, set_cycling_type ): set_cycling_type('iso8601') assert sim_task_failed(conf, point, try_) == expect cylc-flow-8.6.4/tests/unit/run_modes/test_run_modes.py0000664000175000017500000000276615202510242023340 0ustar alastairalastair# 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 . """Tests for utilities supporting run modes. """ import pytest from cylc.flow.run_modes import RunMode def test_run_mode_desc(): """All run mode labels have descriptions.""" for mode in RunMode: assert mode.describe() def test_get_default_live(): """RunMode.get() => live""" assert RunMode.get({}) == RunMode.LIVE @pytest.mark.parametrize('str_', ('LIVE', 'Dummy', 'SkIp', 'siMuLATioN')) def test__missing_(str_): """The RunMode enumeration works when fed a string in the wrong case""" assert RunMode(str_).value == str_.lower() def test__missing_still_doesnt_work(): """ The RunMode enumeration raises an error when fed a string that is not a mode. """ with pytest.raises(ValueError): RunMode('garbage') cylc-flow-8.6.4/tests/unit/run_modes/test_dummy.py0000664000175000017500000000262315202510242022470 0ustar alastairalastair# 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 . """Tests for utilities supporting dummy mode. """ import pytest from cylc.flow.run_modes.dummy import build_dummy_script @pytest.mark.parametrize( 'fail_one_time_only', (True, False) ) def test_build_dummy_script(fail_one_time_only): rtc = { 'outputs': {'foo': '1', 'bar': '2'}, 'simulation': { 'fail try 1 only': fail_one_time_only, 'fail cycle points': '1', } } result = build_dummy_script(rtc, 60) assert result.split('\n') == [ 'sleep 60', "cylc message '1'", "cylc message '2'", f"cylc__job__dummy_result {str(fail_one_time_only).lower()}" " 1 || exit 1" ] cylc-flow-8.6.4/tests/unit/filetree.py0000664000175000017500000001460315202510242020103 0ustar alastairalastair# 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 . """Utilities for testing workflow directory structure. (There should be no tests in this module.) A filetree is represented by a dict like so: { # Dirs are represented by dicts (which are also sub-filetrees): 'dir': { 'another-dir': { # Files are represented by None: 'file.txt': None } }, # Symlinks are represented by the Symlink class, with the target # represented by the relative path from the tmp_path directory: 'symlink': Symlink('dir/another-dir') } """ from pathlib import Path, PosixPath from typing import Any, Dict, List class Symlink(PosixPath): """A class to represent a symlink target.""" ... def create_filetree( filetree: Dict[str, Any], location: Path, root: Path ) -> None: """Create the directory structure represented by the filetree dict. Args: filetree: The filetree to create. location: The absolute path in which to create the filetree. root: The top-level dir from which relative symlink targets are located (typically tmp_path). """ for name, entry in filetree.items(): path = location / name if isinstance(entry, dict): path.mkdir(exist_ok=True) create_filetree(entry, path, root) elif isinstance(entry, Symlink): path.symlink_to(root / entry) else: path.touch() def get_filetree_as_list( filetree: Dict[str, Any], location: Path ) -> List[str]: """Return a list of the paths in a filetree. Args: filetree: The filetree to listify. location: The absolute path to the filetree. """ ret: List[str] = [] for name, entry in filetree.items(): path = location / name ret.append(str(path)) if isinstance(entry, dict): ret.extend(get_filetree_as_list(entry, path)) return ret FILETREE_1 = { 'cylc-run': { 'foo': { 'bar': { '.service': { 'db': None, }, 'flow.cylc': None, 'log': Symlink('sym/cylc-run/foo/bar/log'), 'mirkwood': Symlink('you-shall-not-pass/mirkwood'), 'rincewind.txt': Symlink('you-shall-not-pass/rincewind.txt'), }, }, }, 'sym': { 'cylc-run': { 'foo': { 'bar': { 'log': { 'darmok': Symlink('you-shall-not-pass/darmok'), 'temba.txt': Symlink('you-shall-not-pass/temba.txt'), 'bib': { 'fortuna.txt': None, }, }, }, }, }, }, 'you-shall-not-pass': { # Nothing in here should get deleted 'darmok': { 'jalad.txt': None, }, 'mirkwood': { 'spiders.txt': None, }, 'rincewind.txt': None, 'temba.txt': None, }, } FILETREE_2 = { 'cylc-run': {'foo': {'bar': Symlink('sym-run/cylc-run/foo/bar')}}, 'sym-run': { 'cylc-run': { 'foo': { 'bar': { '.service': { 'db': None, }, 'flow.cylc': None, 'share': Symlink('sym-share/cylc-run/foo/bar/share'), }, }, }, }, 'sym-share': { 'cylc-run': { 'foo': { 'bar': { 'share': { 'cycle': Symlink( 'sym-cycle/cylc-run/foo/bar/share/cycle' ), }, }, }, }, }, 'sym-cycle': { 'cylc-run': { 'foo': { 'bar': { 'share': { 'cycle': { 'macklunkey.txt': None, }, }, }, }, }, }, 'you-shall-not-pass': {}, } FILETREE_3 = { 'cylc-run': { 'foo': { 'bar': Symlink('sym-run/cylc-run/foo/bar'), }, }, 'sym-run': { 'cylc-run': { 'foo': { 'bar': { '.service': { 'db': None, }, 'flow.cylc': None, 'share': { 'cycle': Symlink( 'sym-cycle/cylc-run/foo/bar/share/cycle' ), }, }, }, }, }, 'sym-cycle': { 'cylc-run': { 'foo': { 'bar': { 'share': { 'cycle': { 'sokath.txt': None, }, }, }, }, }, }, 'you-shall-not-pass': {}, } FILETREE_4 = { 'cylc-run': { 'foo': { 'bar': { '.service': { 'db': None, }, 'flow.cylc': None, 'share': { 'cycle': Symlink('sym-cycle/cylc-run/foo/bar/share/cycle'), }, }, }, }, 'sym-cycle': { 'cylc-run': { 'foo': { 'bar': { 'share': { 'cycle': { 'kiazi.txt': None, }, }, }, }, }, }, 'you-shall-not-pass': {}, } cylc-flow-8.6.4/tests/unit/test_option_parsers.py0000664000175000017500000004353715202510242022422 0ustar alastairalastair# 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 . from contextlib import redirect_stdout import io import sys from types import SimpleNamespace from typing import List import pytest from pytest import param import cylc.flow.flags from cylc.flow.option_parsers import ( CylcOptionParser as COP, Options, combine_options, combine_options_pair, OptionSettings, cleanup_sysargv, filter_sysargv ) USAGE_WITH_COMMENT = "usage \n # comment" ARGS = 'args' KWARGS = 'kwargs' SOURCES = 'sources' USEIF = 'useif' @pytest.fixture(scope='module') def parser(): return COP( USAGE_WITH_COMMENT, argdoc=[('SOME_ARG', "Description of SOME_ARG")] ) @pytest.mark.parametrize( 'args,verbosity', [ ([], 0), (['-v'], 1), (['-v', '-v', '-v'], 3), (['-q'], -1), (['-q', '-q', '-q'], -3), (['-q', '-v', '-q'], -1), (['--debug'], 2), (['--debug', '-q'], 1), (['--debug', '-v'], 3), ] ) def test_verbosity( args: List[str], verbosity: int, parser: COP, monkeypatch: pytest.MonkeyPatch ) -> None: """-v, -q, --debug should be additive.""" # patch the cylc.flow.flags value so that it gets reset after the test monkeypatch.setattr('cylc.flow.flags.verbosity', None) opts, args = parser.parse_args(['default-arg'] + args) assert opts.verbosity == verbosity # test side-effect, the verbosity flag should be set assert cylc.flow.flags.verbosity == verbosity def test_help_color(monkeypatch: pytest.MonkeyPatch, parser: COP): """Test for colorized comments in 'cylc cmd --help --color=always'.""" # This colorization is done on the fly when help is printed. monkeypatch.setattr("sys.argv", ['cmd', 'foo', '--color=always']) parser.parse_args(None) assert parser.values.color == "always" f = io.StringIO() with redirect_stdout(f): parser.print_help() assert not (f.getvalue()).startswith("Usage: " + USAGE_WITH_COMMENT) def test_help_nocolor(monkeypatch: pytest.MonkeyPatch, parser: COP): """Test for no colorization in 'cylc cmd --help --color=never'.""" # This colorization is done on the fly when help is printed. monkeypatch.setattr(sys, "argv", ['cmd', 'foo', '--color=never']) parser.parse_args(None) assert parser.values.color == "never" f = io.StringIO() with redirect_stdout(f): parser.print_help() assert (f.getvalue()).startswith("Usage: " + USAGE_WITH_COMMENT) def test_Options_std_opts(): """Test Python Options API with standard options.""" parser = COP(USAGE_WITH_COMMENT, auto_add=True) MyOptions = Options(parser) MyValues = MyOptions(verbosity=1) assert MyValues.verbosity == 1 # Add overlapping args tomorrow @pytest.mark.parametrize( 'first, second, expect', [ param( [{ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'do'}}], [{ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'dont'}}], ( [{ ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'do', 'dont'}, USEIF: '' }] ), id='identical arg lists unchanged' ), param( [{ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'fall'}}], [{ ARGS: ['-f', '--foolish'], KWARGS: {'help': 'not identical'}, SOURCES: {'fold'}}], ( [ { ARGS: ['--foo'], KWARGS: {}, SOURCES: {'fall'}, USEIF: '' }, { ARGS: ['--foolish'], KWARGS: {'help': 'not identical'}, SOURCES: {'fold'}, USEIF: '' } ] ), id='different arg lists lose shared names' ), param( [{ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'cook'}}], [{ ARGS: ['-f', '--foo'], KWARGS: {'help': 'not identical', 'dest': 'foobius'}, SOURCES: {'bake'}, USEIF: '' }], None, id='different args identical arg list cause exception' ), param( [{ARGS: ['-g', '--goo'], KWARGS: {}, SOURCES: {'knit'}}], [{ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'feed'}}], [ { ARGS: ['-g', '--goo'], KWARGS: {}, SOURCES: {'knit'}, USEIF: '' }, { ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'feed'}, USEIF: '' }, ], id='all unrelated args added' ), param( [ {ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'work'}}, {ARGS: ['-r', '--redesdale'], KWARGS: {}, SOURCES: {'work'}} ], [ {ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'sink'}}, { ARGS: ['-b', '--buttered-peas'], KWARGS: {}, SOURCES: {'sink'} } ], [ { ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'work', 'sink'}, USEIF: '' }, { ARGS: ['-b', '--buttered-peas'], KWARGS: {}, SOURCES: {'sink'}, USEIF: '' }, { ARGS: ['-r', '--redesdale'], KWARGS: {}, SOURCES: {'work'}, USEIF: '' }, ], id='do not repeat args' ), param( [ { ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'push'} }, ], [], [ { ARGS: ['-f', '--foo'], KWARGS: {}, SOURCES: {'push'}, USEIF: '' }, ], id='one empty list is fine' ) ] ) def test_combine_options_pair(first, second, expect): """It combines sets of options""" first = [ OptionSettings(i[ARGS], sources=i[SOURCES], **i[KWARGS]) for i in first ] second = [ OptionSettings(i[ARGS], sources=i[SOURCES], **i[KWARGS]) for i in second ] if expect is not None: result = combine_options_pair(first, second) assert [i.__dict__ for i in result] == expect else: with pytest.raises(Exception, match='Clashing Options'): combine_options_pair(first, second) @pytest.mark.parametrize( 'inputs, expect', [ param( [ ([OptionSettings( ['-i', '--inflammable'], help='', sources={'wish'} )]), ([OptionSettings( ['-f', '--flammable'], help='', sources={'rest'} )]), ([OptionSettings( ['-n', '--non-flammable'], help='', sources={'swim'} )]), ], [ {ARGS: ['-i', '--inflammable']}, {ARGS: ['-f', '--flammable']}, {ARGS: ['-n', '--non-flammable']} ], id='merge three argsets no overlap' ), param( [ [ OptionSettings( ['-m', '--morpeth'], help='', sources={'stop'}), OptionSettings( ['-r', '--redesdale'], help='', sources={'stop'}), ], [ OptionSettings( ['-b', '--byker'], help='', sources={'walk'}), OptionSettings( ['-r', '--roxborough'], help='', sources={'walk'}), ], [ OptionSettings( ['-b', '--bellingham'], help='', sources={'leap'}), ] ], [ {ARGS: ['--bellingham']}, {ARGS: ['--roxborough']}, {ARGS: ['--redesdale']}, {ARGS: ['--byker']}, {ARGS: ['-m', '--morpeth']} ], id='merge three overlapping argsets' ), param( [ ([]), ( [ OptionSettings( ['-c', '--campden'], help='x', sources={'foo'}) ] ) ], [ {ARGS: ['-c', '--campden']} ], id="empty list doesn't clear result" ), ] ) def test_combine_options(inputs, expect): """It combines multiple input sets""" result = combine_options(*inputs) result_args = [i.args for i in result] # Order of args irrelevent to test for option in expect: assert option[ARGS] in result_args @pytest.mark.parametrize( 'argv_before, kwargs, expect', [ param( 'vip myworkflow -f something -b something_else --baz', { 'script_name': 'play', 'workflow_id': 'myworkflow', 'compound_script_opts': [ OptionSettings(['--foo', '-f']), OptionSettings(['--bar', '-b'], action='store'), OptionSettings(['--baz'], action='store_true'), ], 'script_opts': [ OptionSettings(['--foo', '-f']), ] }, 'play myworkflow -f something', id='remove some opts' ), param( 'vip myworkflow', { 'script_name': 'play', 'workflow_id': 'myworkflow', 'compound_script_opts': [ OptionSettings(['--foo', '-f']), OptionSettings(['--bar', '-b']), OptionSettings(['--baz']), ], 'script_opts': [] }, 'play myworkflow', id='no opts to keep' ), param( 'vip ./myworkflow --foo something', { 'script_name': 'play', 'workflow_id': 'myworkflow', 'compound_script_opts': [ OptionSettings(['--foo', '-f'])], 'script_opts': [ OptionSettings(['--foo', '-f']), ], 'source': './myworkflow', }, 'play --foo something myworkflow', id='replace path' ), param( 'vip --foo something', { 'script_name': 'play', 'workflow_id': 'myworkflow', 'compound_script_opts': [ OptionSettings(['--foo', '-f'])], 'script_opts': [ OptionSettings(['--foo', '-f']), ], 'source': './myworkflow', }, 'play --foo something myworkflow', id='no path given' ), param( 'vip -n myworkflow --no-run-name', { 'script_name': 'play', 'workflow_id': 'myworkflow', 'compound_script_opts': [ OptionSettings(['--workflow-name', '-n']), OptionSettings(['--no-run-name']), ], 'script_opts': [ OptionSettings(['--not-used']), ] }, 'play myworkflow', id='workflow-id-added' ), ] ) def test_cleanup_sysargv( monkeypatch: pytest.MonkeyPatch, argv_before: str, kwargs: dict, expect: str ): """It replaces the contents of sysargv with Cylc Play argv items. """ # Fake up sys.argv: for this test. dummy_cylc_path = ['/pathto/my/cylc/bin/cylc'] monkeypatch.setattr(sys, 'argv', dummy_cylc_path + argv_before.split()) # Fake options too: opts = SimpleNamespace(**{ i.args[0].replace('--', ''): i for i in kwargs['compound_script_opts'] }) kwargs.update({'options': opts}) if not kwargs.get('source', None): kwargs.update({'source': ''}) # Test the script: cleanup_sysargv(**kwargs) assert sys.argv == dummy_cylc_path + expect.split() @pytest.mark.parametrize( 'sysargs, simple, compound, expect', ( param( # Test for https://github.com/cylc/cylc-flow/issues/5905 '--no-run-name --workflow-name=name'.split(), ['--no-run-name'], ['--workflow-name'], [], id='--workflow-name=name' ), param( '--foo something'.split(), [], [], '--foo something'.split(), id='no-opts-removed' ), param( [], ['--foo'], ['--bar'], [], id='Null-check' ), param( '''--keep1 --keep2 42 --keep3=Hi --throw1 --throw2 84 --throw3=There '''.split(), ['--throw1'], '--throw2 --throw3'.split(), '--keep1 --keep2 42 --keep3=Hi'.split(), id='complex' ), param( "--foo 'foo=42' --bar='foo=94'".split(), [], ['--foo'], ['--bar=\'foo=94\''], id='--bar=\'foo=94\'' ) ) ) def test_filter_sysargv( sysargs, simple, compound, expect ): """It returns the subset of sys.argv that we ask for. n.b. The three most basic cases for this function are stored in its own docstring. """ assert filter_sysargv(sysargs, simple, compound) == expect class TestOptionSettings(): @staticmethod def test_init(): args = ['--foo', '-f'] kwargs = {'bar': 42} sources = {'touch'} useif = 'hello' result = OptionSettings( args, sources=sources, useif=useif, **kwargs) assert result.__dict__ == { 'kwargs': kwargs, 'sources': sources, 'useif': useif, 'args': args } @staticmethod @pytest.mark.parametrize( 'first, second, expect', ( param( (['--foo', '-f'], {'bar': 42}, {'touch'}, 'hello'), (['--foo', '-f'], {'bar': 42}, {'touch'}, 'hello'), True, id='Totally the same'), param( (['--foo', '-f'], {'bar': 42}, {'touch'}, 'hello'), (['--foo', '-f'], {'bar': 42}, {'wibble'}, 'byee'), True, id='Differing extras'), param( (['-f'], {'bar': 42}, {'touch'}, 'hello'), (['--foo', '-f'], {'bar': 42}, {'wibble'}, 'byee'), False, id='Not equal args'), ) ) def test___eq__args_intersection(first, second, expect): args, kwargs, sources, useif = first first = OptionSettings( args, sources=sources, useif=useif, **kwargs) args, kwargs, sources, useif = second second = OptionSettings( args, sources=sources, useif=useif, **kwargs) assert (first == second) == expect @staticmethod @pytest.mark.parametrize( 'first, second, expect', ( param( ['--foo', '-f'], ['--foo', '-f'], ['--foo', '-f'], id='Totally the same'), param( ['--foo', '-f'], ['--foolish', '-f'], ['-f'], id='Some overlap'), param( ['--foo', '-f'], ['--bar', '-b'], [], id='No overlap'), ) ) def test___and__(first, second, expect): first = OptionSettings(first) second = OptionSettings(second) assert sorted(first & second) == sorted(expect) @staticmethod @pytest.mark.parametrize( 'first, second, expect', ( param( ['--foo', '-f'], ['--foo', '-f'], [], id='Totally the same'), param( ['--foo', '-f'], ['--foolish', '-f'], ['--foo'], id='Some overlap'), param( ['--foolish', '-f'], ['--foo', '-f'], ['--foolish'], id='Some overlap not commuting'), param( ['--foo', '-f'], ['--bar', '-b'], ['--foo', '-f'], id='No overlap'), ) ) def test___sub__args_subtraction(first, second, expect): first = OptionSettings(first) second = OptionSettings(second) assert sorted(first - second) == sorted(expect) @staticmethod def test__in_list(): """It is in a list.""" first = OptionSettings(['--foo']) second = OptionSettings(['--foo']) third = OptionSettings(['--bar']) assert first._in_list([second, third]) is True cylc-flow-8.6.4/tests/unit/test_terminal.py0000664000175000017500000001405715202510242021161 0ustar alastairalastair# THIS FILE IS PART OF THE CYLC SUITE 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 . from optparse import OptionParser, Values import pytest from cylc.flow.exceptions import CylcError from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.terminal import ( cli_function, prompt, should_use_color, ) # this puts Exception in globals() where we can easily find it later Exception = Exception SystemExit = SystemExit def get_option_parser(): """An option parser with no options.""" return OptionParser() @cli_function(get_option_parser) def cli(parser, opts, exc_class): """Dummy command line interface which raises an exception. Args: exc_class: The class of the exception to raise. """ if exc_class: raise globals()[exc_class]('message') @pytest.mark.parametrize( 'verbosity,exception_in,exception_out,return_code,stderr', [ # CylcError - "known" error pytest.param( # nicely formatted 0, CylcError, SystemExit, 1, 'CylcError: message\n', id='CylcError' ), pytest.param( # full traceback in debug mode 2, CylcError, CylcError, None, None, id='CylcError-debug' ), # ParsecError - "known" error pytest.param( # nicely formatted 0, ParsecError, SystemExit, 1, 'ParsecError: message\n', id='ParsecError' ), pytest.param( # full traceback in debug mode 2, ParsecError, ParsecError, None, None, id='ParsecError-debug' ), # Exception - "unknown" error pytest.param( # full traceback 0, Exception, Exception, None, None, id='Exception' ), pytest.param( # full traceback in debug mode 2, Exception, Exception, None, None, id='Exception-debug' ), # SystemExit - "unknown" error pytest.param( # full traceback 0, SystemExit, SystemExit, 1, 'ERROR: message\n', id='SystemExit' ), pytest.param( # full traceback in debug mode 2, SystemExit, SystemExit, 1, 'ERROR: message\n', id='SystemExit-debug' ), ] ) def test_cli( verbosity, exception_in, exception_out, return_code, stderr, monkeypatch, capsys ): """Test that the CLI formats exceptions appropriately. The idea here is that "known" errors (those which subclass CylcError or ParsecError) should be formatted nicely (as opposed to dumping the full traceback to stderr) in interactive mode. This behaviour can be overridden using --debug mode. In non-interactive mode we always print the full traceback for logging purposes. Other exceptions represent "unknown" errors which we would expect to occur. We should print the full traceback in these situations. """ monkeypatch.setattr('cylc.flow.flags.verbosity', verbosity) monkeypatch.setattr('cylc.flow.terminal.supports_color', lambda: False) with pytest.raises(exception_out) as exc_ctx: cli(exception_in.__name__) if return_code is not None: assert exc_ctx.value.args[0] == return_code if stderr is not None: assert capsys.readouterr()[1] == stderr @pytest.fixture def stdinput(monkeypatch): def _input(*lines): lines = list(lines) def __input(_message): try: return lines.pop(0) except IndexError: raise Exception('stdinput ran out of lines') monkeypatch.setattr( 'cylc.flow.terminal.input', __input, ) return _input def test_prompt(stdinput): """Test the prompt function with some simulated input.""" # test a multiple choice prompt stdinput('y') assert prompt('yes or no', ['y', 'n']) == 'y' stdinput('n') assert prompt('yes or no', ['y', 'n']) == 'n' # test a prompt with mapped return values stdinput('42') assert prompt('what is the answer', {'41': False, '42': True}) is True stdinput('41') assert prompt('what is the answer', {'41': False, '42': True}) is False # test incorrect input (should re-prompt until it gets a valid response) stdinput('40', '41', '42') assert prompt('what is the answer', ['42']) == '42' # test a prompt with a default stdinput('') assert prompt('whatever', ['x'], default='x') == 'x' # test a prompt with an input pre-process method thinggy stdinput('YES') assert prompt('yes yes yes no', ['yes', 'no'], process=str.lower) == 'yes' @pytest.mark.parametrize('opts, supported, expected', [ ({}, True, False), ({'color': 'never'}, True, False), ({'color': 'auto'}, True, True), ({'color': 'auto'}, False, False), ({'color': 'always'}, False, True), ]) def test_should_use_color( opts: dict, expected: bool, supported: bool, monkeypatch: pytest.MonkeyPatch ): monkeypatch.setattr('cylc.flow.terminal.supports_color', lambda: supported) options = Values(opts) assert should_use_color(options) == expected cylc-flow-8.6.4/tests/unit/test_task_events_mgr.py0000664000175000017500000001043315202510242022533 0ustar alastairalastair# 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 . from typing import Optional from unittest.mock import Mock import pytest from cylc.flow.broadcast_mgr import BroadcastMgr from cylc.flow.task_events_mgr import TaskEventsManager from cylc.flow.task_proxy import TaskProxy from cylc.flow.taskdef import TaskDef @pytest.mark.parametrize( "broadcast, remote, platforms, expected", [ ("hpc1", "a", "b", "hpc1"), (None, "hpc1", "b", "hpc1"), (None, None, "hpc1", "hpc1"), (None, None, None, None), ] ) def test_get_remote_conf(broadcast, remote, platforms, expected): """Test TaskEventsManager._get_remote_conf().""" task_events_mgr = TaskEventsManager( None, None, None, None, None, None, None, None, None) task_events_mgr.broadcast_mgr = Mock( get_broadcast=lambda x: { "remote": { "host": broadcast } } ) itask = Mock( identity='foo.1', tdef=Mock( rtconfig={ 'remote': { 'host': remote } } ), platform={ 'host': platforms } ) assert task_events_mgr._get_remote_conf(itask, 'host') == expected @pytest.mark.parametrize( "broadcast, workflow, platforms, expected", [ ([800], [700], [600], [800]), (None, [700], [600], [700]), (None, None, [600], [600]), ] ) def test_get_workflow_platforms_conf(broadcast, workflow, platforms, expected): """Test TaskEventsManager._get_polling_interval_conf().""" task_events_mgr = TaskEventsManager( None, None, None, None, None, None, None, None, None) KEY = "execution polling intervals" task_events_mgr.broadcast_mgr = Mock( get_broadcast=lambda x: { KEY: broadcast } ) itask = Mock( identity='foo.1', tdef=Mock( rtconfig={ KEY: workflow } ), platform={ KEY: platforms } ) assert ( task_events_mgr._get_workflow_platforms_conf(itask, KEY) == expected ) @pytest.mark.parametrize( 'rt_val, schd_val, glbl_val, expected', [ ('rt', 'schd', 'glbl', 'rt'), (None, 'schd', 'glbl', 'schd'), (None, None, 'glbl', 'glbl'), (None, None, None, 'default'), ] ) def test_get_events_conf__mail_to_from( mock_glbl_cfg, rt_val: Optional[str], schd_val: Optional[str], glbl_val: Optional[str], expected: str ): """Test order of precedence for [mail]to/from.""" if glbl_val: mock_glbl_cfg( 'cylc.flow.task_events_mgr.glbl_cfg', f''' [scheduler] [[mail]] from = {glbl_val} to = {glbl_val} ''' ) mock_task = Mock( spec=TaskProxy, tdef=Mock( spec=TaskDef, rtconfig={ 'events': {}, 'mail': {'to': rt_val, 'from': rt_val} if rt_val else {}, }, ), ) mock_task_events_mgr = Mock( spec=TaskEventsManager, workflow_cfg={ 'scheduler': { 'mail': {'to': schd_val, 'from': schd_val}, }, } if schd_val else {}, broadcast_mgr=Mock( spec_set=BroadcastMgr, get_broadcast=lambda *a, **k: {}, ), ) for key in ('to', 'from'): assert TaskEventsManager._get_events_conf( mock_task_events_mgr, itask=mock_task, key=key, default='default' ) == expected cylc-flow-8.6.4/tests/unit/test_workflow_status.py0000664000175000017500000001201215202510242022610 0ustar alastairalastair# 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 . from types import SimpleNamespace import pytest from metomi.isodatetime.data import TimePoint from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.workflow_status import ( WORKFLOW_STATUS_RUNNING_TO_HOLD, WORKFLOW_STATUS_RUNNING_TO_STOP, StopMode, WorkflowStatus, get_workflow_status, get_workflow_status_msg, ) STOP_TIME = TimePoint(year=2006).to_local_time_zone() def schd( final_point=None, hold_point=None, is_paused=False, is_stalled=None, stop_clock_time=None, stop_mode=None, stop_point=None, stop_task_id=None, reload_pending=False, ): return SimpleNamespace( is_paused=is_paused, is_stalled=is_stalled, stop_clock_time=stop_clock_time, stop_mode=stop_mode, reload_pending=reload_pending, pool=SimpleNamespace( hold_point=hold_point, stop_point=stop_point, stop_task_id=stop_task_id, ), config=SimpleNamespace(final_point=final_point), options=SimpleNamespace(utc_mode=True), ) @pytest.mark.parametrize( 'kwargs, state, message', [ # test each of the states ( {'is_paused': True}, WorkflowStatus.PAUSED, 'paused' ), ( {'reload_pending': 'message'}, WorkflowStatus.PAUSED, 'reloading: message' ), ( {'stop_mode': StopMode.AUTO}, WorkflowStatus.STOPPING, 'stopping: waiting for active jobs to complete' ), ( {'hold_point': 2}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_HOLD % 2 ), ( {'stop_point': 4}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % 4 ), ( {'stop_clock_time': int(STOP_TIME.seconds_since_unix_epoch)}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % str(STOP_TIME) ), ( {'stop_task_id': '6/foo'}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % '6/foo' ), ( {'final_point': 8}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % 8 ), ( {'is_stalled': True}, WorkflowStatus.RUNNING, 'stalled' ), ( {}, WorkflowStatus.RUNNING, 'running' ), # test combinations ( # stopping should trump stalled, paused & running { 'stop_mode': StopMode.REQUEST_NOW, 'is_stalled': True, 'is_paused': True }, WorkflowStatus.STOPPING, 'stopping: shutting down' ), ( {'is_stalled': True, 'is_paused': True}, WorkflowStatus.PAUSED, 'stalled and paused', ), ( # earliest of stop point, hold point and stop task id { 'stop_point': IntegerPoint(4), 'hold_point': IntegerPoint(2), 'stop_task_id': '6/foo', }, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_HOLD % 2, ), ( { 'stop_point': IntegerPoint(11), 'hold_point': IntegerPoint(15), 'stop_task_id': '9/bar', }, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % '9/bar', ), ( { 'stop_point': IntegerPoint(3), 'hold_point': IntegerPoint(3), }, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % 3, ), ( # stop point trumps final point { 'stop_point': IntegerPoint(1), 'final_point': IntegerPoint(2), }, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % 1, ), ] ) def test_get_workflow_status(kwargs, state, message, set_cycling_type): set_cycling_type() scheduler = schd(**kwargs) assert get_workflow_status(scheduler) == state assert get_workflow_status_msg(scheduler) == message cylc-flow-8.6.4/tests/unit/test_config_upgrader.py0000664000175000017500000000766715202510242022515 0ustar alastairalastair# 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 . # # Tests that configs can be upgraded from earlier versions of Cylc. import pytest from cylc.flow.cfgspec.workflow import upg, upgrade_param_env_templates from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults as ord_dict @pytest.mark.parametrize( 'cfg, expected', [ ( # No clashes - order is important: { 'parameter environment templates': ord_dict([ ('FOO', 'jupiter'), ('MOO', 'pluto') ]), 'environment': ord_dict([ ('BAR', 'neptune'), ('BAZ', 'ares') ]) }, { 'environment': ord_dict([ ('FOO', 'jupiter'), ('MOO', 'pluto'), ('BAR', 'neptune'), ('BAZ', 'ares') ]) } ), ( # Clashes - environment wins: { 'parameter environment templates': ord_dict([ ('FOO', 'jupiter'), ('BAR', 'neptune') ]), 'environment': ord_dict([ ('FOO', 'zeus'), ('BAR', 'poseidon') ]) }, { 'environment': ord_dict([ ('FOO', 'zeus'), ('BAR', 'poseidon') ]) } ), ( # No environment section: { 'parameter environment templates': ord_dict([ ('FOO', 'jupiter'), ('BAR', 'neptune') ]) }, { 'environment': ord_dict([ ('FOO', 'jupiter'), ('BAR', 'neptune') ]) } ) ] ) def test_upgrade_param_env_templates(cfg, expected): """Test that the deprecated [runtime][X][parameter environment templates] contents are prepended to [runtime][X][environment], in the correct order""" def _cfg(dic): """Return OrderedDictWithDefaults config populated with values from dic (dictionary)""" result = ord_dict({ 'runtime': ord_dict({ '': ord_dict({ 'script': 'echo whatever' }) }) }) if 'parameter environment templates' in dic: result['runtime']['']['parameter environment templates'] = ( dic['parameter environment templates'] ) if 'environment' in dic: result['runtime']['']['environment'] = dic['environment'] return result config = _cfg(cfg) upgrade_param_env_templates(config, 'flow.cylc') assert config == _cfg(expected) @pytest.mark.parametrize( 'macp, rlim', [(16, 'P15'), ('', '')] ) def test_upgrade_max_active_cycle_points(macp, rlim): """Test that `max active cycle points` is correctly upgraded to `runahead limit`.""" cfg = { 'scheduling': {'max active cycle points': macp} } expected = { 'scheduling': {'runahead limit': rlim} } upg(cfg, 'flow.cylc') assert cfg == expected cylc-flow-8.6.4/tests/unit/xtriggers/0000775000175000017500000000000015202510242017744 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/xtriggers/test_echo.py0000664000175000017500000000255615202510242022303 0ustar alastairalastair# 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 . from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.xtriggers.echo import validate import pytest from pytest import param def test_validate_good(): validate({ 'args': (), 'kwargs': {'succeed': False, 'egg': 'fried', 'potato': 'baked'} }) @pytest.mark.parametrize( 'all_args', ( param({'args': (False,), 'kwargs': {}}, id='no-kwarg'), param( {'args': (), 'kwargs': {'spud': 'mashed'}}, id='no-succeed-kwarg' ), ), ) def test_validate_exceptions(all_args): with pytest.raises(WorkflowConfigError, match='^Requires'): validate(all_args) cylc-flow-8.6.4/tests/unit/xtriggers/__init__.py0000664000175000017500000000000015202510242022043 0ustar alastairalastaircylc-flow-8.6.4/tests/unit/xtriggers/test_workflow_state.py0000664000175000017500000002607215202510242024436 0ustar alastairalastair# 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 . from pathlib import Path import sqlite3 from typing import Any, Callable from shutil import copytree, rmtree import pytest from cylc.flow.dbstatecheck import output_fallback_msg from cylc.flow.exceptions import WorkflowConfigError, InputError from cylc.flow.rundb import CylcWorkflowDAO from cylc.flow.workflow_files import WorkflowFiles from cylc.flow.xtriggers.workflow_state import ( _workflow_state_backcompat, workflow_state, validate, ) from cylc.flow.xtriggers.suite_state import suite_state def test_inferred_run(tmp_run_dir: 'Callable', capsys: pytest.CaptureFixture): """Test that the workflow_state xtrigger infers the run number. Method: the faked run-dir has no DB to connect to, but the WorkflowPoller prints inferred ID to stderr if the run-dir exists. """ id_ = 'isildur' expected_workflow_id = f'{id_}/run1' cylc_run_dir = str(tmp_run_dir()) tmp_run_dir(expected_workflow_id, installed=True, named=True) workflow_state(id_ + '//3000/precious') assert expected_workflow_id in capsys.readouterr().err # Now test we can see workflows in alternate cylc-run directories # e.g. for `cylc workflow-state` or xtriggers targetting another user. alt_cylc_run_dir = cylc_run_dir + "_alt" # copy the cylc-run dir to alt location and delete the original. copytree(cylc_run_dir, alt_cylc_run_dir, symlinks=True) rmtree(cylc_run_dir) # It can no longer parse IDs in the original cylc-run location. workflow_state(id_) assert expected_workflow_id not in capsys.readouterr().err # But it can via an explicit alternate run directory. workflow_state(id_, alt_cylc_run_dir=alt_cylc_run_dir) assert expected_workflow_id in capsys.readouterr().err def test_c7_db_back_compat(tmp_run_dir: 'Callable'): """Test workflow_state xtrigger backwards compatibility with Cylc 7 database.""" id_ = 'celebrimbor' c7_run_dir: Path = tmp_run_dir(id_) (c7_run_dir / WorkflowFiles.FLOW_FILE).rename( c7_run_dir / WorkflowFiles.SUITE_RC ) db_file = c7_run_dir / 'log' / 'db' db_file.parent.mkdir(exist_ok=True) # Note: cannot use CylcWorkflowDAO here as creating outdated DB conn = sqlite3.connect(str(db_file)) try: conn.execute(r""" CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key)); """) conn.execute(r""" CREATE TABLE task_states( name TEXT, cycle TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, PRIMARY KEY(name, cycle) ); """) conn.execute(r""" CREATE TABLE task_outputs( cycle TEXT, name TEXT, outputs TEXT, PRIMARY KEY(cycle, name) ); """) conn.executemany( r'INSERT INTO "suite_params" VALUES(?,?);', [('cylc_version', '7.8.12'), ('cycle_point_format', '%Y'), ('cycle_point_tz', 'Z')] ) conn.execute(r""" INSERT INTO "task_states" VALUES( 'mithril','2012','2023-01-30T18:19:15Z','2023-01-30T18:19:15Z', 0,'succeeded' ); """) conn.execute(r""" INSERT INTO "task_outputs" VALUES( '2012','mithril','{"frodo": "bag end"}' ); """) conn.commit() finally: conn.close() # Test workflow_state function satisfied, _ = workflow_state(f'{id_}//2012/mithril') assert satisfied satisfied, _ = workflow_state(f'{id_}//2012/mithril:succeeded') assert satisfied satisfied, _ = workflow_state( f'{id_}//2012/mithril:frodo', is_trigger=True ) assert satisfied satisfied, _ = workflow_state( f'{id_}//2012/mithril:"bag end"', is_message=True ) assert satisfied with pytest.raises(InputError, match='No such task state "pippin"'): workflow_state(f'{id_}//2012/mithril:pippin') satisfied, _ = workflow_state(id_ + '//2012/arkenstone') assert not satisfied # Test back-compat (old suite_state function) satisfied, _ = suite_state(suite=id_, task='mithril', point='2012') assert satisfied satisfied, _ = suite_state( suite=id_, task='mithril', point='2012', status='succeeded' ) assert satisfied satisfied, _ = suite_state( suite=id_, task='mithril', point='2012', message='bag end' ) assert satisfied satisfied, _ = suite_state(suite=id_, task='arkenstone', point='2012') assert not satisfied def test_c8_db_back_compat( tmp_run_dir: 'Callable', capsys: pytest.CaptureFixture, ): """Test workflow_state xtrigger backwards compatibility with Cylc < 8.3.0 database.""" id_ = 'nazgul' run_dir: Path = tmp_run_dir(id_) db_file = run_dir / 'log' / 'db' db_file.parent.mkdir(exist_ok=True) # Note: don't use CylcWorkflowDAO here as DB should be frozen conn = sqlite3.connect(str(db_file)) try: conn.execute(r""" CREATE TABLE workflow_params( key TEXT, value TEXT, PRIMARY KEY(key) ); """) conn.execute(r""" CREATE TABLE task_states( name TEXT, cycle TEXT, flow_nums TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, flow_wait INTEGER, is_manual_submit INTEGER, PRIMARY KEY(name, cycle, flow_nums) ); """) conn.execute(r""" CREATE TABLE task_outputs( cycle TEXT, name TEXT, flow_nums TEXT, outputs TEXT, PRIMARY KEY(cycle, name, flow_nums) ); """) conn.executemany( r'INSERT INTO "workflow_params" VALUES(?,?);', [('cylc_version', '8.2.7'), ('cycle_point_format', '%Y'), ('cycle_point_tz', 'Z')] ) conn.execute(r""" INSERT INTO "task_states" VALUES( 'gimli','2012','[1]','2023-01-30T18:19:15Z', '2023-01-30T18:19:15Z',1,'succeeded',0,0 ); """) conn.execute(r""" INSERT INTO "task_outputs" VALUES( '2012','gimli','[1]', '["submitted", "started", "succeeded", "axe"]' ); """) conn.commit() finally: conn.close() gimli = f'{id_}//2012/gimli' satisfied, _ = workflow_state(gimli) assert satisfied satisfied, _ = workflow_state(f'{gimli}:succeeded') assert satisfied satisfied, _ = workflow_state(f'{gimli}:axe', is_message=True) assert satisfied _, err = capsys.readouterr() assert not err # Output label selector falls back to message # (won't work if messsage != output label) satisfied, _ = workflow_state(f'{gimli}:axe', is_trigger=True) assert satisfied _, err = capsys.readouterr() assert output_fallback_msg in err def test__workflow_state_backcompat(tmp_run_dir: 'Callable'): """Test the _workflow_state_backcompat & suite_state functions on a *current* Cylc database.""" id_ = 'dune' run_dir: Path = tmp_run_dir(id_) db_file = run_dir / 'log' / 'db' db_file.parent.mkdir(exist_ok=True) with CylcWorkflowDAO(db_file, create_tables=True) as dao: conn = dao.connect() conn.executemany( r'INSERT INTO "workflow_params" VALUES(?,?);', [('cylc_version', '8.3.0'), ('cycle_point_format', '%Y'), ('cycle_point_tz', 'Z')] ) conn.execute(r""" INSERT INTO "task_states" VALUES( 'arrakis','2012','[1]','2023-01-30T18:19:15Z', '2023-01-30T18:19:15Z',1,'succeeded',0,0 ); """) conn.execute(r""" INSERT INTO "task_outputs" VALUES( '2012','arrakis','[1]', '{ "submitted": "submitted", "started": "started", "succeeded": "succeeded", "paul": "lisan al-gaib" }' ); """) conn.commit() func: Any for func in (_workflow_state_backcompat, suite_state): satisfied, _ = func(id_, 'arrakis', '2012') assert satisfied satisfied, _ = func(id_, 'arrakis', '2012', status='succeeded') assert satisfied # Both output label and message work satisfied, _ = func(id_, 'arrakis', '2012', message='paul') assert satisfied satisfied, _ = func(id_, 'arrakis', '2012', message='lisan al-gaib') assert satisfied def test_validate_ok(): """Validate returns ok with valid args.""" validate({ 'workflow_task_id': 'foo//1/bar', 'is_trigger': False, 'is_message': False, 'offset': 'PT1H', 'flow_num': 44, }) @pytest.mark.parametrize( 'id_', (('foo//1'),) ) def test_validate_fail_bad_id(id_): """Validation failure for bad id""" with pytest.raises(WorkflowConfigError, match='Full ID needed'): validate({ 'workflow_task_id': id_, 'offset': 'PT1H', 'flow_num': 44, }) @pytest.mark.parametrize( 'flow_num', ((4.25260), ('Belguim')) ) def test_validate_fail_non_int_flow(flow_num): """Validate failure for non integer flow numbers.""" with pytest.raises(WorkflowConfigError, match='must be an integer'): validate({ 'workflow_task_id': 'foo//1/bar', 'offset': 'PT1H', 'flow_num': flow_num, }) def test_validate_polling_config(): """It should reject invalid or unreliable polling configurations. See https://github.com/cylc/cylc-flow/issues/6157 """ with pytest.raises(WorkflowConfigError, match='No such task state'): validate({ 'workflow_task_id': 'foo//1/bar:elephant', 'is_trigger': False, 'is_message': False, 'flow_num': 44, }) with pytest.raises(WorkflowConfigError, match='Cannot poll for'): validate({ 'workflow_task_id': 'foo//1/bar:waiting', 'is_trigger': False, 'is_message': False, 'flow_num': 44, }) with pytest.raises(WorkflowConfigError, match='is not reliable'): validate({ 'workflow_task_id': 'foo//1/bar:submitted', 'is_trigger': False, 'is_message': False, 'flow_num': 44, }) cylc-flow-8.6.4/tests/unit/xtriggers/test_wall_clock.py0000664000175000017500000000365515202510242023500 0ustar alastairalastair# 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 . from cylc.flow.xtriggers.wall_clock import _wall_clock, validate from metomi.isodatetime.parsers import DurationParser import pytest from pytest import param @pytest.mark.parametrize('trigger_time, expected', [ (499, True), (500, False), ]) def test_wall_clock( monkeypatch: pytest.MonkeyPatch, trigger_time: int, expected: bool ): monkeypatch.setattr( 'cylc.flow.xtriggers.wall_clock.time', lambda: 500 ) assert _wall_clock(trigger_time) == expected @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(monkeypatch_interval_parser): validate({'offset': 'PT1H'}) @pytest.mark.parametrize( 'args, err', ( param({'offset': 1}, "^Invalid", id='invalid-offset-int'), param({'offset': 'Zaphod'}, "^Invalid", id='invalid-offset-str'), ) ) def test_validate_exceptions( monkeypatch_interval_parser, args, err ): with pytest.raises(Exception, match=err): validate(args) cylc-flow-8.6.4/tests/unit/xtriggers/test_xrandom.py0000664000175000017500000000274315202510242023033 0ustar alastairalastair# 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 . import pytest from pytest import param from cylc.flow.xtriggers.xrandom import validate from cylc.flow.exceptions import WorkflowConfigError def test_validate_good(): validate({'percent': 1, 'secs': 0, '_': 'HelloWorld'}) @pytest.mark.parametrize( 'args, err', ( param({'percent': 'foo'}, r"'percent", id='percent-not-numeric'), param({'percent': 101}, r"'percent", id='percent>100'), param({'percent': -1}, r"'percent", id='percent<0'), param({'percent': 100, 'secs': 1.1}, r"'secs'", id='secs-not-int'), ) ) def test_validate_exceptions(args, err): """Illegal args and kwargs cause a WorkflowConfigError raised.""" with pytest.raises(WorkflowConfigError, match=f'^{err}'): validate(args) cylc-flow-8.6.4/tests/unit/test_id_cli.py0000664000175000017500000004763715202510242020603 0ustar alastairalastair# 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 . import logging import os from pathlib import Path import pytest from shutil import copytree, rmtree from cylc.flow import CYLC_LOG from cylc.flow.async_util import pipe from cylc.flow.exceptions import InputError, WorkflowFilesError from cylc.flow.id import detokenise, tokenise, Tokens from cylc.flow.id_cli import ( _expand_workflow_tokens, _parse_src_path, _validate_constraint, _validate_workflow_ids, _validate_number, cli_tokenise, parse_ids_async, ) from cylc.flow.pathutil import get_cylc_run_dir from cylc.flow.workflow_files import WorkflowFiles @pytest.fixture def mock_exists(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr('pathlib.Path.exists', lambda *a, **k: True) @pytest.fixture(scope='module') def abc_src_dir(tmp_path_factory): """Src dir containing three workflows, a, b & c.""" cwd_before = Path.cwd() tmp_path = tmp_path_factory.getbasetemp() os.chdir(tmp_path) for name in ('a', 'b', 'c'): Path(tmp_path, name).mkdir() Path(tmp_path, name, WorkflowFiles.FLOW_FILE).touch() yield tmp_path os.chdir(cwd_before) @pytest.mark.parametrize( 'ids_in,ids_out', [ (('a//',), ['a']), (('a//', 'a//'), ['a']), (('a//', 'b//'), ['a', 'b']), ] ) async def test_parse_ids_workflows(ids_in, ids_out, mock_exists): """It should parse workflows & tasks.""" workflows, _ = await parse_ids_async(*ids_in, constraint='workflows') assert list(workflows) == ids_out assert list(workflows.values()) == [[] for _ in workflows] @pytest.mark.parametrize( 'ids_in,ids_out', [ (('./a',), ['a']), ] ) async def test_parse_ids_workflows_src(ids_in, ids_out, abc_src_dir): """It should parse src workflows.""" workflows, _ = await parse_ids_async( *ids_in, src=True, constraint='workflows', ) assert list(workflows) == ids_out assert list(workflows.values()) == [[] for _ in workflows] @pytest.mark.parametrize( 'ids_in,ids_out', [ ( ('a//i',), {'a': ['//i']}, ), ( ('a//i', 'a//j'), {'a': ['//i', '//j']}, ), ( ('a//i', 'b//i'), {'a': ['//i'], 'b': ['//i']}, ), ( ('a//', '//i', 'b//', '//i'), {'a': ['//i'], 'b': ['//i']}, ), ] ) async def test_parse_ids_tasks(mock_exists, ids_in, ids_out): """It should parse workflow tasks in two formats.""" workflows, _ = await parse_ids_async(*ids_in, constraint='tasks') assert { workflow_id: [detokenise(tokens) for tokens in tokens_list] for workflow_id, tokens_list in workflows.items() } == ids_out @pytest.mark.parametrize( 'ids_in,ids_out', [ ( ('./a', '//i'), {'a': ['//i']} ), ( ('./a', '//i', '//j', '//k'), {'a': ['//i', '//j', '//k']} ), ] ) async def test_parse_ids_tasks_src(mock_exists, ids_in, ids_out, abc_src_dir): """It should parse workflow tasks for src workflows.""" workflows, _ = await parse_ids_async( *ids_in, constraint='tasks', src=True) assert { workflow_id: [detokenise(tokens) for tokens in tokens_list] for workflow_id, tokens_list in workflows.items() } == ids_out @pytest.mark.parametrize( 'ids_in,ids_out', [ (('a//',), {'a': []}), ( ('a//', 'b//', 'c//'), {'a': [], 'b': [], 'c': []} ), (('a//i',), {'a': ['//i']}), (('a//', '//i'), {'a': ['//i']}), ( ('a//', '//i', '//j', '//k'), {'a': ['//i', '//j', '//k']}, ), (('a//', '//i', 'b//'), {'a': ['//i'], 'b': []}), ] ) async def test_parse_ids_mixed(ids_in, ids_out, mock_exists): """It should parse mixed workflows & tasks.""" workflows, _ = await parse_ids_async(*ids_in, constraint='mixed') assert { workflow_id: [detokenise(tokens) for tokens in tokens_list] for workflow_id, tokens_list in workflows.items() } == ids_out @pytest.mark.parametrize( 'ids_in,ids_out', [ (('./a',), {'a': []}), (('./a', '//i'), {'a': ['//i']}), (('./a', '//i', '//j', '//k'), {'a': ['//i', '//j', '//k']}), ] ) async def test_parse_ids_mixed_src(ids_in, ids_out, abc_src_dir, mock_exists): """It should parse mixed workflows & tasks from src workflows.""" workflows, _ = await parse_ids_async( *ids_in, constraint='mixed', src=True ) assert { workflow_id: [detokenise(tokens) for tokens in tokens_list] for workflow_id, tokens_list in workflows.items() } == ids_out @pytest.mark.parametrize( 'ids_in,errors', [ (('a//',), False), (('a//', 'b//'), False), (('a//', 'b//', 'c//'), True), ] ) async def test_parse_ids_max_workflows(ids_in, errors, mock_exists): """It should validate input against the max_workflows constraint.""" try: await parse_ids_async( *ids_in, constraint='workflows', max_workflows=2) except InputError: if not errors: raise else: if errors: raise Exception('Should have raised InputError') @pytest.mark.parametrize( 'ids_in,errors', [ (('a//', '//i'), False), (('a//', '//i', '//j'), False), (('a//', '//i', '//j', '//k'), True), ] ) async def test_parse_ids_max_tasks(ids_in, errors, mock_exists): """It should validate input against the max_tasks constraint.""" try: await parse_ids_async(*ids_in, constraint='tasks', max_tasks=2) except InputError: if not errors: raise else: if errors: raise Exception('Should have raised InputError') async def test_parse_ids_infer_run_name(tmp_run_dir): """It should infer the run name for auto-numbered installations.""" # it doesn't do anything for a named run tmp_run_dir('foo/bar', named=True, installed=True) workflows, *_ = await parse_ids_async('foo//', constraint='workflows') assert list(workflows) == ['foo'] # it correctly identifies the latest run tmp_run_dir('bar/run1') workflows, *_ = await parse_ids_async('bar//', constraint='workflows') assert list(workflows) == ['bar/run1'] tmp_run_dir('bar/run2') workflows, *_ = await parse_ids_async('bar//', constraint='workflows') assert list(workflows) == ['bar/run2'] # it leaves the ID alone if infer_latest_runs = False workflows, *_ = await parse_ids_async( 'bar//', constraint='workflows', infer_latest_runs=False, ) assert list(workflows) == ['bar'] # Now test we can see workflows in alternate cylc-run directories # e.g. for `cylc workflow-state` or xtriggers targetting another user. cylc_run_dir = get_cylc_run_dir() alt_cylc_run_dir = cylc_run_dir + "_alt" # copy the cylc-run dir to alt location and delete the original. copytree(cylc_run_dir, alt_cylc_run_dir, symlinks=True) rmtree(cylc_run_dir) # It can no longer parse IDs in the original cylc-run location. with pytest.raises(InputError): workflows, *_ = await parse_ids_async( 'bar//', constraint='workflows', infer_latest_runs=True, ) # But it can if we specify the alternate location. workflows, *_ = await parse_ids_async( 'bar//', constraint='workflows', infer_latest_runs=True, alt_run_dir=alt_cylc_run_dir ) assert list(workflows) == ['bar/run2'] @pytest.fixture def patch_expand_workflow_tokens(monkeypatch): """Define the output of scan events.""" def _patch_expand_workflow_tokens(_ids): async def _expand_workflow_tokens_impl(tokens, match_active=True): for id_ in _ids: yield tokens.duplicate(workflow=id_) monkeypatch.setattr( 'cylc.flow.id_cli._expand_workflow_tokens_impl', _expand_workflow_tokens_impl, ) _patch_expand_workflow_tokens(['xxx']) return _patch_expand_workflow_tokens @pytest.mark.parametrize( 'ids_in,ids_out,multi_mode', [ # multi mode should be True if multiple workflows are defined (['a//'], ['a'], False), (['a//', 'b//'], ['a', 'b'], True), # or if pattern matching is used, irrespective of the number of matches (['*//'], ['xxx'], True), ] ) async def test_parse_ids_multi_mode( patch_expand_workflow_tokens, ids_in, ids_out, multi_mode, mock_exists ): """It should glob for workflows. Note: More advanced tests for this in the integration tests. """ workflows, _multi_mode = await parse_ids_async( *ids_in, constraint='workflows', match_workflows=True, ) assert list(workflows) == ids_out assert _multi_mode == multi_mode @pytest.fixture def src_dir(tmp_path): """A src dir containing a workflow called "a".""" cwd_before = Path.cwd() src_dir = (tmp_path / 'a') src_dir.mkdir() src_file = src_dir / 'flow.cylc' src_file.touch() other_dir = (tmp_path / 'blargh') other_dir.mkdir() other_file = other_dir / 'nugget' other_file.touch() os.chdir(tmp_path) yield src_dir os.chdir(cwd_before) def test_parse_src_path(src_dir, monkeypatch): """It should locate src dirs.""" # valid absolute path workflow_id, src_path, src_file_path = _parse_src_path( str(src_dir.resolve()) ) assert workflow_id == 'a' assert src_path == src_dir assert src_file_path == src_dir / 'flow.cylc' # broken absolute path with pytest.raises(InputError): workflow_id, src_path, src_file_path = _parse_src_path( str(src_dir.resolve()) + 'xyz' ) # valid ./relative path workflow_id, src_path, src_file_path = _parse_src_path('./a') assert workflow_id == 'a' assert src_path == src_dir assert src_file_path == src_dir / 'flow.cylc' # broken relative path with pytest.raises(InputError): _parse_src_path('./xxx') # relative '.' dir (invalid) with pytest.raises(WorkflowFilesError) as exc_ctx: workflow_id, src_path, src_file_path = _parse_src_path('.') assert 'No flow.cylc or suite.rc in' in str(exc_ctx.value) # relative 'invalid/' (invalid) with pytest.raises(InputError) as exc_ctx: _parse_src_path('xxx/flow.cylc') assert 'Not a valid workflow ID or source directory' in str(exc_ctx.value) # Might be a workflow ID res = _parse_src_path('the/quick/brown/fox') assert res is None # Might be a workflow ID, even though there's a matching relative path res = _parse_src_path('a') assert res is None # Not a src directory (dir) with pytest.raises(WorkflowFilesError) as exc_ctx: _parse_src_path('./blargh') assert 'No flow.cylc or suite.rc in' in str(exc_ctx.value) # Not a src directory (file) with pytest.raises(InputError) as exc_ctx: _parse_src_path('./blargh/nugget') assert 'Path is not a source directory' in str(exc_ctx.value) # move into the src dir monkeypatch.chdir(src_dir) # relative '.' dir (valid) workflow_id, src_path, src_file_path = _parse_src_path('.') assert workflow_id == 'a' assert src_path == src_dir assert src_file_path == src_dir / 'flow.cylc' # relative './' (invalid) with pytest.raises(InputError) as exc_ctx: _parse_src_path('./flow.cylc') assert 'Not a valid workflow ID or source directory' in str(exc_ctx.value) # suite.rc & flow.cylc both present: (src_dir / 'suite.rc').touch() with pytest.raises(WorkflowFilesError) as exc_ctx: _parse_src_path(str(src_dir)) assert 'Both flow.cylc and suite.rc files' in str(exc_ctx.value) async def test_parse_ids_src_path(src_dir): workflows, src_path = await parse_ids_async( './a', src=True, constraint='workflows', ) assert workflows == {'a': []} @pytest.mark.parametrize( 'ids_in,error_msg', [ ( ['/home/me/whatever'], 'Invalid ID: /home/me/whatever', ), ( ['foo/..'], 'cannot be a path that points to the cylc-run directory or above', ), ( ['~alice/foo'], "Operating on other users' workflows is not supported", ), ] ) async def test_parse_ids_invalid_ids( ids_in, error_msg, monkeypatch: pytest.MonkeyPatch ): """It should error for invalid IDs.""" monkeypatch.setattr('cylc.flow.id_cli.get_user', lambda: 'rincewind') with pytest.raises(Exception) as exc_ctx: await parse_ids_async( *ids_in, constraint='workflows', ) assert error_msg in str(exc_ctx.value) async def test_parse_ids_current_user( monkeypatch: pytest.MonkeyPatch, mock_exists ): """It should work if the user in the ID is the current user.""" monkeypatch.setattr('cylc.flow.id_cli.get_user', lambda: 'rincewind') await parse_ids_async('~rincewind/luggage', constraint='workflows') async def test_parse_ids_file(tmp_run_dir): """It should reject IDs that are paths to files.""" tmp_path = tmp_run_dir('x') tmp_file = tmp_path / 'tmpfile' tmp_file.touch() (tmp_path / WorkflowFiles.FLOW_FILE).touch() # using a directory should work await parse_ids_async( str(tmp_path.relative_to(get_cylc_run_dir())), constraint='workflows', ) with pytest.raises(Exception) as exc_ctx: # using a file should not await parse_ids_async( str(tmp_file.relative_to(get_cylc_run_dir())), constraint='workflows', ) assert 'Workflow ID cannot be a file' in str(exc_ctx.value) async def test_parse_ids_constraint(mock_exists): """It should validate input against the constraint.""" # constraint: workflows await parse_ids_async('a//', constraint='workflows') with pytest.raises(InputError): await parse_ids_async('a//b', constraint='workflows') # constraint: tasks await parse_ids_async('a//b', constraint='tasks') with pytest.raises(InputError): await parse_ids_async('a//', constraint='tasks') # constraint: mixed await parse_ids_async('a//', constraint='mixed') await parse_ids_async('a//b', constraint='mixed') # constraint: invalid with pytest.raises(ValueError): await parse_ids_async('foo', constraint='bar') async def test_parse_ids_src_run(abc_src_dir, tmp_run_dir): """It should locate the flow file when src=True.""" # locate flow file for a src workflow workflows, flow_file_path = await parse_ids_async( './a', src=True, constraint='workflows', ) assert list(workflows) == ['a'] assert flow_file_path == abc_src_dir / 'a' / WorkflowFiles.FLOW_FILE # locate flow file for a run workflow run_dir = tmp_run_dir('b') workflows, flow_file_path = await parse_ids_async( 'b', src=True, constraint='workflows', ) assert list(workflows) == ['b'] assert flow_file_path == run_dir / WorkflowFiles.FLOW_FILE def test_validate_constraint(): """It should validate tokens against the constraint.""" # constraint=workflows _validate_constraint(Tokens(workflow='a'), constraint='workflows') with pytest.raises(InputError): _validate_constraint(Tokens(cycle='a'), constraint='workflows') with pytest.raises(InputError): _validate_constraint(Tokens(), constraint='workflows') # constraint=tasks _validate_constraint(Tokens(cycle='a'), constraint='tasks') with pytest.raises(InputError): _validate_constraint(Tokens(workflow='a'), constraint='tasks') with pytest.raises(InputError): _validate_constraint(Tokens(), constraint='tasks') # constraint=mixed _validate_constraint(Tokens(workflow='a'), constraint='mixed') _validate_constraint(Tokens(cycle='a'), constraint='mixed') with pytest.raises(InputError): _validate_constraint(Tokens(), constraint='mixed') def test_validate_workflow_ids_basic(tmp_run_dir): _validate_workflow_ids(Tokens('workflow'), src_path='') with pytest.raises(InputError): _validate_workflow_ids(Tokens('~alice/workflow'), src_path='') run_dir = tmp_run_dir('b') with pytest.raises(InputError): _validate_workflow_ids( Tokens('workflow'), src_path=run_dir / 'flow.cylc', ) def test_validate_workflow_ids_warning(caplog): """It should warn when the run number is provided as a cycle point.""" caplog.set_level(logging.WARN, CYLC_LOG) _validate_workflow_ids(Tokens('workflow/run1//cycle/task'), src_path='') assert caplog.messages == [] _validate_workflow_ids(Tokens('workflow//run1'), src_path='') assert caplog.messages == ['Did you mean: workflow/run1'] caplog.clear() _validate_workflow_ids(Tokens('workflow//run1/cycle/task'), src_path='') assert caplog.messages == ['Did you mean: workflow/run1//cycle/task'] def test_validate_number(): _validate_number(Tokens('a'), max_workflows=1) with pytest.raises(InputError): _validate_number(Tokens('a'), Tokens('b'), max_workflows=1) t1 = Tokens(cycle='1') t2 = Tokens(cycle='2') _validate_number(t1, max_tasks=1) with pytest.raises(InputError): _validate_number(t1, t2, max_tasks=1) _validate_number(t1, max_tasks=1) _validate_number(Tokens('a//1'), Tokens('a//2'), max_workflows=1) _validate_number( Tokens('a'), Tokens('//2'), Tokens('//3'), max_workflows=1 ) with pytest.raises(InputError): _validate_number(Tokens('a//1'), Tokens('b//1'), max_workflows=1) _validate_number(Tokens('a//1'), Tokens('b//1'), max_workflows=2) @pytest.fixture def no_scan(monkeypatch): """Disable the filesystem part of scan.""" @pipe async def _scan(): # something that looks like scan but doesn't do anything yield monkeypatch.setattr('cylc.flow.network.scan.scan', _scan) async def test_expand_workflow_tokens_impl_selector(no_scan): """It should reject filters it can't handle.""" tokens = tokenise('~user/*') await _expand_workflow_tokens([tokens]) tokens = tokens.duplicate(workflow_sel='stopped') with pytest.raises(InputError): await _expand_workflow_tokens([tokens]) @pytest.mark.parametrize('identifier, expected', [ ( '//2024-01-01T00:fail/a', {'cycle': '2024-01-01T00', 'cycle_sel': 'fail', 'task': 'a'} ), ( '//2024-01-01T00:00Z/a', {'cycle': '2024-01-01T00:00Z', 'task': 'a'} ), ( '//2024-01-01T00:00Z:fail/a', {'cycle': '2024-01-01T00:00Z', 'cycle_sel': 'fail', 'task': 'a'} ), ( '//2024-01-01T00:00:00+05:30/a', {'cycle': '2024-01-01T00:00:00+05:30', 'task': 'a'} ), ( '//2024-01-01T00:00:00+05:30:f/a', {'cycle': '2024-01-01T00:00:00+05:30', 'cycle_sel': 'f', 'task': 'a'} ), ( # Nonsensical example, but whatever... '//2024-01-01T00:00Z:00Z/a', {'cycle': '2024-01-01T00:00Z', 'cycle_sel': '00Z', 'task': 'a'} ) ]) def test_iso_long_fmt(identifier, expected): assert { k: v for k, v in cli_tokenise(identifier).items() if v is not None } == expected cylc-flow-8.6.4/tests/unit/test_xtrigger_mgr.py0000664000175000017500000002555515202510242022053 0ustar alastairalastair# 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 . import logging import pytest from cylc.flow import CYLC_LOG from cylc.flow.cycling.iso8601 import ISO8601Point, ISO8601Sequence, init from cylc.flow.exceptions import XtriggerConfigError from cylc.flow.id import Tokens from cylc.flow.subprocctx import SubFuncContext from cylc.flow.task_proxy import TaskProxy from cylc.flow.taskdef import TaskDef from cylc.flow.xtrigger_mgr import ( RE_STR_TMPL, XTRIG_DUP_WARNING, XtriggerCollator ) def test_extract_templates(): """Test escaped templates in xtrigger arg string. They should be left alone and passed into the function as string literals, not identified as template args. """ assert ( RE_STR_TMPL.findall('%(cat)s, %(dog)s, %%(fish)s') == ['cat', 'dog'] ) def test_add_missing_func(): """Test for adding an xtrigger that can't be found.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="fooble", func_name="fooble123", # no such module func_args=["name", "age"], func_kwargs={"location": "soweto"} ) with pytest.raises( XtriggerConfigError, match=r"\[@xtrig\] fooble123\(.*\)\nNo module named 'fooble123'" ): xtriggers.add_trig("xtrig", xtrig, 'fdir') def test_add_xtrigger(): """Test for adding and validating an xtrigger.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="echo", func_name="echo", func_args=["name", "age"], func_kwargs={"location": "soweto"} ) with pytest.raises( XtriggerConfigError, match="Requires 'succeed=True/False' arg" ): xtriggers.add_trig("xtrig", xtrig, 'fdir') xtrig = SubFuncContext( label="echo", func_name="echo", func_args=["name", "age"], func_kwargs={"location": "soweto", "succeed": True} ) xtriggers.add_trig("xtrig", xtrig, 'fdir') assert xtrig == xtriggers.functx_map["xtrig"] def test_add_xtrigger_with_template_good(): """Test adding an xtrigger with a valid string template arg value.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="echo", func_name="echo", func_args=["name", "%(point)s"], # valid template func_kwargs={"location": "soweto", "succeed": True} ) xtriggers.add_trig("xtrig", xtrig, 'fdir') assert xtrig == xtriggers.functx_map["xtrig"] def test_add_xtrigger_with_template_bad(): """Test adding an xtrigger with an invalid string template arg value.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="echo", func_name="echo", func_args=["name", "%(point)s"], # invalid template: func_kwargs={"location": "%(what_is_this)s", "succeed": True} ) with pytest.raises( XtriggerConfigError, match="Illegal template in xtrigger: what_is_this" ): xtriggers.add_trig("xtrig", xtrig, 'fdir') def test_add_xtrigger_with_deprecated_params( caplog: pytest.LogCaptureFixture ): """It should flag deprecated template variables.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="echo", func_name="echo", func_args=[1, "name", "%(suite_name)s"], func_kwargs={"succeed": True} ) caplog.set_level(logging.WARNING, CYLC_LOG) xtriggers.add_trig("xtrig", xtrig, 'fdir') assert caplog.messages == [ 'Xtrigger "xtrig" uses deprecated template variables: suite_name' ] def test_load_xtrigger_for_restart(xtrigger_mgr): """Test loading an xtrigger for restart. The function is loaded from database, where the value is formatted as JSON.""" row = "get_name", "{\"name\": \"function\"}" xtrigger_mgr.load_xtrigger_for_restart(row_idx=0, row=row) assert xtrigger_mgr.sat_xtrig["get_name"]["name"] == "function" def test_load_invalid_xtrigger_for_restart(xtrigger_mgr): """Test loading an invalid xtrigger for restart. It simulates that the DB has a value that is not valid JSON. """ row = "get_name", "{name: \"function\"}" # missing double quotes with pytest.raises(ValueError): xtrigger_mgr.load_xtrigger_for_restart(row_idx=0, row=row) def test_housekeeping_nothing_satisfied(xtrigger_mgr): """The housekeeping method makes sure only satisfied xtrigger function are kept.""" row = "get_name", "{\"name\": \"function\"}" # now XtriggerManager#sat_xtrigger will contain the get_name xtrigger xtrigger_mgr.add_xtriggers(XtriggerCollator()) xtrigger_mgr.load_xtrigger_for_restart(row_idx=0, row=row) assert xtrigger_mgr.sat_xtrig xtrigger_mgr.housekeep([]) assert not xtrigger_mgr.sat_xtrig def test_housekeeping_with_xtrigger_satisfied(xtrigger_mgr): """The housekeeping method makes sure only satisfied xtrigger function are kept.""" xtriggers = XtriggerCollator() xtrig = SubFuncContext( label="get_name", func_name="echo", func_args=[], func_kwargs={"succeed": True} ) xtriggers.add_trig("get_name", xtrig, 'fdir') xtrigger_mgr.add_xtriggers(xtriggers) xtrig.out = "[\"True\", {\"name\": \"Yossarian\"}]" tdef = TaskDef( name="foo", rtcfg={'completion': None}, start_point=1, initial_point=1, ) init() sequence = ISO8601Sequence('P1D', '2019') tdef.xtrig_labels[sequence] = ["get_name"] start_point = ISO8601Point('2019') itask = TaskProxy(Tokens('~user/workflow'), tdef, start_point) # pretend the function has been activated xtrigger_mgr.active.append(xtrig.get_signature()) xtrigger_mgr.callback(xtrig) assert xtrigger_mgr.sat_xtrig xtrigger_mgr.housekeep([itask]) # here we still have the same number as before assert xtrigger_mgr.sat_xtrig def test__call_xtriggers_async(xtrigger_mgr): """Test _call_xtriggers_async""" xtriggers = XtriggerCollator() # the echo1 xtrig (not satisfied) echo1_xtrig = SubFuncContext( label="echo1", func_name="echo", func_args=[], func_kwargs={"succeed": False} ) echo1_xtrig.out = "[\"True\", {\"name\": \"herminia\"}]" xtriggers.add_trig("echo1", echo1_xtrig, "fdir") # the echo2 xtrig (satisfied through callback later) echo2_xtrig = SubFuncContext( label="echo2", func_name="echo", func_args=[], func_kwargs={"succeed": True} ) echo2_xtrig.out = "[\"True\", {\"name\": \"herminia\"}]" xtriggers.add_trig("echo2", echo2_xtrig, "fdir") xtrigger_mgr.add_xtriggers(xtriggers) # create a task tdef = TaskDef( name="foo", rtcfg={'completion': None}, start_point=1, initial_point=1 ) init() sequence = ISO8601Sequence('P1D', '2000') tdef.xtrig_labels[sequence] = ["echo1", "echo2"] # cycle point for task proxy init() start_point = ISO8601Point('2019') # create task proxy itask = TaskProxy(Tokens('~user/workflow'), tdef, start_point) # we start with no satisfied xtriggers, and nothing active assert len(xtrigger_mgr.sat_xtrig) == 0 assert len(xtrigger_mgr.active) == 0 # after calling the first time, we get two active xtrigger_mgr.call_xtriggers_async(itask) assert len(xtrigger_mgr.sat_xtrig) == 0 assert len(xtrigger_mgr.active) == 2 # calling again does not change anything xtrigger_mgr.call_xtriggers_async(itask) assert len(xtrigger_mgr.sat_xtrig) == 0 assert len(xtrigger_mgr.active) == 2 # now we call callback manually as the proc_pool we passed is a mock # then both should be satisfied xtrigger_mgr.callback(echo1_xtrig) xtrigger_mgr.callback(echo2_xtrig) # so both were satisfied, and nothing is active assert len(xtrigger_mgr.sat_xtrig) == 2 assert len(xtrigger_mgr.active) == 0 # calling satisfy_xtriggers again still does not change anything xtrigger_mgr.call_xtriggers_async(itask) assert len(xtrigger_mgr.sat_xtrig) == 2 assert len(xtrigger_mgr.active) == 0 def test_callback_not_active(xtrigger_mgr): """Test callback with no active contexts.""" # calling callback with a SubFuncContext with none active # results in a ValueError get_name = SubFuncContext( label="get_name", func_name="get_name", func_args=[], func_kwargs={} ) with pytest.raises(ValueError): xtrigger_mgr.callback(get_name) def test_callback_invalid_json(xtrigger_mgr): """Test callback with invalid JSON.""" get_name = SubFuncContext( label="get_name", func_name="get_name", func_args=[], func_kwargs={} ) get_name.out = "{no_quotes: \"mom!\"}" xtrigger_mgr.active.append(get_name.get_signature()) xtrigger_mgr.callback(get_name) # this means that the xtrigger was not satisfied # TODO: this means site admins are only aware of this if they # look at the debug log. Is that OK? assert not xtrigger_mgr.sat_xtrig def test_callback(xtrigger_mgr): """Test callback.""" get_name = SubFuncContext( label="get_name", func_name="get_name", func_args=[], func_kwargs={} ) get_name.out = "[\"True\", \"1\"]" xtrigger_mgr.active.append(get_name.get_signature()) xtrigger_mgr.callback(get_name) # this means that the xtrigger was satisfied assert xtrigger_mgr.sat_xtrig def test_report_duplicates( caplog: pytest.LogCaptureFixture ): x1 = SubFuncContext( label="x1", func_name="echo", func_args=[], func_kwargs={"succeed": False} ) x2 = SubFuncContext( label="x2", func_name="echo", func_args=[], func_kwargs={"succeed": False} ) y = SubFuncContext( label="y", func_name="echo", func_args=["arg1"], func_kwargs={"succeed": False} ) # x1 and x2 both have the same function signature. xtriggers = XtriggerCollator() xtriggers.functx_map = { 'x1': x1, 'x2': x2, 'y': y } caplog.set_level(logging.INFO, CYLC_LOG) xtriggers.report_duplicates() assert caplog.messages == [ "Duplicate xtriggers: x1, x2 = echo(succeed=False)", XTRIG_DUP_WARNING ] cylc-flow-8.6.4/tests/unit/test_host_select_remote.py0000664000175000017500000001403215202510242023226 0ustar alastairalastair# 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 . """Test the cylc.flow.host_select module with hosts. NOTE: These tests require a remote host to work with and are skipped unless one is provided. NOTE: These are functional tests, for unit tests see the docstrings in the host_select module. """ from shlex import quote import socket from subprocess import call, DEVNULL import pytest from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.exceptions import HostSelectException from cylc.flow.host_select import ( select_host, select_workflow_host ) from cylc.flow.hostuserutil import get_fqdn_by_host local_host, local_host_alises, _ = socket.gethostbyname_ex('localhost') local_host_fqdn = get_fqdn_by_host(local_host) try: # get a suitable remote host for running tests on # NOTE: do NOT copy this testing approach in other python tests remote_platform = glbl_cfg().get( ['platforms', '_remote_background_shared_tcp', 'hosts'], [] )[0] # don't run tests unless host is contactable if call( ['ssh', quote(remote_platform), 'hostname'], stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULL ): raise KeyError('remote platform') # get the fqdn for this host remote_platform_fqdn = get_fqdn_by_host(remote_platform) except (KeyError, IndexError): pytest.skip('Remote test host not available', allow_module_level=True) remote_platform = None def test_remote_select(): """Test host selection works with remote host names.""" assert select_host([remote_platform]) == ( remote_platform, remote_platform_fqdn ) def test_remote_blacklict(): """Test that blacklisting works with remote host names.""" # blacklist by fqdn with pytest.raises(HostSelectException): select_host( [remote_platform], blacklist=[remote_platform] ) # blacklist by short name with pytest.raises(HostSelectException): select_host( [remote_platform], blacklist=[remote_platform_fqdn] ) # make extra sure filters are really being applied for _ in range(10): assert select_host( [remote_platform, local_host], blacklist=[remote_platform] ) == (local_host, local_host_fqdn) def test_remote_rankings(): """Test that ranking evaluation works on hosts (via SSH).""" assert select_host( [remote_platform], ranking_string=''' # if this test fails due to race conditions # then you have bigger issues than a test failure virtual_memory().available > 1 getloadavg()[0] < 500 cpu_count() > 1 disk_usage('/').free > 1 ''' ) == (remote_platform, remote_platform_fqdn) def test_remote_exclude(monkeypatch): """Ensure that hosts get excluded if they don't meet the rankings. Already tested elsewhere but this double-checks that it works if more than one host is provided to choose from.""" def mocked_get_metrics(hosts, metrics, _=None): # pretend that ssh to remote_platform failed return {f'{local_host_fqdn}': {('cpu_count',): 123}} monkeypatch.setattr( 'cylc.flow.host_select._get_metrics', mocked_get_metrics ) assert select_host( [local_host, remote_platform], ranking_string=''' cpu_count() ''' ) == (local_host, local_host_fqdn) def test_remote_workflow_host_select(mock_glbl_cfg): """test [scheduler][run hosts]available""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {remote_platform} ''' ) assert select_workflow_host() == (remote_platform, remote_platform_fqdn) def test_remote_workflow_host_condemned(mock_glbl_cfg): """test [scheduler][run hosts]condemned hosts""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {remote_platform}, {local_host} condemned = {remote_platform} ''' ) for _ in range(10): assert select_workflow_host() == (local_host, local_host_fqdn) def test_remote_workflow_host_rankings(mock_glbl_cfg): """test [scheduler][run hosts]rankings""" mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {remote_platform} ranking = """ # if this test fails due to race conditions # then you are very lucky virtual_memory().available > 123456789123456789 cpu_count() > 512 disk_usage('/').free > 123456789123456789 """ ''' ) with pytest.raises(HostSelectException) as excinfo: select_workflow_host() # ensure that host selection actually evaluated rankings assert set(excinfo.value.data[remote_platform_fqdn]) - {'returncode'} == { 'virtual_memory().available > 123456789123456789', 'cpu_count() > 512', "disk_usage('/').free > 123456789123456789" } # ensure that none of the rankings passed assert not any(excinfo.value.data[remote_platform_fqdn].values()) cylc-flow-8.6.4/tests/unit/test_c3mro.py0000664000175000017500000000517215202510242020367 0ustar alastairalastair# 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 . import unittest from cylc.flow.c3mro import C3 class TestC3mro(unittest.TestCase): def test_tree_is_empty_by_default(self): c3 = C3() self.assertFalse(c3.tree) def test_tree_parameter(self): c3 = C3({'a': 1}) self.assertTrue(c3.tree) def test_simple_inheritance(self): parents = {} parents['object'] = [] parents['string'] = ['object'] c3 = C3(parents) string_hierarchy = c3.mro('string') self.assertEqual(['string', 'object'], string_hierarchy) def test_simple_inheritance_extra_nodes(self): parents = {} parents['object'] = [] parents['string'] = ['object'] # nodes not related to string parents['root'] = [] parents['diamond'] = ['root'] c3 = C3(parents) self.assertEqual(['string', 'object'], c3.mro('string')) self.assertEqual(['diamond', 'root'], c3.mro('diamond')) def test_empty_tree_key_error(self): parents = {} c3 = C3(parents) with self.assertRaises(KeyError): c3.mro('test') def test_multiple_inheritance_error_py23(self): parents = {} parents['object'] = [] parents['x'] = ['object'] parents['y'] = ['object'] parents['a'] = ['x', 'y'] parents['b'] = ['y', 'x'] parents['z'] = ['a', 'b'] c3 = C3(parents) # see class docstring, this is the case #2 with self.assertRaises(Exception) as cm: c3.mro('z') self.assertTrue("ERROR: z: bad runtime namespace inheritance hierarchy" in str(cm.exception)) def test_mro_of_none(self): with self.assertRaises(Exception) as cm: C3.merge([[], ['x', 'y', 'o'], ['y', 'x', 'o'], []], None) self.assertTrue("ERROR: bad runtime namespace inheritance hierarchy" in str(cm.exception)) cylc-flow-8.6.4/tests/unit/test_dbstatecheck.py0000664000175000017500000000320115202510242021757 0ustar alastairalastair# 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 . from cylc.flow.dbstatecheck import check_polling_config from cylc.flow.exceptions import InputError import pytest def test_check_polling_config(): """It should reject invalid or unreliable polling configurations. See https://github.com/cylc/cylc-flow/issues/6157 """ # invalid polling use cases with pytest.raises(InputError, match='No such task state'): check_polling_config('elephant', False, False) with pytest.raises(InputError, match='Cannot poll for'): check_polling_config('waiting', False, False) with pytest.raises(InputError, match='is not reliable'): check_polling_config('running', False, False) # valid polling use cases check_polling_config('started', True, False) check_polling_config('started', False, True) # valid query use cases check_polling_config(None, False, True) check_polling_config(None, False, False) cylc-flow-8.6.4/tests/unit/test_workflow_events.py0000664000175000017500000000654615202510242022610 0ustar alastairalastair# 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 . """ tests for functions in cylc.flow.workflow_events.py """ import pytest from types import SimpleNamespace from cylc.flow.workflow_events import ( WorkflowEventHandler, get_template_variables, process_mail_footer, ) @pytest.mark.parametrize( 'key, workflow_cfg, glbl_cfg, expected', [ ('handlers', True, True, ['stall']), ('handlers', False, True, None), ('handlers', False, False, None), ('mail events', True, True, []), ('mail events', False, True, ['abort']), ('mail events', False, False, None), ('from', True, True, 'docklands@railway'), ('from', False, True, 'highway@mixture'), ('from', False, False, None), ('abort on workflow timeout', True, True, True), ('abort on workflow timeout', False, True, True), ('abort on workflow timeout', False, False, False), ] ) def test_get_events_handler( mock_glbl_cfg, key, workflow_cfg, glbl_cfg, expected ): """Test order of precedence for getting event handler configuration.""" mock_glbl_cfg( 'cylc.flow.workflow_events.glbl_cfg', ( ''' [scheduler] [[mail]] from = highway@mixture [[events]] abort on workflow timeout = True mail events = abort ''' if glbl_cfg else '' ), ) config = SimpleNamespace() config.cfg = { 'scheduler': { 'events': {'handlers': ['stall'], 'mail events': []}, 'mail': {'from': 'docklands@railway'}, } if workflow_cfg else {'events': {}} } assert WorkflowEventHandler.get_events_conf(config, key) == expected def test_process_mail_footer(caplog, log_filter): schd = SimpleNamespace( config=SimpleNamespace(cfg={'meta': {}}), host='myhost', owner='me', server=SimpleNamespace(port=42), uuid_str=None, workflow='my_workflow', ) template_vars = get_template_variables(schd, '', '') # test all variables assert process_mail_footer( '%(host)s|%(port)s|%(owner)s|%(suite)s|%(workflow)s', template_vars ) == 'myhost|42|me|my_workflow|my_workflow\n' assert not log_filter(contains='Ignoring bad mail footer template') # test invalid variable assert process_mail_footer('%(invalid)s', template_vars) == '' assert log_filter(contains='Ignoring bad mail footer template') # test broken template caplog.clear() assert process_mail_footer('%(invalid)s', template_vars) == '' assert log_filter(contains='Ignoring bad mail footer template') cylc-flow-8.6.4/tests/unit/cycling/0000775000175000017500000000000015202510242017356 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/cycling/__init__.py0000664000175000017500000000135715202510242021475 0ustar alastairalastair# 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 . cylc-flow-8.6.4/tests/unit/cycling/test_integer.py0000664000175000017500000002346015202510242022431 0ustar alastairalastair# 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 . import pytest from cylc.flow.cycling.integer import ( IntegerSequence, IntegerPoint, IntegerInterval, IntervalParsingError, SequenceParsingError, ) from cylc.flow.cycling.iso8601 import ( ISO8601Point, ISO8601Interval, ) from cylc.flow.exceptions import CyclerTypeError def test_exclusions_simple(): """Test the generation of points for integer sequences with exclusions. """ sequence = IntegerSequence('R/P1!3', 1, 5) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [1, 2, 4, 5] def test_multiple_exclusions_simple(): """Tests the multiple exclusion syntax for integer notation""" sequence = IntegerSequence('R/P1!(2,3,7)', 1, 10) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [1, 4, 5, 6, 8, 9, 10] # duplicate excluded points should be ignored sequence1 = IntegerSequence('R/P1!(2,2,2)', 1, 10) sequence2 = IntegerSequence('R/P1!(2)', 1, 10) assert ( sequence1.exclusions.exclusion_points ) == sequence2.exclusions.exclusion_points def test_multiple_exclusions_integer_sequence(): """Tests the multiple exclusion syntax for integer notation""" sequence = IntegerSequence('P1 ! P2', 1, 10) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [2, 4, 6, 8, 10] assert sequence.exclusions[0] == IntegerSequence('P2', 1, 10) def test_multiple_exclusions_integer_sequence2(): """Tests the multiple exclusion syntax for integer notation""" sequence = IntegerSequence('P1 ! +P1/P2', 1, 10) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [1, 3, 5, 7, 9] def test_multiple_exclusions_integer_sequence3(): """Tests the multiple exclusion syntax for integer notation""" sequence = IntegerSequence('P1 ! (P2, 6, 8) ', 1, 10) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [2, 4, 10] def test_multiple_exclusions_integer_sequence_weird_valid_formatting(): """Tests the multiple exclusion syntax for integer notation""" sequence = IntegerSequence('P1 !(P2, 6,8) ', 1, 10) output = [] point = sequence.get_start_point() while point: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [2, 4, 10] def test_multiple_exclusions_integer_sequence_invalid_formatting(): """Tests the multiple exclusion syntax for integer notation""" with pytest.raises(Exception): IntegerSequence('P1 !(6,8), P2 ', 1, 10) def test_multiple_exclusions_extensive(): """Tests IntegerSequence methods for sequences with multi-exclusions""" points = [IntegerPoint(i) for i in range(10)] sequence = IntegerSequence('R/P1!(2,3,7)', 1, 10) assert not sequence.is_on_sequence(points[3]) assert not sequence.is_valid(points[3]) assert sequence.get_prev_point(points[3]) == points[1] assert sequence.get_prev_point(points[4]) == points[1] assert sequence.get_nearest_prev_point(points[3]) == points[1] assert sequence.get_nearest_prev_point(points[4]) == points[1] assert sequence.get_next_point(points[3]) == points[4] assert sequence.get_next_point(points[2]) == points[4] assert sequence.get_next_point_on_sequence(points[3]) == points[4] assert sequence.get_next_point_on_sequence(points[6]) == points[8] sequence = IntegerSequence('R/P1!(1,3,4)', 1, 10) assert sequence.get_first_point(points[1]) == points[2] assert sequence.get_first_point(points[0]) == points[2] assert sequence.get_start_point() == points[2] sequence = IntegerSequence('R/P1!(8,9,10)', 1, 10) assert sequence.get_stop_point() == points[7] def test_exclusions_extensive(): """Test IntegerSequence methods for sequences with exclusions.""" point_0 = IntegerPoint(0) point_1 = IntegerPoint(1) point_2 = IntegerPoint(2) point_3 = IntegerPoint(3) point_4 = IntegerPoint(4) sequence = IntegerSequence('R/P1!3', 1, 5) assert not sequence.is_on_sequence(point_3) assert not sequence.is_valid(point_3) assert sequence.get_prev_point(point_3) == point_2 assert sequence.get_prev_point(point_4) == point_2 assert sequence.get_nearest_prev_point(point_3) == point_2 assert sequence.get_nearest_prev_point(point_3) == point_2 assert sequence.get_next_point(point_3) == point_4 assert sequence.get_next_point(point_2) == point_4 assert sequence.get_next_point_on_sequence(point_3) == point_4 assert sequence.get_next_point_on_sequence(point_2) == point_4 sequence = IntegerSequence('R/P1!1', 1, 5) assert sequence.get_first_point(point_1) == point_2 assert sequence.get_first_point(point_0) == point_2 assert sequence.get_start_point() == point_2 sequence = IntegerSequence('R/P1!5', 1, 5) assert sequence.get_stop_point() == point_4 def test_simple(): """Run some simple tests for integer cycling.""" sequence = IntegerSequence('R/1/P3', 1, 10) start = sequence.p_start stop = sequence.p_stop # Test point generation forwards. point = start output = [] while point and stop and point <= stop: output.append(point) point = sequence.get_next_point(point) assert [int(out) for out in output] == [1, 4, 7, 10] # Test point generation backwards. point = stop output = [] while point and start and point >= start: output.append(point) point = sequence.get_prev_point(point) assert [int(out) for out in output] == [10, 7, 4, 1] # Test sequence comparison sequence1 = IntegerSequence('R/1/P2', 1, 10) sequence2 = IntegerSequence('R/1/P2', 1, 10) assert sequence1 == sequence2 sequence2.set_offset(IntegerInterval('-P2')) assert sequence1 == sequence2 sequence2.set_offset(IntegerInterval('-P1')) assert sequence1 != sequence2 def test_interval_parsing_error(): """It should reject invalid intervals.""" with pytest.raises(IntervalParsingError): IntegerInterval(42) with pytest.raises(IntervalParsingError): IntegerInterval('forty two') def test_sequence_parsing_error(): with pytest.raises(SequenceParsingError): IntegerSequence('zz0+za', 1) def test_interval_arithmetic(): """It should do basic maths on integer intervals.""" a = IntegerInterval('P2') b = IntegerInterval('P3') p = IntegerPoint('3') assert a + b == IntegerInterval('P5') assert a + p == IntegerPoint('5') assert a - b == IntegerInterval('-P1') assert a - p == IntegerPoint('-1') assert abs(IntegerInterval('-P2')) == IntegerInterval('P2') assert a + IntegerInterval.get_null_offset() == a with pytest.raises(CyclerTypeError): IntegerPoint(1000) - ISO8601Point('1000') with pytest.raises(CyclerTypeError): IntegerPoint(1000) + ISO8601Point('1000') with pytest.raises(CyclerTypeError): IntegerInterval('P1') - ISO8601Interval('P1Y') with pytest.raises(CyclerTypeError): IntegerInterval('P1') + ISO8601Interval('P1Y') def test_async_expr(): """The async expression should run once and only once.""" point = IntegerPoint('5') sequence = IntegerSequence(IntegerSequence.get_async_expr(point), 1, 10) assert sequence.get_next_point(IntegerPoint('1')) == point assert sequence.get_next_point(IntegerPoint('5')) is None def test_point_comparisons(): # basic comparisons assert IntegerPoint(1) == IntegerPoint(1) assert IntegerPoint(1) < IntegerPoint(2) assert IntegerPoint(1) <= IntegerPoint(2) assert IntegerPoint(2) >= IntegerPoint(1) assert IntegerPoint(1) != IntegerPoint(2) assert IntegerInterval('P1') == IntegerInterval('P1') assert IntegerInterval('P1') < IntegerInterval('P2') assert IntegerInterval('P1') <= IntegerInterval('P2') assert IntegerInterval('P2') >= IntegerInterval('P1') assert IntegerInterval('P1') != IntegerInterval('P2') # None comparisons work counter intuitively # (reason unknown) assert IntegerPoint(1) < None assert None > IntegerPoint(1) assert IntegerInterval('P1') < None assert None > IntegerInterval('P1') # compare against other PointBase implementations assert ISO8601Point('1000') > IntegerPoint(1000) assert IntegerPoint(1000) < ISO8601Point('1000') assert ISO8601Interval('P1Y') > IntegerInterval('P1') assert IntegerInterval('P1') < ISO8601Interval('P1Y') def test_string_representations(): p = IntegerPoint(1) assert str(p) == '1' assert repr(p) == '1' i = IntegerInterval('P1') assert str(i) == 'P1' assert repr(i) == '' cylc-flow-8.6.4/tests/unit/cycling/test_cycling.py0000664000175000017500000000456615202510242022432 0ustar alastairalastair# 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 . import pytest from cylc.flow.cycling import ( SequenceBase, IntervalBase, PointBase, parse_exclusion, ) def test_simple_abstract_class_test(): """Cannot instantiate abstract classes, they must be defined in the subclasses""" with pytest.raises(TypeError): SequenceBase('sequence-string', 'context_string') with pytest.raises(TypeError): IntervalBase('value') with pytest.raises(TypeError): PointBase('value') def test_parse_exclusion_simple(): """Tests the simple case of exclusion parsing""" expression = "PT1H!20000101T02Z" sequence, exclusion = parse_exclusion(expression) assert sequence == "PT1H" assert exclusion == ['20000101T02Z'] def test_parse_exclusions_list(): """Tests the simple case of exclusion parsing""" expression = "PT1H!(T03, T06, T09)" sequence, exclusion = parse_exclusion(expression) assert sequence == "PT1H" assert exclusion == ['T03', 'T06', 'T09'] def test_parse_exclusions_list_spaces(): """Tests the simple case of exclusion parsing""" expression = "PT1H! (T03, T06, T09) " sequence, exclusion = parse_exclusion(expression) assert sequence == "PT1H" assert exclusion == ['T03', 'T06', 'T09'] @pytest.mark.parametrize( 'expression', [ 'T01/PT1H!(T06, T09), PT5M', 'T01/PT1H!T03, PT17H, (T06, T09), PT5M', 'T01/PT1H! PT8H, (T06, T09)', 'T01/PT1H! T03, T06, T09', 'T01/PT1H !T03 !T06', ], ) def test_parse_bad_exclusion(expression): """Tests incorrectly formatted exclusions""" with pytest.raises(Exception): parse_exclusion(expression) cylc-flow-8.6.4/tests/unit/cycling/test_iso8601.py0000664000175000017500000007532515202510242022114 0ustar alastairalastair# 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 . from datetime import datetime import pytest from pytest import param from cylc.flow.cycling.iso8601 import ( ISO8601Interval, ISO8601Point, ISO8601Sequence, ingest_time, ) from cylc.flow.cycling.loader import ISO8601_CYCLING_TYPE def test_exclusions_simple(set_cycling_type): """Test the generation of points for sequences with exclusions.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") sequence = ISO8601Sequence("PT1H!20000101T02Z", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 while point and count < 4: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0000Z", "20000101T0100Z", "20000101T0300Z", "20000101T0400Z", ] def test_exclusions_offset(set_cycling_type): """Test the generation of points for sequences with exclusions that have an offset on the end""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") sequence = ISO8601Sequence("PT1H!20000101T00Z+PT1H", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 while point and count < 4: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0000Z", "20000101T0200Z", "20000101T0300Z", "20000101T0400Z", ] def test_multiple_exclusions_complex1(set_cycling_type): """Tests sequences that have multiple exclusions and a more complicated format""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # A sequence that specifies a dep start time sequence = ISO8601Sequence( "20000101T01Z/PT1H!20000101T02Z", "20000101T01Z" ) output = [] point = sequence.get_start_point() count = 0 # We are going to make four sequence points while point and count < 4: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect one of the hours to be excluded: T02 assert output == [ "20000101T0100Z", "20000101T0300Z", "20000101T0400Z", "20000101T0500Z", ] def test_multiple_exclusions_complex2(set_cycling_type): """Tests sequences that have multiple exclusions and a more complicated format""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # A sequence that specifies a dep start time sequence = ISO8601Sequence( "20000101T01Z/PT1H!" "(20000101T02Z,20000101T03Z)", "20000101T00Z", "20000101T05Z", ) output = [] point = sequence.get_start_point() count = 0 # We are going to make four sequence points while point and count < 3: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect two of the hours to be excluded: T02, T03 assert output == [ "20000101T0100Z", "20000101T0400Z", "20000101T0500Z", ] def test_multiple_exclusions_simple(set_cycling_type): """Tests generation of points for sequences with multiple exclusions""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") sequence = ISO8601Sequence( "PT1H!(20000101T02Z,20000101T03Z)", "20000101T00Z" ) output = [] point = sequence.get_start_point() count = 0 # We are going to make four sequence points while point and count < 4: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect two of the hours to be excluded: T02 and T03 assert output == [ "20000101T0000Z", "20000101T0100Z", "20000101T0400Z", "20000101T0500Z", ] def test_advanced_exclusions_partial_datetime1(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run 3-hourly but not at 06:00 (from the ICP) sequence = ISO8601Sequence("PT3H!T06", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make ten sequence points while point and count < 10: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect every T06 to be excluded assert output == [ "20000101T0000Z", "20000101T0300Z", "20000101T0900Z", "20000101T1200Z", "20000101T1500Z", "20000101T1800Z", "20000101T2100Z", "20000102T0000Z", "20000102T0300Z", "20000102T0900Z", ] def test_advanced_exclusions_partial_datetime2(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run hourly but not at 00:00, 06:00, 12:00, 18:00 sequence = ISO8601Sequence("T-00!(T00, T06, T12, T18)", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make 18 sequence points while point and count < 18: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect T00, T06, T12, and T18 to be excluded assert output == [ "20000101T0100Z", "20000101T0200Z", "20000101T0300Z", "20000101T0400Z", "20000101T0500Z", "20000101T0700Z", "20000101T0800Z", "20000101T0900Z", "20000101T1000Z", "20000101T1100Z", "20000101T1300Z", "20000101T1400Z", "20000101T1500Z", "20000101T1600Z", "20000101T1700Z", "20000101T1900Z", "20000101T2000Z", "20000101T2100Z", ] def test_advanced_exclusions_partial_datetime3(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run 5 minutely but not at 15 minutes past the hour from ICP sequence = ISO8601Sequence("PT5M!T-15", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make 15 sequence points while point and count < 15: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect xx:15 (15 minutes past the hour) to be excluded assert output == [ "20000101T0000Z", "20000101T0005Z", "20000101T0010Z", "20000101T0020Z", "20000101T0025Z", "20000101T0030Z", "20000101T0035Z", "20000101T0040Z", "20000101T0045Z", "20000101T0050Z", "20000101T0055Z", "20000101T0100Z", "20000101T0105Z", "20000101T0110Z", "20000101T0120Z", ] def test_advanced_exclusions_partial_datetime4(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run daily at 00:00 except on Mondays sequence = ISO8601Sequence("T00!W-1T00", "20170422T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make 19 sequence points while point and count < 9: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect Monday 24th April and Monday 1st May # to be excluded. assert output == [ "20170422T0000Z", "20170423T0000Z", "20170425T0000Z", "20170426T0000Z", "20170427T0000Z", "20170428T0000Z", "20170429T0000Z", "20170430T0000Z", "20170502T0000Z", ] def test_exclusions_to_string(set_cycling_type): set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Check that exclusions are not included where they should not be. basic = ISO8601Sequence("PT1H", "2000", "2001") assert "!" not in str(basic) # Check that exclusions are parsable. sequence = ISO8601Sequence("PT1H!(20000101T10Z, PT6H)", "2000", "2001") sequence2 = ISO8601Sequence(str(sequence), "2000", "2001") assert sequence == sequence2 def test_advanced_exclusions_sequences1(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run hourly from the ICP but not 3-hourly sequence = ISO8601Sequence("PT1H!PT3H", "20000101T01Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] # We should expect to see hourly from ICP but not 3 hourly assert output == [ "20000101T0200Z", "20000101T0300Z", "20000101T0500Z", "20000101T0600Z", "20000101T0800Z", "20000101T0900Z", ] def test_advanced_exclusions_sequences2(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run hourly on the hour but not 3 hourly on the hour sequence = ISO8601Sequence("T-00!T-00/PT3H", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0100Z", "20000101T0200Z", "20000101T0400Z", "20000101T0500Z", "20000101T0700Z", "20000101T0800Z", ] def test_advanced_exclusions_sequences3(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run daily at 12:00 except every 3rd day sequence = ISO8601Sequence("T12!P3D", "20000101T12Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000102T1200Z", "20000103T1200Z", "20000105T1200Z", "20000106T1200Z", "20000108T1200Z", "20000109T1200Z", ] def test_advanced_exclusions_sequences4(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run every hour from 01:00 excluding every 3rd hour. sequence = ISO8601Sequence("T01/PT1H!+PT3H/PT3H", "20000101T01Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0100Z", "20000101T0200Z", "20000101T0300Z", "20000101T0500Z", "20000101T0600Z", "20000101T0800Z", ] def test_advanced_exclusions_sequences5(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run every hour from 01:00 excluding every 3rd hour. sequence = ISO8601Sequence("T-00 ! 2000", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0100Z", "20000101T0200Z", "20000101T0300Z", "20000101T0400Z", "20000101T0500Z", "20000101T0600Z", ] def test_advanced_exclusions_sequences_mix_points_sequences(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run every hour from 01:00 excluding every 3rd hour. sequence = ISO8601Sequence("T-00 ! (2000, PT2H)", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0100Z", "20000101T0300Z", "20000101T0500Z", "20000101T0700Z", "20000101T0900Z", "20000101T1100Z", ] def test_advanced_exclusions_sequences_implied_start_point(set_cycling_type): """Advanced exclusions refers to exclusions that are not just simple points but could be time periods or recurrences such as '!T06' or similar""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run every hour from 01:00 excluding every 3rd hour. sequence = ISO8601Sequence("T05/PT1H!PT3H", "20000101T00Z") output = [] point = sequence.get_start_point() count = 0 # We are going to make six sequence points while point and count < 6: output.append(point) point = sequence.get_next_point(point) count += 1 output = [str(out) for out in output] assert output == [ "20000101T0600Z", "20000101T0700Z", "20000101T0900Z", "20000101T1000Z", "20000101T1200Z", "20000101T1300Z", ] def test_point_init(): """It should error if inited with anything other than a string.""" ISO8601Point('1000') with pytest.raises(TypeError): ISO8601Point(1000) with pytest.raises(TypeError): ISO8601Interval(1000) def test_exclusions_sequences_points(set_cycling_type): """Test ISO8601Sequence methods for sequences with exclusions""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") # Run every hour from 01:00 excluding every 3rd hour sequence = ISO8601Sequence("T01/PT1H!PT3H", "20000101T01Z") point_0 = ISO8601Point("20000101T00Z") point_1 = ISO8601Point("20000101T01Z") point_2 = ISO8601Point("20000101T02Z") point_3 = ISO8601Point("20000101T03Z") point_4 = ISO8601Point("20000101T04Z") assert point_0 not in sequence.exclusions assert point_1 in sequence.exclusions assert sequence.is_on_sequence(point_2) assert sequence.is_on_sequence(point_3) assert not sequence.is_on_sequence(point_4) assert point_4 in sequence.exclusions def test_exclusions_extensive(set_cycling_type): """Test ISO8601Sequence methods for sequences with exclusions""" set_cycling_type(ISO8601_CYCLING_TYPE, "+05") sequence = ISO8601Sequence( "PT1H!20000101T02+05", "20000101T00", "20000101T05" ) point_0 = ISO8601Point("20000101T0000+05") point_1 = ISO8601Point("20000101T0100+05") point_2 = ISO8601Point("20000101T0200+05") # The excluded point. point_3 = ISO8601Point("20000101T0300+05") assert not sequence.is_on_sequence(point_2) assert not sequence.is_valid(point_2) assert sequence.get_prev_point(point_2) == point_1 assert sequence.get_prev_point(point_3) == point_1 assert sequence.get_prev_point(point_2) == point_1 assert sequence.get_nearest_prev_point(point_3) == point_1 assert sequence.get_next_point(point_1) == point_3 assert sequence.get_next_point(point_2) == point_3 sequence = ISO8601Sequence("PT1H!20000101T00+05", "20000101T00+05") assert sequence.get_first_point(point_0) == point_1 assert sequence.get_start_point() == point_1 def test_multiple_exclusions_extensive(set_cycling_type): """Test ISO8601Sequence methods for sequences with multiple exclusions""" set_cycling_type(ISO8601_CYCLING_TYPE, "+05") sequence = ISO8601Sequence( "PT1H!(20000101T02,20000101T03)", "20000101T00", "20000101T06" ) point_0 = ISO8601Point("20000101T0000+05") point_1 = ISO8601Point("20000101T0100+05") point_2 = ISO8601Point("20000101T0200+05") # First excluded point point_3 = ISO8601Point("20000101T0300+05") # Second excluded point point_4 = ISO8601Point("20000101T0400+05") # Check the excluded points are not on the sequence assert not sequence.is_on_sequence(point_2) assert not sequence.is_on_sequence(point_3) assert not sequence.is_valid(point_2) # Should be excluded assert not sequence.is_valid(point_3) # Should be excluded # Check that we can correctly retrieve previous points assert sequence.get_prev_point(point_2) == point_1 # Should skip two excluded points assert sequence.get_prev_point(point_4) == point_1 assert sequence.get_prev_point(point_2) == point_1 assert sequence.get_nearest_prev_point(point_4) == point_1 assert sequence.get_next_point(point_1) == point_4 assert sequence.get_next_point(point_3) == point_4 sequence = ISO8601Sequence("PT1H!20000101T00+05", "20000101T00") # Check that the first point is after 00. assert sequence.get_first_point(point_0) == point_1 assert sequence.get_start_point() == point_1 # Check a longer list of exclusions # Also note you can change the format of the exclusion list # (removing the parentheses) sequence = ISO8601Sequence( "PT1H!(20000101T02+05, 20000101T03+05," "20000101T04+05)", "20000101T00", "20000101T06", ) assert sequence.get_prev_point(point_3) == point_1 assert sequence.get_prev_point(point_4) == point_1 def test_exclusion_zero_duration_warning(set_cycling_type, caplog, log_filter): """It should not log zero-duration warnings for exclusion points. Exclusions may either be sequences or points. We first attempt to parse them as sequences, if this fails, we attempt to parse them as points. The zero-duration recurrence warning would be logged if we attempted to parse a point as a sequence. To avoid spurious warnings this should be turned off for exclusion parsing. """ # parsing a point as a sequences causes a zero-duration warning set_cycling_type(ISO8601_CYCLING_TYPE, "+05") with pytest.raises(Exception): ISO8601Sequence('3000', '2999') assert log_filter(contains='zero-duration') # parsing a point in an exclusion should not caplog.clear() ISO8601Sequence('P1Y ! 3000', '2999') assert not log_filter(contains='zero-duration') def test_simple(set_cycling_type): """Run some simple tests for date-time cycling.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") p_start = ISO8601Point("20100808T00") p_stop = ISO8601Point("20100808T02") i = ISO8601Interval("PT6H") assert p_start - i == ISO8601Point("20100807T18") assert p_stop + i == ISO8601Point("20100808T08") sequence = ISO8601Sequence( "PT10M", str(p_start), str(p_stop), ) sequence.set_offset(-ISO8601Interval("PT10M")) point = sequence.get_next_point(ISO8601Point("20100808T0000")) assert point == ISO8601Point("20100808T0010") output = [] # Test point generation forwards. while point and point < p_stop: output.append(point) assert sequence.is_on_sequence(point) point = sequence.get_next_point(point) assert [str(out) for out in output] == [ "20100808T0010Z", "20100808T0020Z", "20100808T0030Z", "20100808T0040Z", "20100808T0050Z", "20100808T0100Z", "20100808T0110Z", "20100808T0120Z", "20100808T0130Z", "20100808T0140Z", "20100808T0150Z", ] assert point == ISO8601Point("20100808T0200") # Test point generation backwards. output = [] while point and point >= p_start: output.append(point) assert sequence.is_on_sequence(point) point = sequence.get_prev_point(point) assert [str(out) for out in output] == [ "20100808T0200Z", "20100808T0150Z", "20100808T0140Z", "20100808T0130Z", "20100808T0120Z", "20100808T0110Z", "20100808T0100Z", "20100808T0050Z", "20100808T0040Z", "20100808T0030Z", "20100808T0020Z", "20100808T0010Z", "20100808T0000Z", ] assert not sequence.is_on_sequence(ISO8601Point("20100809T0005")) @pytest.mark.parametrize( 'value, expected', [ ('next(T2100Z)', '20100808T2100Z'), ('next(T00)', '20100809T0000Z'), ('next(T-15)', '20100808T1615Z'), ('next(T-45)', '20100808T1545Z'), ('next(-10)', '21100101T0000Z'), ('next(-1008)', '21100801T0000Z'), ('next(--10)', '20101001T0000Z'), ('next(--0325)', '20110325T0000Z'), ('next(---10)', '20100810T0000Z'), ('next(---05T1200Z)', '20100905T1200Z'), param('next(--08-08)', '20110808T0000Z', marks=pytest.mark.xfail), ('next(T15)', '20100809T1500Z'), ('next(T-41)', '20100808T1541Z'), ] ) def test_next_simple(value: str, expected: str, set_cycling_type): """Test the generation of CP using 'next' from single input.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2010-08-08T15:41Z" assert ingest_time(value, my_now) == expected @pytest.mark.parametrize( 'value, expected', [ ('previous(T2100Z)', '20100807T2100Z'), ('previous(T00)', '20100808T0000Z'), ('previous(T-15)', '20100808T1515Z'), ('previous(T-45)', '20100808T1445Z'), ('previous(-10)', '20100101T0000Z'), ('previous(-1008)', '20100801T0000Z'), ('previous(--10)', '20091001T0000Z'), ('previous(--0325)', '20100325T0000Z'), ('previous(---10)', '20100710T0000Z'), ('previous(---05T1200Z)', '20100805T1200Z'), param('previous(--08-08)', '20100808T0000Z', marks=pytest.mark.xfail), ('previous(T15)', '20100808T1500Z'), ('previous(T-41)', '20100808T1441Z'), ] ) def test_previous_simple(value: str, expected: str, set_cycling_type): """Test the generation of CP using 'previous' from single input.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2010-08-08T15:41Z" assert ingest_time(value, my_now) == expected def test_sequence(set_cycling_type): """Test the generation of CP from list input.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "20100808T1540Z" sequence = ( "next(T-00;T-15;T-30;T-45)", # 20100808T1545Z "previous(T-00;T-15;T-30;T-45)", # 20100808T1530Z "next(T00;T06;T12;T18)", # 20100808T1800Z "previous(T00;T06;T12;T18)", ) # 20100808T1200Z output = [] for point in sequence: output.append(ingest_time(point, my_now)) assert output == [ "20100808T1545Z", "20100808T1530Z", "20100808T1800Z", "20100808T1200Z", ] def test_offset_simple(set_cycling_type): """Test the generation of offset CP.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "20100808T1540Z" sequence = ( "PT15M", # 20100808T1555Z "-PT30M", # 20100808T1510Z "PT1H", # 20100808T1640Z "-PT18H", # 20100807T2140Z "P3D", # 20100811T1540Z "-P2W", # 20100725T1540Z "P6M", # 20110208T1540Z "-P1M", # 20100708T1540Z "P1Y", # 20110808T1540Z "-P5Y", # 20050808T1540Z ) output = [] for point in sequence: output.append(ingest_time(point, my_now)) assert output == [ "20100808T1555Z", "20100808T1510Z", "20100808T1640Z", "20100807T2140Z", "20100811T1540Z", "20100725T1540Z", "20110208T1540Z", "20100708T1540Z", "20110808T1540Z", "20050808T1540Z", ] def test_offset(set_cycling_type): """Test the generation of offset CP with 'next' and 'previous'.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "20100808T1540Z" sequence = ( "next(T06) +P1D", # 20100810T0600Z "previous(T-30) -PT12H", # 20100808T0330Z "next(T00;T06;T12;T18) -P1W", # 20100801T1800Z "previous(T00;T06;T12;T18) +PT1H", # 20100808T1300Z ) output = [] for point in sequence: output.append(ingest_time(point, my_now)) assert output == [ "20100810T0600Z", "20100808T0330Z", "20100801T1800Z", "20100808T1300Z", ] def test_weeks_days(set_cycling_type): """Test the generation of CP with day-of-week, ordinal day, and week (with day-of-week specified).""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "20100808T1540Z" sequence = ( "next(-W-1)", # 20100809T0000Z "previous(-W-4)", # 20100805T0000Z "next(-010)", # 20110110T0000Z "previous(-101)", # 20100411T0000Z "next(-W40-1)", # 20101004T0000Z "previous(-W05-1)", # 20100201T0000Z "next(-W05-5)", # 20110204T0000Z "previous(-W40-4)", # 20091001T0000Z ) output = [] for point in sequence: output.append(ingest_time(point, my_now)) assert output == [ "20100809T0000Z", "20100805T0000Z", "20110110T0000Z", "20100411T0000Z", "20101004T0000Z", "20100201T0000Z", "20110204T0000Z", "20091001T0000Z", ] @pytest.mark.parametrize( 'value, expected', [ ('next(T-00)', '20180314T1600Z'), ('previous(T-00)', '20180314T1500Z'), ('next(T-00; T-15; T-30; T-45)', '20180314T1515Z'), ('previous(T-00; T-15; T-30; T-45)', '20180314T1500Z'), ('next(T00)', '20180315T0000Z'), ('previous(T00)', '20180314T0000Z'), ('next(T06:30Z)', '20180315T0630Z'), ('previous(T06:30) -P1D', '20180313T0630Z'), ('next(T00; T06; T12; T18)', '20180314T1800Z'), ('previous(T00; T06; T12; T18)', '20180314T1200Z'), ('next(T00; T06; T12; T18)+P1W', '20180321T1800Z'), ('PT1H', '20180314T1612Z'), ('-P1M', '20180214T1512Z'), ('next(-00)', '21000101T0000Z'), ('previous(--01)', '20180101T0000Z'), ('next(---01)', '20180401T0000Z'), ('previous(--1225)', '20171225T0000Z'), ('next(-2006)', '20200601T0000Z'), ('previous(-W101)', '20180305T0000Z'), ('next(-W-1; -W-3; -W-5)', '20180314T0000Z'), ('next(-001; -091; -181; -271)', '20180401T0000Z'), ('previous(-365T12Z)', '20171231T1200Z'), ] ) def test_user_guide_examples(value: str, expected: str, set_cycling_type): """Test the offset CP examples in the Cylc user guide. https://cylc.github.io/cylc-doc/stable/html/user-guide/writing-workflows/scheduling.html """ set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2018-03-14T15:12Z" assert ingest_time(value, my_now) == expected def test_next_simple_no_now(set_cycling_type): """Test the generation of CP using 'next' with no value for `now`.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = None point = "next(T00Z)+P1D" output = ingest_time(point, my_now) current_time = datetime.utcnow() # my_now is None, but ingest_time will have used a similar time, and # the returned value must be after current_time output_time = datetime.strptime(output, "%Y%m%dT%H%MZ") assert current_time < output_time def test_integer_cycling_is_returned(set_cycling_type): """Test that when integer points are given, the same value is returned.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") integer_point = "1" assert integer_point, ingest_time(integer_point is None) def test_expanded_dates_are_returned(set_cycling_type): """Test that when expanded dates are given, the same value is returned.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") expanded_date = "+0100400101T0000Z" assert expanded_date, ingest_time(expanded_date is None) def test_timepoint_truncated(set_cycling_type): """Test that when a timepoint is given, and is truncated, then the value is added to `now`.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2018-03-14T15:12Z" timepoint_truncated = "T15:00Z" # 20180315T1500Z output = ingest_time(timepoint_truncated, my_now) assert output == "20180315T1500Z" def test_timepoint(set_cycling_type): """Test that when a timepoint is given, and is not truncated, the same value is returned.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2018-03-14T15:12Z" timepoint_truncated = "19951231T0630" # 19951231T0630 output = ingest_time(timepoint_truncated, my_now) assert output == "19951231T0630" @pytest.mark.parametrize( "_input, errortext", ( ("next (T-00, T-30)", "T-00;T-30"), ("next (wildebeest)", "Invalid ISO 8601 date"), ), ) def test_validate_fails_comma_sep_offset_list( _input, errortext, set_cycling_type ): """It raises an exception if validating a list separated by commas""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") with pytest.raises(Exception, match=errortext): ingest_time(_input) cylc-flow-8.6.4/tests/unit/cycling/test_util.py0000664000175000017500000000256015202510242021747 0ustar alastairalastair# 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 . """Test cycling utils.""" import pytest from cylc.flow.cycling.util import add_offset def test_add_offset(): """Test socket start.""" orig_point = '20200202T0000Z' plus_offset = '+PT02H02M' assert str(add_offset(orig_point, plus_offset)) == '20200202T0202Z' minus_offset = '-P1MT22H59M' assert str(add_offset(orig_point, minus_offset)) == '20200101T0101Z' assert str( add_offset(orig_point, minus_offset, dmp_fmt="CCYY-MM-DDThh:mmZ") ) == '2020-01-01T01:01Z' bad_offset = '+foo' with pytest.raises(ValueError, match=r'ERROR, bad offset format'): add_offset(orig_point, bad_offset) cylc-flow-8.6.4/tests/unit/test_context_node.py0000664000175000017500000001066215202510242022035 0ustar alastairalastair# 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 . from textwrap import dedent import pytest from cylc.flow.context_node import ContextNode with ContextNode('a') as a: with ContextNode('b') as b: c = ContextNode('c') d = ContextNode('d') def test_context_node_tree(): """Ensure parents and children are correctly organised.""" assert a.name == 'a' assert a._parent is None assert a._children == {'b': b, 'd': d} assert b.name == 'b' assert b._parent == a assert b._children == {'c': c} assert c.name == 'c' assert c._parent == b assert c._children is None def test_data_state(): """Ensure the in-class state remains intact.""" # DATA should start clean assert ContextNode.DATA == {} # nodes add themselves to DATA then remove themselves from it with ContextNode('foo'): pass # so DATA should end clean assert ContextNode.DATA == {} # however erroneously creating leaf-nodes outside of a context # will make a mess a = ContextNode('a') assert ContextNode.DATA == {a: set()} # just to make sure this doesn't result in hard-to-debug errors # we make sure this doesn't prevent us creating new trees with ContextNode('foo') as foo: bar = ContextNode('bar') assert ContextNode.DATA == {a: set(), foo: {a}} assert list(foo.walk()) == [(0, foo), (1, bar)] # finally clean up for niceness del ContextNode.DATA[a] del ContextNode.DATA[foo] def test_context_iter(): """Test iterating over child nodes.""" assert list(a) == [b, d] assert list(b) == [c] assert list(c) == [] def test_context_contains(): """Test `node.__contains__`.""" assert 'b' in a assert 'c' in b assert 'c' not in a def test_context_getitem(): """Test `node.__getitem__`.""" assert a['b'] == b assert b['c'] == c with pytest.raises(KeyError): assert b['d'] with pytest.raises(TypeError): assert c['b'] def test_context_str(): """Test the string representation of a node.""" assert str(a) == 'a' assert str(b) == 'b' assert str(c) == 'c' def test_context_repr(): """Test the Python representation of a node.""" assert repr(a) == 'a' assert repr(b) == 'a/b' assert repr(c) == 'a/b/c' def test_context_walk(): """Test walking the tree.""" assert list(a.walk()) == [ (0, a), (1, b), (2, c), (1, d) ] assert list(b.walk()) == [ (0, b), (1, c), ] assert list(c.walk()) == [ (0, c) ] def test_context_walk_depth(): assert list(a.walk(depth=1)) == [ (0, a), (1, b), (1, d) ] def test_context_tree(): """Test the string representation of the tree.""" assert a.tree() == dedent(''' a b c d ''').strip() assert b.tree() == dedent(''' b c ''').strip() assert c.tree() == dedent(''' c ''').strip() def test_context_is_root(): """Test root node detection.""" assert a.is_root() assert not b.is_root() assert not c.is_root() def test_context_is_leaf(): """Test leaf node detection.""" assert not a.is_leaf() assert not b.is_leaf() assert c.is_leaf() def test_context_parents(): """Test linearised ancestry.""" assert list(a.parents()) == [] assert list(b.parents()) == [a] assert list(c.parents()) == [b, a] def test_re_enter(): """It should be able to re __enter__ multiple times.""" # create a node with one child with ContextNode('x') as x: ContextNode('y') assert 'y' in x # go back and add another child retrospectively with x: ContextNode('z') assert 'y' in x assert 'z' in x cylc-flow-8.6.4/tests/unit/test_id_match.py0000664000175000017500000001470515202510242021116 0ustar alastairalastair# 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 . from textwrap import dedent from types import SimpleNamespace from typing import TYPE_CHECKING, Set, Tuple, cast import pytest from cylc.flow.id import Tokens from cylc.flow.id_match import id_match from cylc.flow.config import WorkflowConfig if TYPE_CHECKING: from cylc.flow.id_match import TaskTokens def to_tokens(*ids): return cast('Set[TaskTokens]', {Tokens(id_, relative=True) for id_ in ids}) def to_string_ids(*ids): return {id_.relative_id for id_ in ids} def to_string_ids_with_selectors(*ids): return {id_.relative_id_with_selectors for id_ in ids} @pytest.fixture def test_config(tmp_path): path = tmp_path / 'flow.cylc' with open(path, 'w+') as flow_cylc: flow_cylc.write(dedent(''' [scheduler] allow implicit tasks = True [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] P1 = a => b => c => d P3 = z ''')) return WorkflowConfig('test', str(path), SimpleNamespace()) def _id_match( config: 'WorkflowConfig', pool: 'Set[TaskTokens]', ids: 'Set[TaskTokens]', only_match_pool: bool = False, ) -> 'Tuple[Set[str], Set[str]]': """Convenience function for testing, converts strings to tokens.""" matched, unmatched = id_match( config, pool, to_tokens(*ids), only_match_pool=only_match_pool, ) return ( to_string_ids(*matched), to_string_ids_with_selectors(*unmatched), ) @pytest.mark.parametrize( 'ids,matched,unmatched', [ ( {'1'}, {'1/a', '1/b', '1/c'}, set(), ), ( {'2'}, set(), {'2'} ), ( {'*'}, {'1/a', '1/b', '1/c'}, set(), ), ( {'1/*'}, {'1/a', '1/b', '1/c'}, set(), ), ( {'2/*'}, set(), {'2/*'} ), ( {'*/*'}, {'1/a', '1/b', '1/c'}, set(), ), ( {'*/a'}, {'1/a'}, set(), ), ( {'*/z'}, set(), {'*/z'} ), ( {'*/*:x'}, {'1/a', '1/b', '1/c'}, set(), ), ( {'*/*:y'}, set(), {'*/*:y'}, ), ] ) def test_match_task(test_config, ids, matched, unmatched): """It should match task IDs.""" pool = to_tokens('1/a:x', '1/b:x', '1/c:x') _matched, _unmatched = _id_match( test_config, pool, ids, only_match_pool=True, ) assert _matched == matched assert _unmatched == unmatched @pytest.mark.parametrize( 'ids,matched,unmatched', [ ( {'1/a'}, {'1/a'}, set(), ), ( {'1/*'}, {'1/a', '1/b'}, set(), ), ( {'1/*:x'}, {'1/a', '1/b'}, set(), ), ( {'1/*:y'}, set(), {'1/*:y'}, ), ( {'*/*:x'}, {'1/a', '1/b', '2/a'}, set(), ), ( {'1/z'}, set(), {'1/z'}, ), ( {'1'}, {'1/a', '1/b'}, set(), ), ( {'3'}, set(), {'3'}, ), ] ) def test_match_cycle(test_config, ids, matched, unmatched): """It should match cycle IDs.""" pool = to_tokens('1/a:x', '1/b:x', '2/a:x') assert _id_match( test_config, pool, ids, only_match_pool=True, ) == (matched, unmatched) @pytest.mark.parametrize( 'ids,matched,unmatched', [ ( {'1/root'}, {'1/a', '1/b', '1/c', '1/d', '1/z'}, set(), ), ( {'2/root'}, {'2/a', '2/b', '2/c', '2/d'}, # 1/z isn't in cycle 2 set(), ), ( {'*/root'}, # should match all active cycles (i.e. cycles 1 and 2) {'1/a', '1/b', '1/c', '1/d', '1/z', '2/a', '2/b', '2/c', '2/d'}, set(), ), ( {'1/[ad]'}, {'1/a', '1/d'}, set(), ), ( {'1/[!ad]'}, {'1/b', '1/c', '1/z'}, set(), ), ] ) def test_match_inactive(test_config, ids, matched, unmatched): """It should match non-pool tasks""" assert _id_match( test_config, to_tokens('1/a:running', '2/a:waiting'), # active cycles are 1 and 2 ids, ) == (matched, unmatched) @pytest.mark.parametrize( 'ids,matched,unmatched', [ ({'1/A'}, {'1/a'}, set()), ({'1/B'}, {'1/b1', '1/b2'}, set()), ({'1/C'}, set(), {'1/C'}), ({'1/root'}, {'1/a', '1/b1', '1/b2'}, set()), ] ) def test_match_family(tmp_path, ids, matched, unmatched): """It should match family IDs.""" pool = to_tokens('1/a:x', '1/b1:x', '1/b2:x') path = tmp_path / 'flow.cylc' with open(path, 'w+') as flow_cylc: flow_cylc.write(dedent(''' [scheduling] cycling mode = integer initial cycle point = 1 [[graph]] P1 = a & b1 & b2 [runtime] [[a]] inherit = A [[b1, b2]] inherit = B [[A, B]] ''')) config = WorkflowConfig('test', str(path), SimpleNamespace()) assert _id_match(config, pool, ids) == (matched, unmatched) cylc-flow-8.6.4/tests/unit/tui/0000775000175000017500000000000015202510242016527 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/tui/__init__.py0000664000175000017500000000000015202510242020626 0ustar alastairalastaircylc-flow-8.6.4/tests/unit/tui/test_overlay.py0000664000175000017500000000533515202510242021627 0ustar alastairalastair# 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 . import ast from unittest.mock import Mock import pytest import urwid from cylc.flow.tui.app import BINDINGS import cylc.flow.tui.overlay from cylc.flow.workflow_status import WorkflowStatus @pytest.fixture def overlay_functions(): """List overlay all generator functions in cylc.flow.tui.overlay Uses ast to parse functions out of the module. """ filepath = cylc.flow.tui.overlay.__file__ with open(filepath, 'r') as source_file: tree = ast.parse(source_file.read(), filename=filepath) return [ getattr(cylc.flow.tui.overlay, obj.name) for obj in tree.body if isinstance(obj, ast.FunctionDef) and not obj.name.startswith('_') ] def test_interface(overlay_functions): """Ensure all overlay functions have the correct signature.""" for function in overlay_functions: # mock up an app object to keep things working app = Mock( filters={'tasks': {}, 'workflows': {'id': '.*'}}, bindings=BINDINGS, tree_walker=Mock( get_focus=Mock( return_value=[ Mock( get_node=Mock( return_value=Mock( get_value=lambda: { 'id_': '~u/a', 'type_': 'workflow', 'data': { 'status': WorkflowStatus.RUNNING.value, }, } ) ) ) ] ) ) ) widget, options = function(app) assert isinstance(widget, urwid.Widget) assert isinstance(options, dict) assert 'width' in options assert 'height' in options cylc-flow-8.6.4/tests/unit/tui/test_data.py0000664000175000017500000000343615202510242021057 0ustar alastairalastair# 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 . import pytest from cylc.flow import __version__ import cylc.flow.tui.data from cylc.flow.tui.data import ( _QUERY, VersionIncompat, generate_mutation, get_query, ) def test_generate_mutation(monkeypatch): """It should produce a GraphQL mutation with the args filled in.""" arg_types = { 'foo': 'String!', 'bar': '[Int]' } monkeypatch.setattr(cylc.flow.tui.data, 'ARGUMENT_TYPES', arg_types) assert generate_mutation( 'my_mutation', {'foo': 'foo', 'bar': 'bar', 'user': 'user'} ) == ''' mutation($foo: String!, $bar: [Int]) { my_mutation (foos: $foo, bars: $bar) { result } } ''' def test_query_compat(): """It should return a query or raise an exception.""" # old version - unsupported with pytest.raises(VersionIncompat, match='6.11.4'): get_query('6.11.4') # current version - supported assert get_query(__version__) == _QUERY # future version - supported assert get_query('9.0.0') == _QUERY cylc-flow-8.6.4/tests/unit/tui/test_util.py0000664000175000017500000002366715202510242021133 0ustar alastairalastair# 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 . from datetime import ( datetime, timedelta ) from unittest.mock import Mock import pytest from cylc.flow.tui.util import ( JOB_ICON, TASK_ICONS, render_node, compute_tree, get_task_icon ) from cylc.flow.wallclock import ( get_time_string, get_current_time_string ) def test_render_node__job_info(): """It renders job information nodes.""" assert render_node( None, {'a': 1, 'b': 2}, 'job_info' ) == [ 'a 1\n', 'b 2' ] def test_render_node__job(): """It renders job nodes.""" assert render_node( None, {'state': 'succeeded', 'submitNum': 1}, 'job' ) == [ '#01 ', [('job_succeeded', JOB_ICON)] ] def test_render_node__task__succeeded(): """It renders tasks.""" node = Mock() node.get_child_node = lambda _: None assert render_node( node, { 'name': 'foo', 'state': 'succeeded', 'isHeld': False, 'isQueued': False, 'isRunahead': False, 'flowNums': '[1]', }, 'task' ) == [ ('body', TASK_ICONS['succeeded']), ' ', ('body', 'foo'), ] def test_render_node__task__running(): """It renders running tasks.""" child = Mock() child.get_value = lambda: {'data': { 'startedTime': get_current_time_string(), 'state': 'running' }} node = Mock() node.get_child_node = lambda _: child assert render_node( node, { 'name': 'foo', 'state': 'running', 'isHeld': False, 'isQueued': False, 'isRunahead': False, 'flowNums': '[1]', 'task': {'meanElapsedTime': 100} }, 'task' ) == [ ('body', TASK_ICONS['running']), ' ', ('job_running', JOB_ICON), ' ', ('body', 'foo'), ] def test_render_node__family(): """It renders families.""" assert render_node( None, { 'state': 'succeeded', 'isHeld': False, 'isQueued': False, 'isRunahead': False, 'id': 'myid' }, 'family' ) == [ [('body', TASK_ICONS['succeeded'])], ' ', 'myid' ] def test_render_node__cycle_point(): """It renders cycle points.""" assert render_node( None, {'id': 'myid'}, 'cycle_point' ) == 'myid' @pytest.mark.parametrize( 'status,is_held,is_queued,is_runahead,start_offset,mean_time,expected', [ # task states ('waiting', False, False, False, None, None, ['○']), ('submitted', False, False, False, None, None, ['⊙']), ('running', False, False, False, None, None, ['⊙']), ('succeeded', False, False, False, None, None, ['●']), ('submit-failed', False, False, False, None, None, ['⊘']), ('failed', False, False, False, None, None, ['⊗']), # progress indicator ('running', False, False, False, 0, 100, ['⊙']), ('running', False, False, False, 25, 100, ['◔']), ('running', False, False, False, 50, 100, ['◑']), ('running', False, False, False, 75, 100, ['◕']), ('running', False, False, False, 100, 100, ['◕']), # is-held modifier ('waiting', True, False, False, None, None, ['\u030E', '○']), # is-queued modifier ('waiting', False, True, False, None, None, ['\u033F', '○']), # is-runahead modifier ('waiting', False, False, True, None, None, ['\u0340', '○']), ] ) def test_get_task_icon( status, is_held, is_queued, is_runahead, start_offset, mean_time, expected): """It renders task icons.""" start_time = None if start_offset is not None: start_time = get_time_string( datetime.utcnow() - timedelta(seconds=start_offset) ) assert ( ( get_task_icon( status, is_held=is_held, is_queued=is_queued, is_runahead=is_runahead, colour='custom', start_time=start_time, mean_time=mean_time, ) ) ) == [('custom', char) for char in expected] def test_compute_tree(): """It computes a tree in the right structure for urwid. Note this test doesn't use full data or propper ids because it's purpose is not to test the GraphQL interface but to ensure the assumptions made by compute_tree check out. """ tree = compute_tree({ 'workflows': [{ 'id': 'workflow id', 'port': 1234, 'cyclePoints': [ { 'id': '1/family-suffix', 'cyclePoint': '1' } ], 'familyProxies': [ { # top level family 'name': 'FOO', 'id': '1/FOO', 'cyclePoint': '1', 'firstParent': {'name': 'root', 'id': '1/root'} }, { # nested family 'name': 'FOOT', 'id': '1/FOOT', 'cyclePoint': '1', 'firstParent': {'name': 'FOO', 'id': '1/FOO'} }, ], 'taskProxies': [ { # top level task 'name': 'pub', 'id': '1/pub', 'firstParent': {'name': 'root', 'id': '1/root'}, 'cyclePoint': '1', 'jobs': [] }, { # child task (belongs to family) 'name': 'fan', 'id': '1/fan', 'firstParent': {'name': 'fan', 'id': '1/fan'}, 'cyclePoint': '1', 'jobs': [] }, { # nested child task (belongs to incestuous family) 'name': 'fool', 'id': '1/fool', 'firstParent': {'name': 'FOOT', 'id': '1/FOOT'}, 'cyclePoint': '1', 'jobs': [] }, { # a task which has jobs 'name': 'worker', 'id': '1/worker', 'firstParent': {'name': 'root', 'id': '1/root'}, 'cyclePoint': '1', 'jobs': [ {'id': '1/worker/03', 'submitNum': '3'}, {'id': '1/worker/02', 'submitNum': '2'}, {'id': '1/worker/01', 'submitNum': '1'} ] } ] }] }) # the root node assert tree['type_'] == 'root' assert tree['id_'] == 'root' assert len(tree['children']) == 1 # the workflow node workflow = tree['children'][0] assert workflow['type_'] == 'workflow' assert workflow['id_'] == 'workflow id' assert set(workflow['data']) == { # whatever if present on the node should end up in data 'cyclePoints', 'familyProxies', 'id', 'port', 'taskProxies' } assert len(workflow['children']) == 1 # the cycle point node cycle = workflow['children'][0] assert cycle['type_'] == 'cycle' assert cycle['id_'] == '//1' assert list(cycle['data']) == [ 'id', 'cyclePoint' ] assert len(cycle['children']) == 3 assert [ node['id_'] for node in cycle['children'] ] == [ # test alphabetical sorting '1/FOO', '1/pub', '1/worker' ] # test family node family = cycle['children'][0] assert family['type_'] == 'family' assert family['id_'] == '1/FOO' assert list(family['data']) == [ 'name', 'id', 'cyclePoint', 'firstParent' ] assert len(family['children']) == 1 # test nested family nested_family = family['children'][0] assert nested_family['type_'] == 'family' assert nested_family['id_'] == '1/FOOT' assert list(nested_family['data']) == [ 'name', 'id', 'cyclePoint', 'firstParent' ] assert len(nested_family['children']) == 1 # test task task = nested_family['children'][0] assert task['type_'] == 'task' assert task['id_'] == '1/fool' assert list(task['data']) == [ 'name', 'id', 'firstParent', 'cyclePoint', 'jobs' ] assert len(task['children']) == 0 # test task with jobs task = cycle['children'][-1] assert [ # test sorting job['id_'] for job in task['children'] ] == [ '1/worker/03', '1/worker/02', '1/worker/01' ] # test job job = task['children'][0] assert job['type_'] == 'job' assert job['id_'] == '1/worker/03' assert list(job['data']) == [ 'id', 'submitNum' ] assert len(job['children']) == 1 # test job info job_info = job['children'][0] assert job_info['type_'] == 'job_info' assert job_info['id_'] == '1/worker/03_info' assert list(job_info['data']) == [ 'id', 'submitNum' ] assert len(job_info['children']) == 0 cylc-flow-8.6.4/tests/unit/plugins/0000775000175000017500000000000015202510242017407 5ustar alastairalastaircylc-flow-8.6.4/tests/unit/plugins/test_pre_configure.py0000664000175000017500000000756115202510242023660 0ustar alastairalastair# 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 . """"Test the pre_configure entry point.""" from random import random import pytest from cylc.flow.exceptions import PluginError from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.parsec.fileparse import process_plugins class EntryPointWrapper: """Wraps a method to make it look like an entry point.""" def __init__(self, fcn): self.name = fcn.__name__ self.fcn = fcn def load(self): return self.fcn @EntryPointWrapper def pre_configure_basic(*_, **__): """Simple plugin that returns one env var and one template var.""" return { 'env': { 'ANSWER': '42' }, 'template_variables': { 'QUESTION': 'What do you get if you multiply 7 by 6?' } } @EntryPointWrapper def pre_configure_templating_detected(*_, **__): """Plugin that detects a random templating engine.""" return { 'templating_detected': str(random()) } @EntryPointWrapper def pre_configure_error(*_, **__): """Plugin that raises an exception.""" raise Exception('foo') def test_pre_configure(monkeypatch): """It should call the plugin.""" monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [pre_configure_basic] if namespace == 'cylc.pre_configure' else [] ) ) extra_vars = process_plugins('/', None) assert extra_vars == { 'env': { 'ANSWER': '42' }, 'template_variables': { 'QUESTION': 'What do you get if you multiply 7 by 6?' }, 'templating_detected': None } def test_pre_configure_duplicate(monkeypatch): """It should error when plugins clash.""" monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [ pre_configure_basic, pre_configure_basic, ] if namespace == 'cylc.pre_configure' else [] ) ) with pytest.raises(ParsecError): process_plugins('/', None) def test_pre_configure_templating_detected(monkeypatch): """It should error when plugins clash (for templating).""" monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [ pre_configure_templating_detected, pre_configure_templating_detected, ] if namespace == 'cylc.pre_configure' else [] ) ) with pytest.raises(ParsecError): process_plugins('/', None) def test_pre_configure_exception(monkeypatch): """It should wrap plugin errors.""" monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [ pre_configure_error, ] if namespace == 'cylc.pre_configure' else [] ) ) with pytest.raises(PluginError) as exc_ctx: process_plugins('/', None) # the context of the original error should be preserved in the raised # exception assert exc_ctx.value.entry_point == 'cylc.pre_configure' assert exc_ctx.value.plugin_name == 'pre_configure_error' assert str(exc_ctx.value.exc) == 'foo' cylc-flow-8.6.4/tests/unit/test_resources.py0000664000175000017500000000467315202510242021363 0ustar alastairalastair# 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 . import pytest from pathlib import Path from shlex import split from subprocess import run from cylc.flow.resources import ( RESOURCE_NAMES, get_resources, _backup, ) def test_get_resources_one(tmpdir): """Test extraction of a specific resource. Check that a file of the right name gets extracted. Do not check file content becuase there is no assurance that it will remain constant. """ get_resources('job.sh', tmpdir) assert (tmpdir / 'job.sh').isfile() @pytest.mark.parametrize( 'resource', [ r for r in list(RESOURCE_NAMES.keys()) if r[0] != '!' ] + ['tutorial/runtime-tutorial'] ) def test_get_resources_all(resource, tmpdir): get_resources(resource, tmpdir) assert (tmpdir / Path(resource).name).exists() def test_cli(tmpdir): result = run( split(f'cylc get-resources job.sh {str(tmpdir)}'), capture_output=True ) if result.returncode != 0: raise AssertionError( f'{result.stderr}' ) def test_backup(tmp_path, caplog): a = tmp_path / 'a' abc = tmp_path / 'a' / 'b' / 'c' abc.mkdir(parents=True) before = set(tmp_path.glob('*')) _backup(a) assert len(caplog.record_tuples) == 1 after = set(tmp_path.glob('*')) assert len(after - before) == 1 new = list(after - before)[0] assert new.name.startswith(a.name) new_abc = new / 'b' / 'c' assert new_abc.exists() def test_vim_deprecated(): """It fails, returning a warning if user asks for obsolete syntax file """ output = run( ['cylc', 'get-resources', 'syntax/cylc.vim'], capture_output=True ) assert 'has been replaced' in output.stderr.decode() cylc-flow-8.6.4/tests/unit/test_util.py0000664000175000017500000000163415202510242020320 0ustar alastairalastair# 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 . from cylc.flow.util import deserialise_set def test_deserialise_set(): actual = deserialise_set('["2", "3"]') expected = {'2', '3'} assert actual == expected cylc-flow-8.6.4/tests/unit/test_clean.py0000664000175000017500000010616415202510242020431 0ustar alastairalastair# 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 . from glob import iglob import logging import os from pathlib import Path import shutil from subprocess import Popen from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, ) from unittest import mock import pytest from cylc.flow import ( CYLC_LOG, clean as cylc_clean, ) from cylc.flow.clean import ( _clean_using_glob, _remote_clean_cmd, clean, glob_in_run_dir, init_clean, ) from cylc.flow.exceptions import ( CylcError, InputError, SchedulerAlive, ServiceFileError, WorkflowFilesError, ) from cylc.flow.pathutil import parse_rm_dirs from cylc.flow.scripts.clean import CleanOptions from cylc.flow.workflow_files import ( WorkflowFiles, get_symlink_dirs, ) from .filetree import ( FILETREE_1, FILETREE_2, FILETREE_3, FILETREE_4, Symlink, create_filetree, get_filetree_as_list, ) if TYPE_CHECKING: from .conftest import MonkeyMock NonCallableFixture = Any # global.cylc[install]scan depth for these tests: MAX_SCAN_DEPTH = 3 @pytest.fixture def glbl_cfg_max_scan_depth(mock_glbl_cfg: Callable) -> None: mock_glbl_cfg( 'cylc.flow.workflow_files.glbl_cfg', f''' [install] max depth = {MAX_SCAN_DEPTH} ''' ) @pytest.mark.parametrize( 'id_, stopped, err, err_msg', [ ('foo/..', True, WorkflowFilesError, "cannot be a path that points to the cylc-run directory or above"), ('foo/../..', True, WorkflowFilesError, "cannot be a path that points to the cylc-run directory or above"), ('foo', False, ServiceFileError, "Cannot clean running workflow"), ] ) def test_clean_check__fail( id_: str, stopped: bool, err: Type[Exception], err_msg: str, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """Test that _clean_check() fails appropriately. Params: id_: Workflow name. stopped: Whether the workflow is stopped when _clean_check() is called. err: Expected error class. err_msg: Message that is expected to be in the exception. """ def mocked_detect_old_contact_file(*a, **k): if not stopped: raise SchedulerAlive('Mocked error') monkeypatch.setattr( 'cylc.flow.clean.detect_old_contact_file', mocked_detect_old_contact_file ) with pytest.raises(err) as exc: cylc_clean._clean_check(CleanOptions(), id_, tmp_path) assert err_msg in str(exc.value) @pytest.mark.parametrize( 'db_platforms, opts, clean_called, remote_clean_called', [ pytest.param( ['localhost', 'localhost'], {}, True, False, id="Only platform in DB is localhost" ), pytest.param( ['horse'], {}, True, True, id="Remote platform in DB" ), pytest.param( ['horse'], {'local_only': True}, True, False, id="Local clean only" ), pytest.param( ['horse'], {'remote_only': True}, False, True, id="Remote clean only" ) ] ) def test_init_clean( db_platforms: List[str], opts: Dict[str, Any], clean_called: bool, remote_clean_called: bool, monkeypatch: pytest.MonkeyPatch, monkeymock: 'MonkeyMock', tmp_run_dir: Callable ) -> None: """Test the init_clean() function logic. Params: db_platforms: Platform names that would be loaded from the database. opts: Any options passed to the cylc clean CLI. clean_called: If a local clean is expected to go ahead. remote_clean_called: If a remote clean is expected to go ahead. """ id_ = 'foo/bar/' rdir = tmp_run_dir(id_, installed=True) Path(rdir, WorkflowFiles.Service.DIRNAME, WorkflowFiles.Service.DB).touch() mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') monkeypatch.setattr('cylc.flow.clean.get_platforms_from_db', lambda x: set(db_platforms)) init_clean(id_, opts=CleanOptions(**opts)) assert mock_clean.called is clean_called assert mock_remote_clean.called is remote_clean_called def test_init_clean__no_dir( monkeymock: 'MonkeyMock', tmp_run_dir: Callable, caplog: pytest.LogCaptureFixture ) -> None: """Test init_clean() when the run dir doesn't exist""" caplog.set_level(logging.INFO, CYLC_LOG) mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') init_clean('foo/bar', opts=CleanOptions()) assert "No directory to clean" in caplog.text assert mock_clean.called is False assert mock_remote_clean.called is False def test_init_clean__no_db( monkeymock: 'MonkeyMock', tmp_run_dir: Callable, caplog: pytest.LogCaptureFixture ) -> None: """Test init_clean() when the workflow database doesn't exist""" caplog.set_level(logging.INFO, CYLC_LOG) tmp_run_dir('bespin') mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') init_clean('bespin', opts=CleanOptions()) assert ( "No workflow database for bespin - will only clean locally" ) in caplog.text assert mock_clean.called is True assert mock_remote_clean.called is False def test_init_clean__remote_only_no_db( monkeymock: 'MonkeyMock', tmp_run_dir: Callable ) -> None: """Test remote-only init_clean() when the workflow DB doesn't exist""" tmp_run_dir('hoth') mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') with pytest.raises(ServiceFileError) as exc: init_clean('hoth', opts=CleanOptions(remote_only=True)) assert ( "No workflow database for hoth - cannot perform remote clean" ) in str(exc.value) assert mock_clean.called is False assert mock_remote_clean.called is False def test_init_clean__running_workflow( monkeypatch: pytest.MonkeyPatch, tmp_run_dir: Callable ) -> None: """Test init_clean() fails when workflow is still running""" def mock_err(*args, **kwargs): raise SchedulerAlive("Mocked error") monkeypatch.setattr('cylc.flow.clean.detect_old_contact_file', mock_err) tmp_run_dir('yavin') with pytest.raises(ServiceFileError) as exc: init_clean('yavin', opts=CleanOptions()) assert "Cannot clean running workflow" in str(exc.value) @pytest.mark.parametrize( 'rm_dirs, expected_clean, expected_remote_clean', [(None, None, []), (["r2d2:c3po"], {"r2d2", "c3po"}, ["r2d2:c3po"])] ) def test_init_clean__rm_dirs( rm_dirs: Optional[List[str]], expected_clean: Set[str], expected_remote_clean: List[str], monkeymock: 'MonkeyMock', monkeypatch: pytest.MonkeyPatch, tmp_run_dir: Callable ) -> None: """Test init_clean() with the --rm option. Params: rm_dirs: Dirs given by --rm option. expected_clean: The dirs that are expected to be passed to clean(). expected_remote_clean: The dirs that are expected to be passed to remote_clean(). """ id_ = 'dagobah' run_dir: Path = tmp_run_dir(id_) Path( run_dir, WorkflowFiles.Service.DIRNAME, WorkflowFiles.Service.DB ).touch() mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') platforms = {'platform_one'} monkeypatch.setattr('cylc.flow.clean.get_platforms_from_db', lambda x: platforms) opts = CleanOptions(rm_dirs=rm_dirs) if rm_dirs else CleanOptions() init_clean(id_, opts=opts) mock_clean.assert_called_with(id_, run_dir, expected_clean) mock_remote_clean.assert_called_with( id_, platforms, opts.remote_timeout, expected_remote_clean ) @pytest.mark.parametrize( 'id_, symlink_dirs, rm_dirs, expected_deleted, expected_remaining', [ pytest.param( 'foo/bar', {}, None, ['cylc-run/foo'], ['cylc-run'], id="Basic clean" ), pytest.param( 'foo/bar/baz', { 'log': 'sym-log', 'share': 'sym-share', 'share/cycle': 'sym-cycle', 'work': 'sym-work' }, None, ['cylc-run/foo', 'sym-log/cylc-run/foo', 'sym-share/cylc-run/foo', 'sym-cycle/cylc-run/foo', 'sym-work/cylc-run/foo'], ['cylc-run', 'sym-log/cylc-run', 'sym-share/cylc-run', 'sym-cycle/cylc-run', 'sym-work/cylc-run'], id="Symlink dirs" ), pytest.param( 'foo', { 'run': 'sym-run', 'log': 'sym-log', 'share': 'sym-share', 'share/cycle': 'sym-cycle', 'work': 'sym-work' }, None, ['cylc-run/foo', 'sym-run/cylc-run/foo', 'sym-log/cylc-run/foo', 'sym-share/cylc-run/foo', 'sym-cycle/cylc-run/foo', 'sym-work/cylc-run/foo'], ['cylc-run', 'sym-run/cylc-run', 'sym-log/cylc-run', 'sym-share/cylc-run', 'sym-cycle/cylc-run', 'sym-work'], id="Symlink dirs including run dir" ), pytest.param( 'foo', {}, {'log', 'share'}, ['cylc-run/foo/log', 'cylc-run/foo/share'], ['cylc-run/foo/work'], id="Targeted clean" ), pytest.param( 'foo', {'log': 'sym-log'}, {'log'}, ['cylc-run/foo/log', 'sym-log/cylc-run/foo'], ['cylc-run/foo/work', 'cylc-run/foo/share/cycle', 'sym-log/cylc-run'], id="Targeted clean with symlink dirs" ), pytest.param( 'foo', {}, {'share/cy*'}, ['cylc-run/foo/share/cycle'], ['cylc-run/foo/log', 'cylc-run/foo/work', 'cylc-run/foo/share'], id="Targeted clean with glob" ), pytest.param( 'foo', {'log': 'sym-log'}, {'w*', 'wo*', 'l*', 'lo*'}, ['cylc-run/foo/work', 'cylc-run/foo/log', 'sym-log/cylc-run/foo'], ['cylc-run/foo/share', 'cylc-run/foo/share/cycle'], id="Targeted clean with degenerate glob" ), ] ) def test_clean( id_: str, symlink_dirs: Dict[str, str], rm_dirs: Optional[Set[str]], expected_deleted: List[str], expected_remaining: List[str], tmp_path: Path, tmp_run_dir: Callable ) -> None: """Test the clean() function. Params: id_: Workflow name. symlink_dirs: As you would find in the global config under [symlink dirs][platform]. rm_dirs: As passed to clean(). expected_deleted: Dirs (relative paths under tmp_path) that are expected to be cleaned. expected_remaining: Any dirs (relative paths under tmp_path) that are not expected to be cleaned. """ # --- Setup --- run_dir: Path = tmp_run_dir(id_) if 'run' in symlink_dirs: target = tmp_path / symlink_dirs['run'] / 'cylc-run' / id_ target.mkdir(parents=True) shutil.rmtree(run_dir) run_dir.symlink_to(target) symlink_dirs.pop('run') for symlink_name, target_name in symlink_dirs.items(): target = tmp_path / target_name / 'cylc-run' / id_ / symlink_name target.mkdir(parents=True) symlink = run_dir / symlink_name symlink.symlink_to(target) for d_name in ('log', 'share', 'share/cycle', 'work'): if d_name not in symlink_dirs: (run_dir / d_name).mkdir() for rel_path in [*expected_deleted, *expected_remaining]: assert (tmp_path / rel_path).exists() # --- The actual test --- cylc_clean.clean(id_, run_dir, rm_dirs) for rel_path in expected_deleted: assert (tmp_path / rel_path).exists() is False assert (tmp_path / rel_path).is_symlink() is False for rel_path in expected_remaining: assert (tmp_path / rel_path).exists() def test_clean__broken_symlink_run_dir( tmp_path: Path, tmp_run_dir: Callable ) -> None: """Test clean() successfully remove a run dir that is a broken symlink.""" # Setup id_ = 'foo/bar' run_dir: Path = tmp_run_dir(id_) target = tmp_path.joinpath('rabbow/cylc-run', id_) target.mkdir(parents=True) shutil.rmtree(run_dir) run_dir.symlink_to(target) target.rmdir() assert run_dir.parent.exists() is True # cylc-run/foo should exist # Test cylc_clean.clean(id_, run_dir) assert run_dir.parent.exists() is False # cylc-run/foo should be gone assert target.parent.exists() is False # rabbow/cylc-run/foo too def test_clean__bad_symlink_dir_wrong_type( tmp_path: Path, tmp_run_dir: Callable ) -> None: """Test clean() raises error when a symlink dir actually points to a file instead of a dir""" id_ = 'foo' run_dir: Path = tmp_run_dir(id_) symlink = run_dir.joinpath('log') target = tmp_path.joinpath('sym-log', 'cylc-run', id_, 'meow.txt') target.parent.mkdir(parents=True) target.touch() symlink.symlink_to(target) with pytest.raises(WorkflowFilesError) as exc: cylc_clean.clean(id_, run_dir) assert "Invalid symlink at" in str(exc.value) assert symlink.exists() is True def test_clean__bad_symlink_dir_wrong_form( tmp_path: Path, tmp_run_dir: Callable ) -> None: """Test clean() raises error when a symlink dir points to an unexpected dir""" run_dir: Path = tmp_run_dir('foo') symlink = run_dir.joinpath('log') target = tmp_path.joinpath('sym-log', 'oops', 'log') target.mkdir(parents=True) symlink.symlink_to(target) with pytest.raises(WorkflowFilesError) as exc: cylc_clean.clean('foo', run_dir) assert 'should end with "cylc-run/foo/log"' in str(exc.value) assert symlink.exists() is True @pytest.mark.parametrize('pattern', ['thing/', 'thing/*']) def test_clean__rm_dir_not_file(pattern: str, tmp_run_dir: Callable): """Test clean() does not remove a file when the rm_dir glob pattern would match a dir only.""" id_ = 'foo' run_dir: Path = tmp_run_dir(id_) a_file = run_dir.joinpath('thing') a_file.touch() rm_dirs = parse_rm_dirs([pattern]) cylc_clean.clean(id_, run_dir, rm_dirs) assert a_file.exists() @pytest.fixture def filetree_for_testing_cylc_clean(tmp_path: Path): """Fixture that creates a filetree from the given dict, and returns which files are expected to be deleted and which aren't. See tests/unit/filetree.py Args: id_: Workflow name. initial_filetree: The filetree before cleaning. filetree_left_behind: The filetree that is expected to be left behind after cleaning, excluding the 'you-shall-not-pass/' directory, which is always expected to be left behind. Returns: run_dir: Workflow run dir. files_to_delete: List of files that are expected to be deleted. files_not_to_delete: List of files that are not expected to be deleted. """ def _filetree_for_testing_cylc_clean( id_: str, initial_filetree: Dict[str, Any], filetree_left_behind: Dict[str, Any] ) -> Tuple[Path, List[str], List[str]]: create_filetree(initial_filetree, tmp_path, tmp_path) files_not_to_delete = [ os.path.normpath(i) for i in iglob(str(tmp_path / 'you-shall-not-pass/**'), recursive=True) ] files_not_to_delete.extend( get_filetree_as_list(filetree_left_behind, tmp_path) ) files_to_delete = list( set(get_filetree_as_list(initial_filetree, tmp_path)).difference( files_not_to_delete ) ) run_dir = tmp_path / 'cylc-run' / id_ return run_dir, files_to_delete, files_not_to_delete return _filetree_for_testing_cylc_clean @pytest.mark.parametrize( 'pattern, initial_filetree, filetree_left_behind', [ pytest.param( '**', FILETREE_1, { 'cylc-run': {'foo': {}}, 'sym': {'cylc-run': {'foo': {'bar': {}}}} } ), pytest.param( '*/**', FILETREE_1, { 'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, 'rincewind.txt': Symlink('whatever') }}}, 'sym': {'cylc-run': {'foo': {'bar': {}}}} } ), pytest.param( '**/*.txt', FILETREE_1, { 'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, 'log': Symlink('whatever'), 'mirkwood': Symlink('whatever') }}}, 'sym': {'cylc-run': {'foo': {'bar': { 'log': { 'darmok': Symlink('whatever'), 'bib': {} } }}}} } ) ] ) def test__clean_using_glob( pattern: str, initial_filetree: Dict[str, Any], filetree_left_behind: Dict[str, Any], filetree_for_testing_cylc_clean: Callable ) -> None: """Test _clean_using_glob(), particularly that it does not follow and delete symlinks (apart from the standard symlink dirs). Params: pattern: The glob pattern to test. initial_filetree: The filetree to test against. files_left_behind: The filetree expected to remain after _clean_using_glob() is called (excluding /you-shall-not-pass, which is always expected to remain). """ # --- Setup --- run_dir: Path files_to_delete: List[str] files_not_to_delete: List[str] run_dir, files_to_delete, files_not_to_delete = ( filetree_for_testing_cylc_clean( 'foo/bar', initial_filetree, filetree_left_behind) ) # --- Test --- _clean_using_glob(run_dir, pattern, symlink_dirs=['log']) for file in files_not_to_delete: assert os.path.exists(file) is True for file in files_to_delete: assert os.path.lexists(file) is False @pytest.mark.parametrize( 'rm_dirs, initial_filetree, filetree_left_behind', [ pytest.param( {'**'}, FILETREE_1, { 'cylc-run': {}, 'sym': {'cylc-run': {}} }, id="filetree1 **" ), pytest.param( {'*/**'}, FILETREE_1, { 'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, 'rincewind.txt': Symlink('whatever') }}}, 'sym': {'cylc-run': {}} }, id="filetree1 */**" ), pytest.param( {'**/*.txt'}, FILETREE_1, { 'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, 'log': Symlink('whatever'), 'mirkwood': Symlink('whatever') }}}, 'sym': {'cylc-run': {'foo': {'bar': { 'log': { 'darmok': Symlink('whatever'), 'bib': {} } }}}} }, id="filetree1 **/*.txt" ), pytest.param( {'**/cycle'}, FILETREE_2, { 'cylc-run': {'foo': { 'bar': Symlink('sym-run/cylc-run/foo/bar') }}, 'sym-run': {'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, 'share': Symlink('sym-share/cylc-run/foo/bar/share') }}}}, 'sym-share': {'cylc-run': {'foo': {'bar': { 'share': {} }}}}, 'sym-cycle': {'cylc-run': {}} }, id="filetree2 **/cycle" ), pytest.param( {'share'}, FILETREE_2, { 'cylc-run': {'foo': { 'bar': Symlink('sym-run/cylc-run/foo/bar') }}, 'sym-run': {'cylc-run': {'foo': {'bar': { '.service': {'db': None}, 'flow.cylc': None, }}}}, 'sym-share': {'cylc-run': {}}, 'sym-cycle': {'cylc-run': {}}, }, id="filetree2 share" ), pytest.param( {'**'}, FILETREE_2, { 'cylc-run': {}, 'sym-run': {'cylc-run': {}}, 'sym-share': {'cylc-run': {}}, 'sym-cycle': {'cylc-run': {}} }, id="filetree2 **" ), pytest.param( {'*'}, FILETREE_2, { 'cylc-run': {'foo': { 'bar': Symlink('sym-run/cylc-run/foo/bar') }}, 'sym-run': {'cylc-run': {'foo': {'bar': { '.service': {'db': None}, }}}}, 'sym-share': {'cylc-run': {}}, 'sym-cycle': {'cylc-run': {}}, }, id="filetree2 *" ), pytest.param( # Check https://bugs.python.org/issue35201 has no effect {'non-exist/**'}, FILETREE_2, FILETREE_2, id="filetree2 non-exist/**" ), pytest.param( {'**'}, FILETREE_3, { 'cylc-run': {}, 'sym-run': {'cylc-run': {}}, 'sym-cycle': {'cylc-run': {}}, }, id="filetree3 **" ), pytest.param( {'**'}, FILETREE_4, { 'cylc-run': {}, 'sym-cycle': {'cylc-run': {}}, }, id="filetree4 **" ) ], ) def test_clean__targeted( rm_dirs: Set[str], initial_filetree: Dict[str, Any], filetree_left_behind: Dict[str, Any], caplog: pytest.LogCaptureFixture, tmp_run_dir: Callable, filetree_for_testing_cylc_clean: Callable ) -> None: """Test clean(), particularly that it does not follow and delete symlinks (apart from the standard symlink dirs). This is similar to test__clean_using_glob(), but the filetree expected to remain after cleaning is different due to the tidy up of empty dirs. Params: rm_dirs: The glob patterns to test. initial_filetree: The filetree to test against. files_left_behind: The filetree expected to remain after clean() is called (excluding /you-shall-not-pass, which is always expected to remain). """ # --- Setup --- caplog.set_level(logging.DEBUG, CYLC_LOG) id_ = 'foo/bar' run_dir: Path files_to_delete: List[str] files_not_to_delete: List[str] run_dir, files_to_delete, files_not_to_delete = ( filetree_for_testing_cylc_clean( id_, initial_filetree, filetree_left_behind) ) # --- Test --- cylc_clean.clean(id_, run_dir, rm_dirs) for file in files_not_to_delete: assert os.path.exists(file) is True for file in files_to_delete: assert os.path.lexists(file) is False @pytest.mark.parametrize( 'rm_dirs', [ [".."], ["foo:.."], ["foo/../../meow"] ] ) def test_init_clean__targeted_bad( rm_dirs: List[str], tmp_run_dir: Callable, monkeymock: 'MonkeyMock' ): """Test init_clean() fails when abusing --rm option.""" tmp_run_dir('chalmers') mock_clean = monkeymock('cylc.flow.clean.clean') mock_remote_clean = monkeymock('cylc.flow.clean.remote_clean') with pytest.raises(InputError) as exc_info: init_clean('chalmers', opts=CleanOptions(rm_dirs=rm_dirs)) assert "cannot take paths that point to the run directory or above" in str( exc_info.value ) mock_clean.assert_not_called() mock_remote_clean.assert_not_called() PLATFORMS = { 'enterprise': { 'hosts': ['kirk', 'picard'], 'install target': 'picard', 'name': 'enterprise' }, 'voyager': { 'hosts': ['janeway'], 'install target': 'janeway', 'name': 'voyager' }, 'stargazer': { 'hosts': ['picard'], 'install target': 'picard', 'name': 'stargazer' }, 'exeter': { 'hosts': ['localhost'], 'install target': 'localhost', 'name': 'exeter' } } @pytest.mark.parametrize( ('platform_names', 'failed_platforms', 'expected_platforms', 'exc_expected', 'expected_err_msgs'), [ pytest.param( ['exeter'], None, None, False, [], id="Only localhost install target - no remote clean" ), pytest.param( ['exeter', 'enterprise'], None, ['enterprise'], False, [], id="Localhost and remote install target" ), pytest.param( ['enterprise', 'stargazer', 'voyager'], None, ['enterprise', 'voyager'], False, [], id="Only remote install targets" ), pytest.param( ['enterprise', 'stargazer', 'voyager'], {'enterprise': 255}, ['enterprise', 'stargazer', 'voyager'], False, [], id="Install target with 1 failed, 1 successful platform" ), pytest.param( ['enterprise', 'stargazer', 'voyager'], {'enterprise': 255, 'stargazer': 255}, ['enterprise', 'stargazer', 'voyager'], True, ["could not clean on these install target(s)", "[picard]"], id="Install target with all failed platforms" ), pytest.param( ['enterprise', 'voyager'], {'enterprise': 255, 'voyager': 255}, ['enterprise', 'voyager'], True, ["could not clean on these install target(s)", "[picard]", "[janeway]"], id="All install targets have all failed platforms" ), pytest.param( ['enterprise', 'stargazer'], {'enterprise': 1}, ['enterprise'], True, ["could not clean on these install target(s)", "[picard]"], id=("Remote clean cmd fails on a platform for non-SSH reason - " "does not retry") ), ] ) def test_remote_clean( platform_names: list[str], failed_platforms: dict[str, int] | None, expected_platforms: list[str] | None, exc_expected: bool, expected_err_msgs: list[str], monkeymock: 'MonkeyMock', monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, log_filter: Callable ) -> None: """Test remote_clean() logic. Params: platform_names: These platforms will be considered to exist. failed_platforms: If specified, any platforms that clean will artificially fail on in this test case. The key is the platform name, the value is the remote clean cmd return code. expected_platforms: If specified, all the platforms that the remote clean cmd is expected to run on. exc_expected: If a CylcError is expected to be raised. expected_err_msgs: List of error messages expected to be in the log. """ # ----- Setup ----- caplog.set_level(logging.DEBUG, CYLC_LOG) monkeypatch.setattr( 'cylc.flow.clean.platform_from_name', lambda name: PLATFORMS[name] ) # Remove randomness: monkeymock('cylc.flow.clean.shuffle') def mocked_remote_clean_cmd_side_effect(id_, platform, timeout, rm_dirs): proc_ret_code = 0 if failed_platforms and platform['name'] in failed_platforms: proc_ret_code = failed_platforms[platform['name']] return mock.Mock( poll=lambda: proc_ret_code, communicate=lambda: ("Mocked stdout", "Mocked stderr"), args=[] ) mocked_remote_clean_cmd = monkeymock( 'cylc.flow.clean._remote_clean_cmd', spec=_remote_clean_cmd, side_effect=mocked_remote_clean_cmd_side_effect, ) rm_dirs = ["whatever"] # ----- Test ----- id_ = 'foo' if exc_expected: with pytest.raises(CylcError) as exc: cylc_clean.remote_clean( id_, platform_names, timeout='irrelevant', rm_dirs=rm_dirs ) assert "Remote clean failed" in (exc_msg := str(exc.value)) for msg in expected_err_msgs: assert msg in exc_msg else: cylc_clean.remote_clean( id_, platform_names, timeout='irrelevant', rm_dirs=rm_dirs ) for msg in expected_err_msgs: assert log_filter(logging.ERROR, msg) if expected_platforms: for p_name in expected_platforms: mocked_remote_clean_cmd.assert_any_call( id_, PLATFORMS[p_name], rm_dirs, 'irrelevant' ) else: mocked_remote_clean_cmd.assert_not_called() def test_remote_clean__timeout( monkeymock: 'MonkeyMock', monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ): """Test remote_clean() gives a sensible error message for return code 124. """ caplog.set_level(logging.ERROR, CYLC_LOG) monkeymock( 'cylc.flow.clean._remote_clean_cmd', spec=_remote_clean_cmd, return_value=mock.Mock( spec=Popen, poll=lambda: 124, communicate=lambda: ('', '') ) ) monkeypatch.setattr( 'cylc.flow.clean.platform_from_name', lambda name: PLATFORMS[name] ) with pytest.raises(CylcError) as e: cylc_clean.remote_clean( 'blah', platform_names=['stargazer'], timeout='blah' ) assert "cylc clean timed out" in str(e.value) # No need to log the remote clean cmd etc. for timeout assert "ssh" not in caplog.text.lower() assert "stderr" not in caplog.text.lower() @pytest.mark.parametrize( 'rm_dirs, expected_args', [ (None, []), (['holodeck', 'ten_forward'], ['--rm', 'holodeck', '--rm', 'ten_forward']) ] ) def test_remote_clean_cmd( rm_dirs: Optional[List[str]], expected_args: List[str], monkeymock: 'MonkeyMock' ) -> None: """Test _remote_clean_cmd() Params: rm_dirs: Argument passed to _remote_clean_cmd(). expected_args: Expected CLI arguments of the cylc clean command that gets constructed. """ id_ = 'jean/luc/picard' platform = { 'name': 'enterprise', 'install target': 'mars', 'hosts': ['Trill'], 'selection': {'method': 'definition order'} } mock_construct_ssh_cmd = monkeymock( 'cylc.flow.clean.construct_ssh_cmd', return_value=['blah']) monkeymock('cylc.flow.clean.Popen') cylc_clean._remote_clean_cmd(id_, platform, rm_dirs, timeout='dunno') args, _kwargs = mock_construct_ssh_cmd.call_args constructed_cmd = args[0] assert constructed_cmd == [ 'clean', '--local-only', '--no-scan', id_, *expected_args ] def test_clean_top_level(tmp_run_dir: Callable): """Test that cleaning last remaining run dir inside a workflow dir removes the top level dir if it's empty (excluding _cylc-install).""" # Setup id_ = 'blue/planet/run1' run_dir: Path = tmp_run_dir(id_, installed=True, named=True) cylc_install_dir = run_dir.parent / WorkflowFiles.Install.DIRNAME assert cylc_install_dir.is_dir() runN_symlink = run_dir.parent / WorkflowFiles.RUN_N assert runN_symlink.exists() # Test clean(id_, run_dir) assert not run_dir.parent.parent.exists() # Now check that if the top level dir is not empty, it doesn't get removed run_dir: Path = tmp_run_dir(id_, installed=True, named=True) jellyfish_file = (run_dir.parent / 'jellyfish.txt') jellyfish_file.touch() clean(id_, run_dir) assert cylc_install_dir.is_dir() assert jellyfish_file.exists() @pytest.mark.parametrize( 'pattern, filetree, expected_matches', [ pytest.param( '**', FILETREE_1, ['cylc-run/foo/bar', 'cylc-run/foo/bar/log'], id="filetree1 **" ), pytest.param( '*', FILETREE_1, ['cylc-run/foo/bar/flow.cylc', 'cylc-run/foo/bar/log', 'cylc-run/foo/bar/mirkwood', 'cylc-run/foo/bar/rincewind.txt'], id="filetree1 *" ), pytest.param( '**/*.txt', FILETREE_1, ['cylc-run/foo/bar/log/bib/fortuna.txt', 'cylc-run/foo/bar/log/temba.txt', 'cylc-run/foo/bar/rincewind.txt'], id="filetree1 **/*.txt" ), pytest.param( '**', FILETREE_2, ['cylc-run/foo/bar', 'cylc-run/foo/bar/share', 'cylc-run/foo/bar/share/cycle'], id="filetree2 **" ), pytest.param( 'share', FILETREE_2, ['cylc-run/foo/bar/share'], id="filetree2 share" ), pytest.param( '**', FILETREE_3, ['cylc-run/foo/bar', 'cylc-run/foo/bar/share/cycle'], id="filetree3 **" ), pytest.param( '**/s*', FILETREE_3, ['cylc-run/foo/bar/share', 'cylc-run/foo/bar/share/cycle/sokath.txt'], id="filetree3 **/s*" ), pytest.param( '**', FILETREE_4, ['cylc-run/foo/bar', 'cylc-run/foo/bar/share/cycle'], id="filetree4 **" ), ] ) def test_glob_in_run_dir( pattern: str, filetree: Dict[str, Any], expected_matches: List[str], tmp_path: Path, tmp_run_dir: Callable ) -> None: """Test that glob_in_run_dir() returns the minimal set of results with no redundant paths. """ # Setup cylc_run_dir: Path = tmp_run_dir() id_ = 'foo/bar' run_dir = cylc_run_dir / id_ create_filetree(filetree, tmp_path, tmp_path) symlink_dirs = [run_dir / i for i in get_symlink_dirs(id_, run_dir)] expected = [tmp_path / i for i in expected_matches] # Test assert glob_in_run_dir(run_dir, pattern, symlink_dirs) == expected cylc-flow-8.6.4/tests/unit/test_async_util.py0000664000175000017500000001512615202510242021516 0ustar alastairalastair# 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 . import asyncio from inspect import signature import logging from pathlib import Path from random import random import pytest from cylc.flow.async_util import ( pipe, asyncqgen, scandir, ) LOG = logging.getLogger('test') @pipe() async def a_range(n): for num in range(n): LOG.info(f'a_range({n})') yield num @pipe async def even(x): LOG.info(f'even({x})') return x % 2 == 0 @pipe async def mult(x, y, kwarg='useless kwarg'): LOG.info(f'mult{x, y}') return x * y @pipe async def sleepy(x): """A filter which waits a while then passes.""" LOG.info(f'sleepy({x})') await asyncio.sleep(0.1) return True @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe(preserve_order): """It passes values through the pipe.""" pipe = a_range(5) | even | mult(2) pipe.preserve_order = preserve_order result = [] async for num in pipe: result.append(num) assert result == [ 0, 4, 8, ] @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe_single(preserve_order): """It allow single-step pipes.""" pipe = a_range(5) pipe.preserve_order = preserve_order result = [] async for num in pipe: result.append(num) assert result == [ 0, 1, 2, 3, 4 ] @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe_reusable(preserve_order): """It can be re-used once depleted.""" pipe = a_range(5) | even | mult(2) pipe.preserve_order = preserve_order for _ in range(5): result = [] async for num in pipe: result.append(num) assert result == [ 0, 4, 8, ] @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe_filter_stop(preserve_order): """It yields values early with the filter_stop argument.""" pipe = a_range(5) | even(filter_stop=False) pipe |= mult(10) pipe.preserve_order = preserve_order result = [] async for num in pipe: result.append(num) # the even numbers should be multiplied by 10 # the odd numbers should be yielded early (so don't get multiplied) assert result == [ 0, 1, 20, 3, 40, ] @pipe async def one(x): await asyncio.sleep(random() / 5) return x @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe_preserve_order(preserve_order): """It should control result order according to pipe configuration.""" n = 50 pipe = a_range(n) | one | one | one pipe.preserve_order = preserve_order result = [] async for item in pipe: result.append(item) # the odds of getting 50 items in order by chance are pretty slim assert (result == list(range(n))) is preserve_order @pytest.mark.parametrize('preserve_order', (True, False)) async def test_pipe_concurrent(caplog, preserve_order): """It runs pipes concurrently. It is easy to make something which appears to be concurrent, this test is intended to ensure that it actually IS concurrent. """ pipe = a_range(5) | even | sleepy | mult(2) pipe.preserve_order = preserve_order caplog.set_level(logging.INFO, 'test') async for num in pipe: pass order = [ # a list of the log messages generated by each step of the pipe # as it processes an item x[2].split('(')[0] for x in caplog.record_tuples ] assert 'mult' in order assert len(order) == 4 * 4 # 4 steps * 4 items yielded by a_range # ensure that the steps aren't completed in order (as sync code would) # the sleep should ensure this # NOTE: not the best test but better than nothing assert order != [ 'a_range', 'even', 'sleepy', 'mult' ] * 4 def test_pipe_str(): """It has helpful textual representations.""" pipe = a_range(5) | even(filter_stop=False) | mult(10, kwarg=42) assert str(pipe) == 'a_range(5)' assert repr(pipe) == 'a_range(5) | even() | mult(10, kwarg=42)' @pipe() # NOTE: these brackets are what the next function is testing async def div(x, y): return x / y def test_pipe_brackets(): """Ensure that pipe functions can be declared with or without brackets.""" pipe = a_range(5) | div assert repr(pipe) == 'a_range(5) | div()' @pipe async def documented(x: str, y: int = 0): """The docstring for the pipe function.""" pass def test_documentation(): """It should preserve the docstring, signature & annotations of the wrapped function.""" assert documented.__doc__ == 'The docstring for the pipe function.' assert documented.__annotations__ == {'x': str, 'y': int} assert str(signature(documented)) == '(x: str, y: int = 0)' def test_rewind(): """It should be possible to move throught the pipe stages.""" pipe = a_range | mult | even assert pipe.fastforward().rewind() == pipe async def test_asyncqgen(): """It should provide an async gen interface to an async queue.""" queue = asyncio.Queue() gen = asyncqgen(queue) await queue.put(1) await queue.put(2) await queue.put(3) ret = [] async for item in gen: ret.append(item) assert ret == [1, 2, 3] async def test_scandir(tmp_path: Path): """It should list directory contents (including symlinks).""" (tmp_path / 'a').touch() (tmp_path / 'b').touch() (tmp_path / 'c').symlink_to(tmp_path / 'b') assert sorted(await scandir(tmp_path)) == [ Path(tmp_path, 'a'), Path(tmp_path, 'b'), Path(tmp_path, 'c') ] async def test_scandir_non_exist(tmp_path: Path): """scandir() should raise FileNotFoundError if called on a path that doesn't exist.""" with pytest.raises(FileNotFoundError): await scandir(tmp_path / 'HORSE') cylc-flow-8.6.4/tests/unit/test_links.py0000664000175000017500000000575515202510242020473 0ustar alastairalastair# 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 . """Check links inserted into internal documentation. Reason for doing this here: - Some links don't appear to be being picked up by Cylc-doc linkcheck. - As we have more links it's worth checking them here, rather than waiting for them to show up in Cylc. """ import os import re import pytest import fnmatch from time import sleep from pathlib import Path from urllib import request from urllib.error import HTTPError EXCLUDE = [ r'*//www.gnu.org/licenses/', r'*//my-site.com/*', r'*//ahost/%(owner)s/notes/%(workflow)s', r'*//www.h2g2.com/*', r'*//web.archive.org/*' ] def get_links(): searchdir = Path(__file__).parent.parent.parent / 'cylc' / 'flow' return sorted({ url for file_ in searchdir.rglob('*.py') for url in re.findall( r'(https?:\/\/.*?)[\n\s\>`"\',]', file_.read_text() ) if not any( fnmatch.fnmatch(url, pattern) for pattern in EXCLUDE ) }) def make_request(link): """Make an HTTP request, using GITHUB_TOKEN for GitHub URLs if available. The GITHUB_TOKEN environment variable contains a GitHub Actions token. This is used to authenticate the workflow requests to github.com, which helps avoid rate limiting (unauthenticated requests are limited to 60/hour, authenticated to 5000/hour). """ req = request.Request(link) github_token = os.environ.get('GITHUB_TOKEN') if github_token and 'github.com' in link: req.add_header('Authorization', f'token {github_token}') return request.urlopen(req).getcode() @pytest.mark.linkcheck @pytest.mark.parametrize('link', get_links()) def test_embedded_url(link): """Check links in the source code are not broken. TIP: use `--dist=load` when running pytest to enable parametrized tests to run in parallel """ try: make_request(link) except HTTPError as exc: # Allowing 403 (forbidden) & 429 (rate-limited) as the link # is probably valid, but we are blocked. if exc.code in {403, 429}: pytest.skip(f'{exc} | {link}') # Sleep and retry to reduce risk of flakiness: sleep(10) try: make_request(link) except HTTPError as exc: raise Exception(f'{exc} | {link}') cylc-flow-8.6.4/tests/unit/test_scheduler.py0000664000175000017500000001303115202510242021313 0ustar alastairalastair# 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 . """Tests for Cylc scheduler server.""" import logging from secrets import token_hex from time import time from types import SimpleNamespace from typing import List from unittest.mock import ( MagicMock, Mock, ) import pytest from cylc.flow import CYLC_LOG from cylc.flow.exceptions import InputError from cylc.flow.scheduler import Scheduler from cylc.flow.scheduler_cli import RunOptions from cylc.flow.task_pool import TaskPool from cylc.flow.task_proxy import TaskProxy from cylc.flow.workflow_status import AutoRestartMode @pytest.mark.parametrize( 'opts_to_test, is_restart, err_msg', [ pytest.param( ['icp', 'startcp', 'starttask'], True, "option --{} is not valid for restart", id="start opts on restart" ), pytest.param( ['icp', 'startcp', 'starttask'], False, "option --{}=reload is not valid", id="start opts =reload" ), pytest.param( ['fcp', 'stopcp'], False, "option --{}=reload is only valid for restart", id="end opts =reload when not restart" ), ] ) def test_check_startup_opts( opts_to_test: List[str], is_restart: bool, err_msg: str ) -> None: """Test Scheduler._check_startup_opts()""" for opt in opts_to_test: mocked_scheduler = Mock(is_restart=is_restart) mocked_scheduler.options = SimpleNamespace(**{opt: 'reload'}) with pytest.raises(InputError) as excinfo: Scheduler._check_startup_opts(mocked_scheduler) assert err_msg.format(opt) in str(excinfo) @pytest.mark.parametrize( 'auto_restart_time, expected', [ (-1, True), (0, True), (1, False), (None, False), ] ) def test_should_auto_restart_now( auto_restart_time, expected, monkeypatch: pytest.MonkeyPatch ): """Test Scheduler.should_auto_restart_now().""" time_now = time() monkeypatch.setattr('cylc.flow.scheduler.time', lambda: time_now) if auto_restart_time is not None: auto_restart_time += time_now mock_schd = Mock(spec=Scheduler, auto_restart_time=auto_restart_time) assert Scheduler.should_auto_restart_now(mock_schd) == expected def test_release_tasks_to_run__auto_restart(): """Test that Scheduler.release_tasks_to_run() works as expected during auto restart.""" mock_schd = MagicMock( auto_restart_time=(time() - 100), auto_restart_mode=AutoRestartMode.RESTART_NORMAL, is_paused=False, stop_mode=None, pool=Mock( spec=TaskPool, get_tasks=lambda: [Mock(spec=TaskProxy)] ), workflow='parachutes', options=RunOptions(), task_job_mgr=MagicMock() ) Scheduler.release_tasks_to_run(mock_schd) # Should not actually release any more tasks, just submit the # preparing ones mock_schd.pool.release_queued_tasks.assert_not_called() Scheduler.start_job_submission(mock_schd, mock_schd.pool.get_tasks()) mock_schd.submit_task_jobs.assert_called() def test_auto_restart_DNS_error(mock_glbl_cfg, caplog, log_filter): """Ensure that DNS errors in host selection are caught.""" # fake a "get address info" error # this error can occur due to an unknown host resulting from broken # DNS or an invalid host name in the global config hostname = f'elephant_{token_hex(4)}' mock_glbl_cfg( 'cylc.flow.host_select.glbl_cfg', f''' [scheduler] [[run hosts]] available = {hostname} ''' ) schd = Mock( workflow='myworkflow', options=RunOptions(abort_if_any_task_fails=False), INTERVAL_AUTO_RESTART_ERROR=0, ) caplog.set_level(logging.ERROR, CYLC_LOG) assert not Scheduler.workflow_auto_restart(schd, max_retries=2) assert log_filter(contains=hostname) def test_auto_restart_popen_error(monkeypatch, caplog, log_filter): """Ensure that subprocess errors are handled.""" def _select_workflow_host(cached=False): # mock a host-select return value return ('foo', 'foo') monkeypatch.setattr( 'cylc.flow.scheduler.select_workflow_host', _select_workflow_host, ) def _popen(*args, **kwargs): # mock an auto-restart command failure return Mock( wait=lambda: 1, communicate=lambda: ('mystdout', 'mystderr'), ) monkeypatch.setattr( 'cylc.flow.scheduler.Popen', _popen, ) schd = Mock( workflow='myworkflow', options=RunOptions(abort_if_any_task_fails=False), INTERVAL_AUTO_RESTART_ERROR=0, ) caplog.set_level(logging.ERROR, CYLC_LOG) assert not Scheduler.workflow_auto_restart(schd, max_retries=2) assert log_filter(contains='mystderr') cylc-flow-8.6.4/tests/unit/test_platforms_get_host.py0000664000175000017500000000750615202510242023252 0ustar alastairalastair# 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 . # # Tests for the get_host_from_platform lookup. import pytest from cylc.flow.exceptions import NoHostsError, NoPlatformsError from cylc.flow.platforms import get_host_from_platform, get_platform_from_group from cylc.flow.exceptions import CylcError TEST_PLATFORM = { 'name': 'Elephant', 'hosts': ['nellie', 'dumbo', 'jumbo'], 'selection': {'method': 'definition order'} } TEST_GROUP = { 'platforms': ['one', 'two', 'three'], 'selection': {'method': 'definition order'} } @pytest.mark.parametrize( 'badhosts, expect', [ pytest.param(None, 'nellie'), pytest.param({'nellie', 'dumbo'}, 'jumbo') ] ) def test_get_host_from_platform(badhosts, expect): platform = TEST_PLATFORM assert get_host_from_platform(platform, badhosts) == expect def test_get_host_from_platform_fails_no_goodhosts(): platform = TEST_PLATFORM with pytest.raises(NoHostsError) as err: get_host_from_platform(platform, {'nellie', 'dumbo', 'jumbo'}) assert err.exconly() == ( 'cylc.flow.exceptions.NoHostsError: ' 'Unable to find valid host for Elephant' ) def test_get_host_from_platform_fails_bad_method(): platform = TEST_PLATFORM.copy() platform['selection']['method'] = 'roulette' with pytest.raises(CylcError) as err: get_host_from_platform(platform, {'Elephant'}) assert err.exconly() == ( 'cylc.flow.exceptions.CylcError: method "roulette" is not a ' 'supported host selection method.' ) @pytest.mark.parametrize( 'expect, bad_hosts', [ pytest.param( 'one', None, id='No bad_hosts get platform one'), pytest.param( 'three', {'foo', 'bar'}, id='All platforms bad except platform three') ] ) def test_get_platform_from_group(monkeypatch, expect, bad_hosts): def get_plat(name): if name == 'three': return {'hosts': {'foo', 'bar', 'baz'}} else: return {'hosts': {'foo', 'bar'}} monkeypatch.setattr('cylc.flow.platforms.platform_from_name', get_plat) platform = get_platform_from_group(TEST_GROUP, 'mygroup_name', bad_hosts) assert platform == expect def test_get_platform_from_group_fails_no_goodhosts(monkeypatch): monkeypatch.setattr( 'cylc.flow.platforms.platform_from_name', lambda _: {'hosts': {'foo', 'bar', 'baz'}}, ) with pytest.raises(NoPlatformsError) as err: get_platform_from_group( TEST_GROUP, 'mygroup_name', {'foo', 'bar', 'baz'} ) assert err.exconly() == ( 'cylc.flow.exceptions.NoPlatformsError: ' 'Unable to find a platform from group mygroup_name.' ) def test_get_platform_from_group_fails_bad_method(monkeypatch): monkeypatch.setattr( 'cylc.flow.platforms.platform_from_name', lambda _: {'hosts': {'foo', 'bar', 'baz'}}, ) group = TEST_GROUP.copy() group['selection']['method'] = 'roulette' with pytest.raises(CylcError) as err: get_platform_from_group(group, {'foo'}) assert err.exconly() == ( 'cylc.flow.exceptions.CylcError: "roulette" is not a ' 'supported platform selection method.' ) cylc-flow-8.6.4/tests/integration/0000775000175000017500000000000015202510242017272 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/main_loop/0000775000175000017500000000000015202510242021247 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/main_loop/__init__.py0000664000175000017500000000000015202510242023346 0ustar alastairalastaircylc-flow-8.6.4/tests/integration/main_loop/test_auto_restart.py0000664000175000017500000000352515202510242025401 0ustar alastairalastair# 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 . import asyncio from unittest.mock import Mock import pytest from cylc.flow.main_loop import MainLoopPluginException from cylc.flow.scheduler import Scheduler from cylc.flow.workflow_status import AutoRestartMode async def test_no_detach( one_conf, flow, scheduler, run, mock_glbl_cfg, log_filter, monkeypatch: pytest.MonkeyPatch ): """Test that the Scheduler aborts when auto restart tries to happen while in no-detach mode.""" mock_glbl_cfg( 'cylc.flow.scheduler.glbl_cfg', ''' [scheduler] [[main loop]] plugins = auto restart [[[auto restart]]] interval = PT1S ''') monkeypatch.setattr( 'cylc.flow.main_loop.auto_restart._should_auto_restart', Mock(return_value=AutoRestartMode.RESTART_NORMAL) ) id_: str = flow(one_conf) schd: Scheduler = scheduler(id_, paused_start=True, no_detach=True) with pytest.raises(MainLoopPluginException) as exc: async with run(schd): await asyncio.sleep(2) assert log_filter(contains=f"Workflow shutting down - {exc.value}") cylc-flow-8.6.4/tests/integration/test_task_job_mgr.py0000664000175000017500000002221515202510242023346 0ustar alastairalastair# 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 . from contextlib import suppress import json import logging from typing import Any as Fixture from unittest.mock import Mock from cylc.flow import CYLC_LOG from cylc.flow.job_runner_mgr import JOB_FILES_REMOVED_MESSAGE from cylc.flow.scheduler import Scheduler from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_RUNNING, ) async def test_run_job_cmd_no_hosts_error( flow, scheduler, start, mock_glbl_cfg, log_filter, ): """It should catch NoHostsError. NoHostsError's should be caught and handled rather than raised because they will cause problems (i.e. trigger shutdown) if they make it to the Scheduler. NoHostError's can occur in the poll & kill logic, this test ensures that these methods catch the NoHostsError and handle the event as a regular SSH failure by pushing the issue down the 255 callback. See https://github.com/cylc/cylc-flow/pull/5195 """ # define a platform mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[no-host-platform]] ''', ) # define a workflow with a task which runs on that platform id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo' } }, 'runtime': { 'foo': { 'platform': 'no-host-platform' } } }) # start the workflow schd: Scheduler = scheduler(id_) async with start(schd) as log: # set logging to debug level log.set_level(logging.DEBUG, CYLC_LOG) # tell Cylc the task is running on that platform schd.pool.get_tasks()[0].state_reset(TASK_STATUS_RUNNING) schd.pool.get_tasks()[0].platform = { 'name': 'no-host-platform', 'hosts': ['no-host-platform'], } # tell Cylc that that platform is not contactable # (i.e. all hosts are in bad_hosts) # (this casuses the NoHostsError to be raised) schd.task_job_mgr.bad_hosts.add('no-host-platform') # polling the task should not result in an error... schd.task_job_mgr.poll_task_jobs(schd.pool.get_tasks()) # ...but the failure should be logged assert log_filter( contains='No available hosts for no-host-platform', ) log.clear() # killing the task should not result in an error... schd.task_job_mgr.kill_task_jobs(schd.pool.get_tasks()) # ...but the failure should be logged assert log_filter( contains='No available hosts for no-host-platform', ) async def test__run_job_cmd_logs_platform_lookup_fail( one_conf: Fixture, flow: Fixture, scheduler: Fixture, run: Fixture, db_select: Fixture, caplog: Fixture ) -> None: """TaskJobMg._run_job_cmd handles failure to get platform.""" id_: str = flow(one_conf) schd: 'Scheduler' = scheduler(id_, paused_start=True) # Run async with run(schd): from types import SimpleNamespace schd.task_job_mgr._run_job_cmd( schd.task_job_mgr.JOBS_POLL, [SimpleNamespace(platform={'name': 'culdee fell summit'})], None, None ) warning = caplog.records[-1] assert warning.levelname == 'ERROR' assert 'Unable to run command jobs-poll' in warning.msg async def test__prep_submit_task_job_impl_handles_execution_time_limit( flow: Fixture, scheduler: Fixture, start: Fixture, ): """Ensure that emptying the execution time limit unsets it. Previously unsetting the etl by either broadcast or reload would not unset a previous etl. See https://github.com/cylc/cylc-flow/issues/5891 """ id_ = flow({ "scheduling": { "cycling mode": "integer", "graph": {"R1": "a"} }, "runtime": { "root": {}, "a": { "script": "sleep 10", "execution time limit": 'PT5S' } } }) # Run in live mode - function not called in sim mode. schd = scheduler(id_, run_mode='live') async with start(schd): task_a = schd.pool.get_tasks()[0] # We're not interested in the job file stuff, just # in the summary state. with suppress(FileExistsError): schd.task_job_mgr._prep_submit_task_job_impl( task_a, task_a.tdef.rtconfig ) assert task_a.summary['execution_time_limit'] == 5.0 # If we delete the etl it gets deleted in the summary: task_a.tdef.rtconfig['execution time limit'] = None with suppress(FileExistsError): schd.task_job_mgr._prep_submit_task_job_impl( task_a, task_a.tdef.rtconfig ) assert not task_a.summary.get('execution_time_limit', '') # put everything back and test broadcast too. task_a.tdef.rtconfig['execution time limit'] = 5.0 task_a.summary['execution_time_limit'] = 5.0 schd.broadcast_mgr.broadcasts = { '1': {'a': {'execution time limit': None}}} with suppress(FileExistsError): # We run a higher level function here to ensure # that the broadcast is applied. schd.task_job_mgr._prep_submit_task_job(task_a) assert not task_a.summary.get('execution_time_limit', '') async def test_broadcast_platform_change( mock_glbl_cfg, flow, scheduler, start, log_filter, ): """Broadcast can change task platform. Even after host selection failure. see https://github.com/cylc/cylc-flow/issues/6320 """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[foo]] hosts = food ''') id_ = flow({ "scheduling": {"graph": {"R1": "mytask"}}, # Platform = None doesn't cause this issue! "runtime": {"mytask": {"platform": "localhost"}}}) schd: Scheduler = scheduler(id_, run_mode='live') async with start(schd): # Change the task platform with broadcast: schd.broadcast_mgr.put_broadcast( ['1'], ['mytask'], [{'platform': 'foo'}]) # Simulate prior failure to contact hosts: schd.bad_hosts.add('food') # Attempt job submission: schd.submit_task_jobs(schd.pool.get_tasks()) # Check that task platform hasn't become "localhost": assert schd.pool.get_tasks()[0].platform['name'] == 'foo' # ... and that remote init failed because all hosts bad: assert log_filter(regex=r"platform: foo .*\(no hosts were reachable\)") async def test_poll_job_deleted_log_folder( one_conf, flow, scheduler, start, log_filter ): """Capture a task error caused by polling finding the job log dir deleted. https://github.com/cylc/cylc-flow/issues/6425 """ response = { 'run_signal': JOB_FILES_REMOVED_MESSAGE, 'run_status': 1, 'job_runner_exit_polled': 1, } schd: Scheduler = scheduler(flow(one_conf)) async with start(schd): itask = schd.pool.get_tasks()[0] itask.submit_num = 1 job_id = itask.job_tokens.relative_id schd.task_job_mgr._poll_task_job_callback( itask, cmd_ctx=Mock(), line=f'2025-02-13T12:08:30Z|{job_id}|{json.dumps(response)}', ) assert itask.state(TASK_STATUS_FAILED) assert log_filter( logging.ERROR, f"job log directory {job_id} no longer exists" ) async def test__prep_submit_task_job_impl_handles_all_old_platform_settings( flow: Fixture, scheduler: Fixture, start: Fixture, mock_glbl_cfg: Fixture, ): """Ensure that if the old host/batch system settings are set that task job manager will wait for the any hostname to be resolved. https://github.com/cylc/cylc-flow/pull/6990 """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[bakery]] hosts = localhost job runner = loaf ''', ) id_ = flow({ "scheduling": {"graph": {"R1": "a"}}, "runtime": {"a": {"job": {"batch system": "loaf"}}} }) schd = scheduler(id_, run_mode='live') async with start(schd): task_a = schd.pool.get_tasks()[0] with suppress(FileExistsError): schd.task_job_mgr._prep_submit_task_job(task_a) assert task_a.platform['name'] == 'bakery' cylc-flow-8.6.4/tests/integration/test_data_store_mgr.py0000664000175000017500000010364215202510242023703 0ustar alastairalastair# 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 . import logging from logging import INFO from typing import ( Iterable, List, cast, ) from unittest.mock import Mock import pytest from cylc.flow import LOG from cylc.flow.commands import ( force_trigger_tasks, run_cmd, ) from cylc.flow.data_messages_pb2 import ( PbJob, PbPrerequisite, PbTaskProxy, ) from cylc.flow.data_store_mgr import ( EDGES, FAMILY_PROXIES, JOBS, TASK_PROXIES, TASKS, WORKFLOW, ) from cylc.flow.id import ( TaskTokens, Tokens, ) from cylc.flow.network.log_stream_handler import ProtobufStreamHandler from cylc.flow.scheduler import Scheduler from cylc.flow.task_events_mgr import TaskEventsManager from cylc.flow.task_outputs import ( TASK_OUTPUT_FAILED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, ) from cylc.flow.wallclock import get_current_time_string # NOTE: Some of these tests mutate the data store, so running them in # isolation may see failures when they actually pass if you run the # whole file def job_config(schd): return { 'owner': schd.owner, 'submit_num': 3, 'task_id': '1/foo', 'job_runner_name': 'background', 'env-script': None, 'err-script': None, 'exit-script': None, 'execution_time_limit': None, 'init-script': None, 'post-script': None, 'pre-script': None, 'script': 'sleep 5; echo "I come in peace"', 'work_d': None, 'directives': {}, 'environment': {"FOO": "foo"}, 'param_var': {}, 'platform': {'name': 'platform'}, 'execution retry delays': [10.0, 20.0], 'execution time limit': 30.0, } @pytest.fixture def job_db_row(): return [ '1', 'foo', 4, '2020-04-03T13:40:18+13:00', 0, '2020-04-03T13:40:20+13:00', None, None, 'background', '20542', 'localhost', ] def int_id(_): return '1/foo/03' def ext_id(schd): return f'~{schd.owner}/{schd.workflow}//{int_id(None)}' def get_pb_prereqs(schd: 'Scheduler') -> 'List[PbPrerequisite]': """Get all protobuf prerequisites from the data store task proxies.""" return [ p for t in cast( 'Iterable[PbTaskProxy]', schd.data_store_mgr.updated[TASK_PROXIES].values() ) for p in t.prerequisites ] def get_pb_job(schd: Scheduler, itask: TaskProxy) -> PbJob: """Get the protobuf job for a given task from the data store.""" return schd.data_store_mgr.data[schd.id][JOBS][itask.job_tokens.id] @pytest.fixture(scope='module') async def mod_harness(mod_flow, mod_scheduler, mod_start): flow_def = { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'foo => bar' } } } id_: str = mod_flow(flow_def) schd: 'Scheduler' = mod_scheduler(id_) async with mod_start(schd): await schd.update_data_structure() data = schd.data_store_mgr.data[schd.data_store_mgr.workflow_id] yield schd, data @pytest.fixture(scope='module') async def edgeharness(mod_flow, mod_scheduler, mod_start): """Graph with > n in window edge at n=1.""" flow_def = { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': """ b1 & b2 => c1 & c2 c1 => c2 """ } } } id_: str = mod_flow(flow_def) schd: 'Scheduler' = mod_scheduler(id_) async with mod_start(schd): await schd.update_data_structure() data = schd.data_store_mgr.data[schd.data_store_mgr.workflow_id] yield schd, data @pytest.fixture(scope='module') async def xharness(mod_flow, mod_scheduler, mod_start): """Like harness, but add xtriggers.""" flow_def = { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'xtriggers': { 'x': 'xrandom(0)', 'x2': 'xrandom(0)', 'y': 'xrandom(0, _=1)' }, 'graph': { 'R1': """ @x => foo @x2 => foo @y => foo @x => bar """ } } } id_: str = mod_flow(flow_def) schd: 'Scheduler' = mod_scheduler(id_) async with mod_start(schd): await schd.update_data_structure() data = schd.data_store_mgr.data[schd.data_store_mgr.workflow_id] yield schd, data def collect_states(data, node_type): return [ t.state for t in data[node_type].values() if t.state != '' ] def test_generate_definition_elements(mod_harness): """Test method that generates all definition elements.""" schd, data = mod_harness task_defs = schd.config.taskdefs.keys() assert len(data[TASKS]) == len(task_defs) assert len(data[TASK_PROXIES]) == len(task_defs) def test_generate_graph_elements(mod_harness): schd, data = mod_harness task_defs = schd.config.taskdefs.keys() assert len(data[TASK_PROXIES]) == len(task_defs) def test_get_data_elements(mod_harness): schd, data = mod_harness flow_msg = schd.data_store_mgr.get_data_elements(TASK_PROXIES) assert len(flow_msg.added) == len(data[TASK_PROXIES]) flow_msg = schd.data_store_mgr.get_data_elements(WORKFLOW) assert flow_msg.added.last_updated == data[WORKFLOW].last_updated none_msg = schd.data_store_mgr.get_data_elements('fraggle') assert len(none_msg.ListFields()) == 0 def test_get_entire_workflow(mod_harness): """Test method that populates the entire workflow protobuf message.""" schd, data = mod_harness flow_msg = schd.data_store_mgr.get_entire_workflow() assert len(flow_msg.task_proxies) == len(data[TASK_PROXIES]) def test_increment_graph_window(mod_harness): """Test method that adds and removes elements window boundary.""" schd, data = mod_harness assert schd.data_store_mgr.prune_trigger_nodes assert len(data[TASK_PROXIES]) == 2 def test_in_window_extra_edges(edgeharness): """Test edges beyond walk but within window are generated.""" schd, data = edgeharness w_id = schd.data_store_mgr.workflow_id assert f'{w_id}//$edge|1/c1|1/c2' in data[EDGES] def test_initiate_data_model(mod_harness): """Test method that generates all data elements in order.""" schd: Scheduler schd, data = mod_harness assert len(data[WORKFLOW].task_proxies) == 2 schd.data_store_mgr.initiate_data_model(reloaded=True) assert len(data[WORKFLOW].task_proxies) == 2 # Check n-window preserved on reload: schd.data_store_mgr.set_graph_window_extent(2) schd.data_store_mgr.update_data_structure() assert schd.data_store_mgr.n_edge_distance == 2 schd.data_store_mgr.initiate_data_model(reloaded=True) assert schd.data_store_mgr.n_edge_distance == 2 async def test_delta_task_state(mod_harness): """Test update_data_structure. This method will generate and apply adeltas/updates given.""" schd, data = mod_harness # follow only needs to happen once .. tests working on the same object? w_id = schd.data_store_mgr.workflow_id schd.data_store_mgr.data[w_id] = data assert TASK_STATUS_FAILED not in set(collect_states(data, TASK_PROXIES)) for itask in schd.pool.get_tasks(): itask.state.reset(TASK_STATUS_FAILED) schd.data_store_mgr.delta_task_state(itask) assert TASK_STATUS_FAILED in set(collect_states( schd.data_store_mgr.updated, TASK_PROXIES)) # put things back the way we found them for itask in schd.pool.get_tasks(): itask.state.reset(TASK_STATUS_WAITING) schd.data_store_mgr.delta_task_state(itask) await schd.update_data_structure() async def test_delta_task_held(mod_harness): """Test update_data_structure. This method will generate and apply adeltas/updates given.""" schd: Scheduler schd, data = mod_harness schd.pool.hold_tasks({TaskTokens('*', 'root')}) await schd.update_data_structure() assert True in {t.is_held for t in data[TASK_PROXIES].values()} for itask in schd.pool.get_tasks(): itask.state.reset(is_held=False) schd.data_store_mgr.delta_task_held( itask.tdef.name, itask.point, False ) assert True not in { t.is_held for t in schd.data_store_mgr.updated[TASK_PROXIES].values() } # put things back the way we found them schd.pool.release_held_tasks({TaskTokens('*', 'root')}) await schd.update_data_structure() def test_insert_job(mod_harness): """Test method that adds a new job to the store.""" schd: Scheduler schd, data = mod_harness assert len(schd.data_store_mgr.added[JOBS]) == 0 itask = schd.pool.get_tasks()[0] schd.data_store_mgr.insert_job(itask, 'submitted', job_config(schd)) assert len(schd.data_store_mgr.added[JOBS]) == 1 assert ext_id(schd) in schd.data_store_mgr.added[JOBS] def test_insert_db_job(mod_harness, job_db_row): """Test method that adds a new job from the db to the store.""" schd: Scheduler schd, data = mod_harness assert len(schd.data_store_mgr.added[JOBS]) == 1 schd.data_store_mgr.insert_db_job(0, job_db_row) assert len(schd.data_store_mgr.added[JOBS]) == 2 assert ext_id(schd) in schd.data_store_mgr.added[JOBS] def test_delta_job_msg(mod_harness): """Test method adding messages to job element.""" schd: Scheduler schd, data = mod_harness j_id = ext_id(schd) tokens = Tokens(j_id) # First update creation assert schd.data_store_mgr.updated[JOBS].get('j_id') is None schd.data_store_mgr.delta_job_msg(tokens, 'The Atomic Age') assert schd.data_store_mgr.updated[JOBS][j_id].messages def test_delta_job_attr(mod_harness): """Test method modifying job fields to job element.""" schd: Scheduler schd, data = mod_harness schd.data_store_mgr.delta_job_attr( Mock(job_tokens=Tokens(ext_id(schd))), 'job_runner_name', 'at' ) assert schd.data_store_mgr.updated[JOBS][ext_id(schd)].messages != ( schd.data_store_mgr.added[JOBS][ext_id(schd)].job_runner_name ) def test_delta_job_time(mod_harness): """Test method setting job state change time.""" schd: Scheduler schd, data = mod_harness event_time = get_current_time_string() schd.data_store_mgr.delta_job_time( Mock(job_tokens=Tokens(ext_id(schd))), 'submitted', event_time ) job_updated = schd.data_store_mgr.updated[JOBS][ext_id(schd)] with pytest.raises(ValueError): job_updated.HasField('jumped_time') assert job_updated.submitted_time != ( schd.data_store_mgr.added[JOBS][ext_id(schd)].submitted_time ) async def test_update_data_structure(mod_harness): """Test update_data_structure. This method will generate and apply adeltas/updates given.""" schd, data = mod_harness w_id = schd.data_store_mgr.workflow_id schd.data_store_mgr.data[w_id] = data schd.pool.hold_tasks({TaskTokens('*', 'root')}) await schd.update_data_structure() assert TASK_STATUS_FAILED not in set(collect_states(data, TASK_PROXIES)) assert TASK_STATUS_FAILED not in set(collect_states(data, FAMILY_PROXIES)) assert TASK_STATUS_FAILED not in data[WORKFLOW].state_totals assert len({t.id for t in data[TASK_PROXIES].values() if t.is_held}) == 2 for itask in schd.pool.get_tasks(): itask.state.reset(TASK_STATUS_FAILED) schd.data_store_mgr.delta_task_state(itask) schd.data_store_mgr.update_data_structure() # State change applied assert TASK_STATUS_FAILED in set(collect_states(data, TASK_PROXIES)) # family state changed and applied assert TASK_STATUS_FAILED in set(collect_states(data, FAMILY_PROXIES)) async def test_prune_data_store(flow, scheduler, start): """Test prune_data_store. This method will expand and reduce the data-store to invoke pruning. Also test rapid addition and removal of families (as happens with suicide triggers): https://github.com/cylc/cylc-ui/issues/1999 """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo => bar' } }, 'runtime': { 'FOOBAR': {}, 'FOO': { 'inherit': 'FOOBAR' }, 'foo': { 'inherit': 'FOO' }, 'BAR': { 'inherit': 'FOOBAR' }, 'bar': { 'inherit': 'BAR' } } }) schd = scheduler(id_) async with start(schd): # initialise the data store await schd.update_data_structure() w_id = schd.data_store_mgr.workflow_id data = schd.data_store_mgr.data[w_id] schd.pool.hold_tasks({TaskTokens('*', 'root')}) await schd.update_data_structure() assert ( len({t.id for t in data[TASK_PROXIES].values() if t.is_held}) == 2 ) # Window size reduction to invoke pruning schd.data_store_mgr.set_graph_window_extent(0) schd.data_store_mgr.update_data_structure() assert ( len({t.id for t in data[TASK_PROXIES].values() if t.is_held}) == 1 ) # Test rapid addition and removal # bar/BAR task/family proxies not in .added assert len({ t.name for t in schd.data_store_mgr.added[TASK_PROXIES].values() if t.name == 'bar' }) == 0 assert len({ f.name for f in schd.data_store_mgr.added[FAMILY_PROXIES].values() if f.name == 'BAR' }) == 0 # Add bar/BAR on set output of foo for itask in schd.pool.get_tasks(): schd.pool.spawn_on_output(itask, TASK_OUTPUT_SUCCEEDED) # bar/BAR now found. assert len({ t.name for t in schd.data_store_mgr.added[TASK_PROXIES].values() if t.name == 'bar' }) == 1 assert len({ f.name for f in schd.data_store_mgr.added[FAMILY_PROXIES].values() if f.name == 'BAR' }) == 1 # Before updating the data-store, remove bar/BAR. schd.pool.remove(schd.pool._get_task_by_id('1/bar'), 'Test removal') schd.data_store_mgr.update_data_structure() # bar/BAR not found in data or added stores. assert len({ t.name for t in data[TASK_PROXIES].values() if t.name == 'bar' }) == 0 assert len({ t.name for t in schd.data_store_mgr.added[TASK_PROXIES].values() if t.name == 'bar' }) == 0 assert len({ f.name for f in data[FAMILY_PROXIES].values() if f.name == 'BAR' }) == 0 assert len({ f.name for f in schd.data_store_mgr.added[FAMILY_PROXIES].values() if f.name == 'BAR' }) == 0 async def test_family_ascent_point_prune(mod_harness): """Test _family_ascent_point_prune. This method tries to remove non-existent family.""" schd, data = mod_harness fp_id = 'NotAFamilyProxy' parent_ids = {fp_id} checked_ids = set() node_ids = set() schd.data_store_mgr._family_ascent_point_prune( next(iter(parent_ids)), node_ids, parent_ids, checked_ids, schd.data_store_mgr.family_pruned_ids ) assert len(checked_ids) == 1 assert len(parent_ids) == 0 def test_delta_task_prerequisite(mod_harness): """Test delta_task_prerequisites.""" schd: Scheduler schd, data = mod_harness schd.pool.set_prereqs_and_outputs( {itask.tokens for itask in schd.pool.get_tasks()}, [TASK_STATUS_SUCCEEDED], [], flow=[] ) assert all(p.satisfied for p in get_pb_prereqs(schd)) for itask in schd.pool.get_tasks(): # set prereqs as not-satisfied for prereq in itask.state.prerequisites: for key in prereq: prereq[key] = False schd.data_store_mgr.delta_task_prerequisite(itask) assert not any(p.satisfied for p in get_pb_prereqs(schd)) def test_delta_task_xtrigger(xharness): """Test delta_task_xtrigger.""" schd: Scheduler schd, _ = xharness foo = schd.pool._get_task_by_id('1/foo') bar = schd.pool._get_task_by_id('1/bar') assert not foo.state.xtriggers['x'] # not satisfied assert not foo.state.xtriggers['x2'] # not satisfied assert not foo.state.xtriggers['y'] # not satisfied assert not bar.state.xtriggers['x'] # not satisfied # satisfy foo's dependence on x schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, [], ['xtrigger/x:succeeded'], flow=[] ) # check the task pool assert foo.state.xtriggers['x'] # satisfied assert not foo.state.xtriggers['y'] # satisfied assert not bar.state.xtriggers['x'] # not satisfied # data store should have one updated task proxy with satisfied xtrigger x [pbfoo] = schd.data_store_mgr.updated[TASK_PROXIES].values() assert pbfoo.id.endswith('foo') xtrig = pbfoo.xtriggers['x=xrandom(0)'] assert xtrig.label == 'x' assert xtrig.satisfied # unsatisfy it again schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, [], ['xtrigger/x:unsatisfied'], flow=[] ) # check the task pool assert not foo.state.xtriggers['x'] # not satisfied # data store should have one updated task proxy with unsatisfied xtrigger x [pbfoo] = schd.data_store_mgr.updated[TASK_PROXIES].values() assert pbfoo.id.endswith('foo') xtrig = pbfoo.xtriggers['x=xrandom(0)'] assert xtrig.label == 'x' assert not xtrig.satisfied # satisfy both of foo's xtriggers at once schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, [], ['xtrigger/all:succeeded'], flow=[] ) # check the task pool assert foo.state.xtriggers['x'] # satisfied assert foo.state.xtriggers['y'] # satisfied # data store should have one updated task proxy with satisfied xtrigger x [pbfoo] = schd.data_store_mgr.updated[TASK_PROXIES].values() assert pbfoo.id.endswith('foo') xtrig_x = pbfoo.xtriggers['x=xrandom(0)'] assert xtrig_x.label == 'x' assert xtrig_x.satisfied # updated task proxy should also contain duplicate xtrigger labels xtrig_x2 = pbfoo.xtriggers['x2=xrandom(0)'] assert xtrig_x2.label == 'x2' assert xtrig_x2.satisfied xtrig_y = pbfoo.xtriggers['y=xrandom(0, _=1)'] assert xtrig_y.label == 'y' assert xtrig_y.satisfied async def test_absolute_graph_edges(flow, scheduler, start): """It should add absolute graph edges to the store. See: https://github.com/cylc/cylc-flow/issues/5845 """ runahead_cycles = 1 id_ = flow({ 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'runahead limit': f'P{runahead_cycles}', 'graph': { 'R1': 'build', 'P1': 'build[^] => run', }, }, }) schd = scheduler(id_) async with start(schd): await schd.update_data_structure() assert { (Tokens(edge.source).relative_id, Tokens(edge.target).relative_id) for edge in schd.data_store_mgr.data[schd.id][EDGES].values() } == { ('1/build', f'{cycle}/run') # +1 for Python's range() # +2 for Cylc's runahead for cycle in range(1, runahead_cycles + 3) } async def test_group_state_on_window_resize(flow, scheduler, start): """Test group state change on n-window resize. This method will expand and reduce the data-store to change the family/root states. See: https://github.com/cylc/cylc-flow/issues/7115 """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo:failed => bar' } }, 'runtime': { 'FOOBAR': {}, 'FOO': { 'inherit': 'FOOBAR' }, 'foo': { 'inherit': 'FOO', }, 'BAR': { 'inherit': 'FOOBAR' }, 'bar': { 'inherit': 'BAR' } } }) schd = scheduler(id_) async with start(schd): # initialise the data store await schd.update_data_structure() w_id = schd.data_store_mgr.workflow_id data = schd.data_store_mgr.data[w_id] schd.pool.hold_tasks({TaskTokens('1', 'bar')}) await schd.update_data_structure() # At startup foo/FOO/FOOBAR/root should be waiting assert { t.state for t in data[TASK_PROXIES].values() } == {TASK_STATUS_WAITING} assert { data[FAMILY_PROXIES][f_id].state for f_id in [ schd.data_store_mgr.id_.duplicate(cycle='1', task=t).id for t in ['FOO', 'FOOBAR', 'root'] ] } == {TASK_STATUS_WAITING} # Set foo to failed, spawn in bar, remove foo itask = schd.pool._get_task_by_id('1/foo') itask.state.reset(TASK_STATUS_FAILED) schd.pool.spawn_on_output(itask, TASK_OUTPUT_FAILED) schd.data_store_mgr.delta_task_state(itask) schd.pool.remove(itask, 'Test removal') schd.data_store_mgr.update_data_structure() # With both tasks there should be a mix failed and waiting assert { t.state for t in data[TASK_PROXIES].values() } == {TASK_STATUS_FAILED, TASK_STATUS_WAITING} assert [ data[FAMILY_PROXIES][f_id].state for f_id in [ schd.data_store_mgr.id_.duplicate(cycle='1', task=t).id for t in ['FOO', 'BAR', 'FOOBAR', 'root'] ] ] == [ TASK_STATUS_FAILED, TASK_STATUS_WAITING, TASK_STATUS_FAILED, TASK_STATUS_FAILED, ] # Window size reduction to remove failed state task schd.data_store_mgr.set_graph_window_extent(0) schd.data_store_mgr.update_data_structure() # Now bar/BAR/FOOBAR/root should be waiting assert { t.state for t in data[TASK_PROXIES].values() } == {TASK_STATUS_WAITING} assert { data[FAMILY_PROXIES][f_id].state for f_id in [ schd.data_store_mgr.id_.duplicate(cycle='1', task=t).id for t in ['BAR', 'FOOBAR', 'root'] ] } == {TASK_STATUS_WAITING} # Window size increase to add failed state task again schd.data_store_mgr.set_graph_window_extent(1) schd.data_store_mgr.update_data_structure() # Again there should be a mix failed and waiting states assert { t.state for t in data[TASK_PROXIES].values() } == {TASK_STATUS_FAILED, TASK_STATUS_WAITING} assert [ data[FAMILY_PROXIES][f_id].state for f_id in [ schd.data_store_mgr.id_.duplicate(cycle='1', task=t).id for t in ['FOO', 'BAR', 'FOOBAR', 'root'] ] ] == [ TASK_STATUS_FAILED, TASK_STATUS_WAITING, TASK_STATUS_FAILED, TASK_STATUS_FAILED, ] async def test_flow_numbers(flow, scheduler, start): """It should update flow numbers when a task is triggered. See https://github.com/cylc/cylc-flow/issues/6114 """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a => b' } } }) schd = scheduler(id_) async with start(schd): # initialise the data store await schd.update_data_structure() # the task should not have a flow number as it is n>0 ds_task = schd.data_store_mgr.get_data_elements(TASK_PROXIES).added[1] assert ds_task.name == 'b' assert ds_task.flow_nums == '[]' # force trigger the task in a new flow await run_cmd(force_trigger_tasks(schd, ['1/b'], ['2'])) # update the data store await schd.update_data_structure() # the task should now exist in the new flow ds_task = schd.data_store_mgr.get_data_elements(TASK_PROXIES).added[1] assert ds_task.name == 'b' assert ds_task.flow_nums == '[2]' async def test_delta_task_outputs(one: 'Scheduler', start): """Ensure task outputs are inserted into the store. Note: Task outputs should *not* be updated incrementally until we have a protocol for doing so, see https://github.com/cylc/cylc-flow/pull/6403 """ def get_data_outputs(): """Return satisfied outputs from the *data* store.""" return { output.label for output in one.data_store_mgr.data[one.id][TASK_PROXIES][ itask.tokens.id ].outputs.values() if output.satisfied } def get_delta_outputs(): """Return satisfied outputs from the *delta* store. Or return None if there's nothing there. """ try: return { output.label for output in one.data_store_mgr.updated[TASK_PROXIES][ itask.tokens.id ].outputs.values() if output.satisfied } except KeyError: return None def _patch_remove(*args, **kwargs): """Prevent the task/workflow from completing.""" pass async with start(one): one.pool.remove = _patch_remove # create a job submission itask = one.pool.get_tasks()[0] assert itask itask.submit_num += 1 one.data_store_mgr.insert_job( itask, itask.state.status, {'submit_num': 1} ) await one.update_data_structure() # satisfy the submitted & started outputs # (note started implies submitted) one.task_events_mgr.process_message( itask, 'INFO', TaskEventsManager.EVENT_STARTED ) # the delta should be populated with the newly satisfied outputs assert get_data_outputs() == set() assert get_delta_outputs() == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, } # the delta should be applied to the store await one.update_data_structure() assert get_data_outputs() == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, } assert get_delta_outputs() is None # satisfy the succeeded output one.task_events_mgr.process_message( itask, 'INFO', TaskEventsManager.EVENT_SUCCEEDED ) # the delta should be populated with ALL satisfied outputs # (not just the newly satisfied output) assert get_data_outputs() == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, } assert get_delta_outputs() == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, } # the delta should be applied to the store await one.update_data_structure() assert get_data_outputs() == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, } assert get_delta_outputs() is None async def test_remove_added_jobs_of_pruned_task(one: Scheduler, start): """When a task is pruned, any of its jobs added in the same batch must be removed from the batch. See https://github.com/cylc/cylc-flow/pull/6656 """ async with start(one): itask = one.pool.get_tasks()[0] itask.state_reset(TASK_STATUS_PREPARING) one.task_events_mgr.process_message(itask, INFO, TASK_OUTPUT_SUCCEEDED) assert not one.data_store_mgr.data[one.id][JOBS] assert len(one.data_store_mgr.added[JOBS]) == 1 one.data_store_mgr.update_data_structure() assert not one.data_store_mgr.data[one.id][JOBS] assert not one.data_store_mgr.added[JOBS] async def test_log_events(one: Scheduler, start): """It should record log events and strip and ANSI formatting.""" async with start(one): handler = ProtobufStreamHandler( one, level=logging.INFO, ) LOG.addHandler(handler) try: # log a message with some ANSIMARKUP formatting LOG.warning( 'here hare here' ) await one.update_data_structure() log_records = one.data_store_mgr.data[one.id][WORKFLOW].log_records assert len(log_records) == 1 log_record = log_records[0] # the message should be in the store, the ANSI formatting should be # stripped assert log_record.level == 'WARNING' assert log_record.message == 'here hare here' finally: LOG.removeHandler(handler) async def test_no_backwards_job_state_change(one: Scheduler, start): """It should not allow backwards job state changes.""" async with start(one): itask = one.pool.get_tasks()[0] itask.state_reset(TASK_STATUS_PREPARING) itask.submit_num += 1 await one.update_data_structure() one.task_events_mgr.process_message(itask, INFO, TASK_OUTPUT_STARTED) await one.update_data_structure() assert get_pb_job(one, itask).state == TASK_STATUS_RUNNING # Simulate late arrival of "submitted" message one.task_events_mgr.process_message(itask, INFO, TASK_OUTPUT_SUBMITTED) await one.update_data_structure() assert get_pb_job(one, itask).state == TASK_STATUS_RUNNING async def test_job_estimated_finish_time(one_conf, flow, scheduler, start): """It should set estimated_finish_time on job elements along with started_time.""" wid = flow({ **one_conf, 'scheduler': {'UTC mode': True}, 'runtime': { 'one': {'execution time limit': 'PT2M'}, }, }) schd: Scheduler = scheduler(wid) date = '2081-07-02T' async def start_job(itask: TaskProxy, start_time: str): if not schd.pool.get_task(itask.point, itask.tdef.name): schd.pool.add_to_pool(itask) await schd.update_data_structure() itask.state_reset(TASK_STATUS_PREPARING) itask.submit_num += 1 itask.jobs = [] schd.task_events_mgr.process_message( itask, INFO, TASK_OUTPUT_SUBMITTED # submit time irrelevant ) await schd.update_data_structure() schd.task_events_mgr.process_message( itask, INFO, TASK_OUTPUT_STARTED, f'{date}{start_time}' ) await schd.update_data_structure() async with start(schd): itask = schd.pool.get_tasks()[0] await start_job(itask, '06:00:00Z') # 1st job: estimate based on execution time limit: assert ( get_pb_job(schd, itask).estimated_finish_time == f'{date}06:02:00Z' ) # Finish this job and start a new one: schd.task_events_mgr.process_message( itask, INFO, TASK_OUTPUT_SUCCEEDED, f'{date}06:00:40Z' ) await start_job(itask, '06:01:00Z') # >=2nd job: estimate based on mean of previous jobs: assert ( get_pb_job(schd, itask).estimated_finish_time == f'{date}06:01:40Z' ) async def test__family_ascent_point_update(flow, scheduler, run, validate): """Check that task states are cascaded up the family tree to the root family and the workflow "containsRetry" flag. https://github.com/cylc/cylc-flow/issues/7174 """ wid = flow({ 'scheduling': { 'initial cycle point': '3333', 'graph': { 'R1': ( 'FAM' '\n@wallclock => is_wallclock' '\n@echo_false => is_xt' ) }, 'xtriggers': { 'wallclock': 'wall_clock()', 'echo_false': 'echo(succeed=False)' } }, 'runtime': { 'FAM': {}, 'is_retry': { 'inherit': 'FAM', 'execution retry delays': 'PT10M', 'simulation': { 'fail cycle points': 'all', 'default run length': 'PT0S' } }, 'normal': {'inherit': 'FAM'}, 'is_wallclock': {'inherit': 'FAM'}, 'is_xt': {'inherit': 'FAM'}, } }) is_flags = ['is_retry', 'is_wallclock', 'is_xtriggered'] validate(wid) schd = scheduler(wid, paused_start=False) async with run(schd): data = schd.data_store_mgr.data assert data[schd.id]['workflow'].contains_retry is False await schd._main_loop() assert data[schd.id]['workflow'].contains_retry is True for family_proxy in data[schd.id]['family_proxies'].values(): for attribute in is_flags: assert getattr(family_proxy, attribute) is True cylc-flow-8.6.4/tests/integration/__init__.py0000664000175000017500000000000015202510242021371 0ustar alastairalastaircylc-flow-8.6.4/tests/integration/test_mode_on_restart.py0000664000175000017500000000404215202510242024067 0ustar alastairalastair# 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 . """What happens to the mode on restart? """ import pytest from cylc.flow.exceptions import InputError from cylc.flow.scheduler import Scheduler MODES = [('live'), ('simulation'), ('dummy')] @pytest.mark.parametrize('mode_after', MODES) @pytest.mark.parametrize('mode_before', MODES + [None]) async def test_restart_mode( flow, run, scheduler, start, one_conf, mode_before, mode_after ): """Restarting a workflow in live mode leads to workflow in live mode. N.B - we need use run becuase the check in question only happens on start. """ schd: Scheduler id_ = flow(one_conf) schd = scheduler(id_, run_mode=mode_before) async with start(schd): if not mode_before: mode_before = 'live' assert schd.get_run_mode().value == mode_before schd = scheduler(id_, run_mode=mode_after) if ( mode_before == mode_after or not mode_before and mode_after != 'live' ): # Restarting in the same mode is fine. async with run(schd): assert schd.get_run_mode().value == mode_before else: # Restarting in a new mode is not: errormsg = f'^This.*{mode_before} mode: You.*{mode_after} mode.$' with pytest.raises(InputError, match=errormsg): async with run(schd): pass cylc-flow-8.6.4/tests/integration/test_subprocctx.py0000664000175000017500000000424515202510242023104 0ustar alastairalastair# 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 . """Tests involving the Cylc Subprocess Context Object """ from logging import DEBUG from textwrap import dedent async def test_log_xtrigger_stdout( flow, scheduler, run_dir, start, log_filter ): """Output from xtriggers should appear in the scheduler log: (As per the toy example in the Cylc Docs) """ # Setup a workflow: id_ = flow({ 'scheduler': {'allow implicit tasks': True}, 'scheduling': { 'graph': {'R1': '@myxtrigger => foo'}, 'xtriggers': {'myxtrigger': 'myxtrigger()'} } }) # Create an xtrigger: xt_lib = run_dir / id_ / 'lib/python/myxtrigger.py' xt_lib.parent.mkdir(parents=True, exist_ok=True) xt_lib.write_text(dedent(r""" from sys import stderr def myxtrigger(): print('Hello World') print('Hello Hades', file=stderr) return True, {} """)) schd = scheduler(id_) async with start(schd, level=DEBUG): # Set off check for x-trigger: task = schd.pool.get_tasks()[0] schd.xtrigger_mgr.call_xtriggers_async(task) # while not schd.xtrigger_mgr._get_xtrigs(task): while schd.proc_pool.is_not_done(): schd.proc_pool.process() # Assert that both stderr and out from the print statement # in our xtrigger appear in the log. for expected in ['Hello World', 'Hello Hades']: assert log_filter(DEBUG, expected) cylc-flow-8.6.4/tests/integration/test_remove.py0000664000175000017500000004334515202510242022211 0ustar alastairalastair# 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 . import logging import pytest from cylc.flow.commands import ( force_trigger_tasks, reload_workflow, remove_tasks, run_cmd, ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.id import TaskTokens from cylc.flow.scheduler import Scheduler from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_state import TASK_STATUS_FAILED @pytest.fixture async def cylc_show_prereqs(cylc_show): """Fixture that returns the prereq info from `cylc show` in an easy-to-use format.""" async def inner(schd: Scheduler, task: str): prerequisites = (await cylc_show(schd, task))[task]['prerequisites'] return [ ( p['satisfied'], {c['taskId']: c['satisfied'] for c in p['conditions']}, ) for p in prerequisites ] return inner @pytest.fixture def example_workflow(flow): return flow({ 'scheduling': { 'graph': { # Note: test both `&` and separate arrows for combining # dependencies 'R1': ''' a1 & a2 => b a3 => b ''', }, }, }) def get_data_store_flow_nums(schd: Scheduler, itask: TaskProxy): _, ds_tproxy = schd.data_store_mgr.store_node_fetcher(itask.tokens) if ds_tproxy: return ds_tproxy.flow_nums async def test_basic( example_workflow, scheduler, start, db_select ): """Test removing a task from all flows.""" schd: Scheduler = scheduler(example_workflow) async with start(schd): a1 = schd.pool._get_task_by_id('1/a1') a3 = schd.pool._get_task_by_id('1/a3') schd.pool.spawn_on_output(a1, TASK_OUTPUT_SUCCEEDED) schd.pool.spawn_on_output(a3, TASK_OUTPUT_SUCCEEDED) await schd.update_data_structure() assert a1 in schd.pool.get_tasks() for table in ('task_states', 'task_outputs'): assert db_select(schd, True, table, 'flow_nums', name='a1') == [ ('[1]',), ] assert db_select( schd, True, 'task_prerequisites', 'satisfied', prereq_name='a1' ) == [ ('satisfied naturally',), ] assert get_data_store_flow_nums(schd, a1) == '[1]' await run_cmd(remove_tasks(schd, ['1/a1'], [])) await schd.update_data_structure() assert a1 not in schd.pool.get_tasks() # removed from pool for table in ('task_states', 'task_outputs'): assert db_select(schd, True, table, 'flow_nums', name='a1') == [ ('[]',), # removed from all flows ] assert db_select( schd, True, 'task_prerequisites', 'satisfied', prereq_name='a1' ) == [ ('0',), # prereq is now unsatisfied ] assert get_data_store_flow_nums(schd, a1) == '[]' async def test_specific_flow( example_workflow, scheduler, start, db_select ): """Test removing a task from a specific flow.""" schd: Scheduler = scheduler(example_workflow) def select_prereqs(): return db_select( schd, True, 'task_prerequisites', 'flow_nums', 'satisfied', prereq_name='a1', ) async with start(schd): a1 = schd.pool._get_task_by_id('1/a1') await run_cmd(force_trigger_tasks(schd, ['1/a1'], ['1', '2'])) schd.pool.spawn_on_output(a1, TASK_OUTPUT_SUCCEEDED) await schd.update_data_structure() assert a1 in schd.pool.get_tasks() assert a1.flow_nums == {1, 2} for table in ('task_states', 'task_outputs'): assert sorted( db_select(schd, True, table, 'flow_nums', name='a1') ) == [ ('[1, 2]',), # triggered task ('[1]',), # original spawned task ] assert select_prereqs() == [ ('[1, 2]', 'satisfied naturally'), ] assert get_data_store_flow_nums(schd, a1) == '[1, 2]' await run_cmd(remove_tasks(schd, ['1/a1'], ['1'])) await schd.update_data_structure() assert a1 in schd.pool.get_tasks() # still in pool assert a1.flow_nums == {2} for table in ('task_states', 'task_outputs'): assert sorted( db_select(schd, True, table, 'flow_nums', name='a1') ) == [ ('[2]',), ('[]',), ] assert select_prereqs() == [ ('[1, 2]', '0'), ] assert get_data_store_flow_nums(schd, a1) == '[2]' async def test_unset_prereq(example_workflow, scheduler, start): """Test removing a task unsets any prerequisites it satisfied.""" schd: Scheduler = scheduler(example_workflow) async with start(schd): for task in ('a1', 'a2', 'a3'): schd.pool.spawn_on_output( schd.pool.get_task(IntegerPoint('1'), task), TASK_OUTPUT_SUCCEEDED, ) b = schd.pool.get_task(IntegerPoint('1'), 'b') assert b.prereqs_are_satisfied() await run_cmd(remove_tasks(schd, ['1/a1'], [])) assert not b.prereqs_are_satisfied() async def test_not_unset_prereq( example_workflow, scheduler, start, db_select ): """Test removing a task does not unset a force-satisfied prerequisite (one that was satisfied by `cylc set --pre`).""" schd: Scheduler = scheduler(example_workflow) async with start(schd): # This set prereq should not be unset by removing a1: schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'b')}, outputs=[], prereqs=['1/a1'], flow=[] ) # Whereas the prereq satisfied by this set output *should* be unset # by removing a2: schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a2')}, outputs=['succeeded'], prereqs=[], flow=[] ) await schd.update_data_structure() assert sorted( db_select( schd, True, 'task_prerequisites', 'prereq_name', 'satisfied' ) ) == [ ('a1', 'force satisfied'), ('a2', 'satisfied naturally'), ('a3', '0'), ] await run_cmd(remove_tasks(schd, ['1/a1', '1/a2'], [])) await schd.update_data_structure() assert sorted( db_select( schd, True, 'task_prerequisites', 'prereq_name', 'satisfied' ) ) == [ ('a1', 'force satisfied'), ('a2', '0'), ('a3', '0'), ] async def test_nothing_to_do( example_workflow, scheduler, start, log_filter ): """Test removing an invalid task.""" schd: Scheduler = scheduler(example_workflow) async with start(schd): await run_cmd(remove_tasks(schd, ['1/doh'], [])) assert log_filter(logging.WARNING, 'No tasks match "1/doh"') async def test_logging( flow, scheduler, start, log_filter, caplog: pytest.LogCaptureFixture ): """Test logging of a mixture of valid and invalid task removals.""" schd: Scheduler = scheduler( flow({ 'scheduler': { 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': '2000', 'graph': { 'R3//P1Y': 'b[-P1Y] => a & b', }, }, }) ) tasks_to_remove = [ # Active, removable tasks: '2000/*', # Future, non-removable tasks: '2001/a', '2001/b', # Glob that doesn't match any active tasks: '2002/*', # Invalid tasks: '2005/a', '2000/doh', ] async with start(schd): await run_cmd(remove_tasks(schd, tasks_to_remove, [])) assert log_filter( logging.INFO, "Removed tasks: 2000/a (flows=1), 2000/b (flows=1)" ) assert log_filter(logging.WARNING, "Task(s) not removable: 2001/a, 2001/b") assert log_filter(logging.WARNING, "Invalid cycle point for task: a, 2005") assert log_filter( logging.WARNING, "No tasks match the IDs:\n* 2000/doh\n* 2005/a" ) # No tasks were submitted/running so none should have been killed: assert "job killed" not in caplog.text async def test_logging_flow_nums( example_workflow, scheduler, start, log_filter ): """Test logging of task removals involving flow numbers.""" schd: Scheduler = scheduler(example_workflow) async with start(schd): await run_cmd(force_trigger_tasks(schd, ['1/a1'], ['1', '2'])) # Removing from flow that doesn't exist doesn't work: await run_cmd(remove_tasks(schd, ['1/a1'], ['3'])) assert log_filter( logging.WARNING, "Task(s) not removable: 1/a1 (flows=3)" ) # But if a valid flow is included, it will be removed from that flow: await run_cmd(remove_tasks(schd, ['1/a1'], ['2', '3'])) assert log_filter(logging.INFO, "Removed tasks: 1/a1 (flows=2)") assert schd.pool._get_task_by_id('1/a1').flow_nums == {1} async def test_retrigger(flow, scheduler, run, reflog, complete): """Test prereqs & re-run behaviour when removing tasks.""" schd: Scheduler = scheduler( flow('a => b => c'), paused_start=False, ) async with run(schd): reflog_triggers: set = reflog(schd) await complete(schd, '1/b') await run_cmd(remove_tasks(schd, ['1/a', '1/b'], [])) schd.process_workflow_db_queue() # Removing 1/b should un-queue 1/c: assert len(schd.pool.task_queue_mgr.queues['default'].deque) == 0 assert reflog_triggers == { ('1/a', None), ('1/b', ('1/a',)), } reflog_triggers.clear() await run_cmd(force_trigger_tasks(schd, ['1/a'], [])) await complete(schd) assert reflog_triggers == { ('1/a', None), # 1/b should have run again after 1/a on the re-trigger in flow 1: ('1/b', ('1/a',)), ('1/c', ('1/b',)), } async def test_prereqs( flow, scheduler, run, complete, cylc_show_prereqs, log_filter ): """Test prereqs & stall behaviour when removing tasks.""" schd: Scheduler = scheduler( flow('(a1 | a2) & b => x'), paused_start=False, ) async with run(schd): await complete(schd, '1/a1', '1/a2', '1/b') await run_cmd(remove_tasks(schd, ['1/a1'], [])) assert not schd.pool.is_stalled() assert len(schd.pool.task_queue_mgr.queues['default'].deque) # `cylc show` should reflect the now-unsatisfied condition: assert await cylc_show_prereqs(schd, '1/x') == [ (True, {'1/a1': False, '1/a2': True, '1/b': True}) ] await run_cmd(remove_tasks(schd, ['1/b'], [])) # Should cause stall now because 1/c prereq is unsatisfied: assert len(schd.pool.task_queue_mgr.queues['default'].deque) == 0 assert schd.pool.is_stalled() assert log_filter( logging.WARNING, "1/x is waiting on ['1/a1:succeeded', '1/b:succeeded']", ) assert await cylc_show_prereqs(schd, '1/x') == [ (False, {'1/a1': False, '1/a2': True, '1/b': False}) ] assert schd.pool._get_task_by_id('1/x') await run_cmd(remove_tasks(schd, ['1/a2'], [])) # Should cause 1/x to be removed from the pool as it no longer has # any satisfied prerequisite tasks: assert not schd.pool._get_task_by_id('1/x') assert log_filter( logging.INFO, regex=r"1/x.* removed .* prerequisite task\(s\) removed", ) async def test_downstream_preparing(flow, scheduler, start): """Downstream dependents should not be removed if they are already preparing.""" schd: Scheduler = scheduler( flow(''' a => x a => y '''), ) async with start(schd): a = schd.pool._get_task_by_id('1/a') schd.pool.spawn_on_output(a, TASK_OUTPUT_SUCCEEDED) assert schd.pool.get_task_ids() == {'1/a', '1/x', '1/y'} schd.pool._get_task_by_id('1/y').state_reset('preparing') await run_cmd(remove_tasks(schd, ['1/a'], [])) assert schd.pool.get_task_ids() == {'1/y'} async def test_downstream_other_flows(flow, scheduler, run, complete): """Downstream dependents should not be removed if they exist in other flows.""" schd: Scheduler = scheduler( flow(''' a => b => c => x a => x '''), paused_start=False, ) async with run(schd): await complete(schd, '1/a') await run_cmd(force_trigger_tasks(schd, ['1/c'], ['2'])) c = schd.pool._get_task_by_id('1/c') schd.pool.spawn_on_output(c, TASK_OUTPUT_SUCCEEDED) assert schd.pool._get_task_by_id('1/x').flow_nums == {1, 2} await run_cmd(remove_tasks(schd, ['1/c'], ['2'])) assert schd.pool.get_task_ids() == {'1/b', '1/x'} # Note: in future we might want to remove 1/x from flow 2 as well, to # maintain flow continuity. However it is tricky at the moment because # other prerequisite tasks could exist in flow 2 (we don't know as # prereqs do not hold flow info other than in the DB). assert schd.pool._get_task_by_id('1/x').flow_nums == {1, 2} async def test_suicide(flow, scheduler, run, reflog, complete): """Test that suicide prereqs are unset by `cylc remove`.""" schd: Scheduler = scheduler( flow(''' a => b => c => d => x a & c => !x '''), paused_start=False, ) async with run(schd): reflog_triggers: set = reflog(schd) await complete(schd, '1/b') await run_cmd(remove_tasks(schd, ['1/a'], [])) await complete(schd) assert reflog_triggers == { ('1/a', None), ('1/b', ('1/a',)), ('1/c', ('1/b',)), ('1/d', ('1/c',)), # 1/x not suicided as 1/a was removed: ('1/x', ('1/d',)), } async def test_kill_running(flow, scheduler, run, complete, reflog): """Test removing a running task should kill it. Note this only tests simulation mode and a separate test for live mode exists in tests/functional/cylc-remove. """ schd: Scheduler = scheduler( flow({ 'scheduling': { 'graph': { 'R1': ''' a:started => b => c a:failed => q ''' }, }, 'runtime': { 'a': { 'simulation': { 'default run length': 'PT30S' }, }, }, }), paused_start=False, ) async with run(schd): reflog_triggers = reflog(schd) await complete(schd, '1/b') a = schd.pool._get_task_by_id('1/a') await run_cmd(remove_tasks(schd, ['1/a'], [])) assert a.state(TASK_STATUS_FAILED, is_held=True) await complete(schd) assert reflog_triggers == { ('1/a', None), ('1/b', ('1/a',)), ('1/c', ('1/b',)), # The a:failed output should not cause 1/q to run } async def test_reload_changed_config(flow, scheduler, run, complete): """Test that a task is removed from the pool if its configuration changes to make it no longer match the graph.""" wid = flow({ 'scheduling': { 'graph': { 'R1': ''' a => b a:started => s & b ''', }, }, 'runtime': { 'a': { 'simulation': { # Ensure 1/a still in pool during reload 'fail cycle points': 'all', }, }, }, }) schd: Scheduler = scheduler(wid, paused_start=False) async with run(schd): await complete(schd, '1/s') # Change graph then reload flow('b', workflow_id=wid) await run_cmd(reload_workflow(schd)) assert schd.config.cfg['scheduling']['graph']['R1'] == 'b' assert schd.pool.get_task_ids() == {'1/a', '1/b'} await run_cmd(remove_tasks(schd, ['1/a'], [])) await complete(schd, '1/b') async def test_remove_triggered(flow, scheduler, start): """It should remove tasks from pool and from to_trigger set.""" conf = { 'scheduling': { 'graph': { 'R1': 'a & b' }, }, } schd: Scheduler = scheduler(flow(conf)) async with start(schd): foo, bar = schd.pool.get_tasks() # trigger foo (now) await run_cmd( force_trigger_tasks(schd, [foo.identity], []) ) assert foo in schd.pool.tasks_to_trigger_now # trigger bar await run_cmd( force_trigger_tasks(schd, [bar.identity], []) ) assert bar in schd.pool.tasks_to_trigger_now await run_cmd( remove_tasks(schd, [foo.identity, bar.identity], []) ) assert not schd.pool.get_tasks() assert not schd.pool.tasks_to_trigger_now cylc-flow-8.6.4/tests/integration/test_broadcast_mgr.py0000664000175000017500000002244315202510242023517 0ustar alastairalastair# 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 . """Tests for Broadcast Manager.""" import asyncio import pytest from cylc.flow import commands from cylc.flow.cycling.integer import IntegerInterval, IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Interval, ISO8601Point from cylc.flow.task_state import TASK_STATUS_FAILED async def test_reject_valid_broadcast_is_remote_clash_with_config( one_conf, flow, start, scheduler, log_filter ): """`put_broadcast` gracefully rejects invalid broadcast: Existing config = [task][remote]host = foo Broadcast = [task]platform = bar https://github.com/cylc/cylc-flow/issues/6693 """ one_conf.update({'runtime': {'root': {'platform': 'foo'}}}) wid = flow(one_conf) schd = scheduler(wid) async with start(schd): bc_mgr = schd.broadcast_mgr good, bad = bc_mgr.put_broadcast( point_strings=['1'], namespaces=['one'], settings=[{'remote': {'host': 'bar'}}] ) # the error should be reported in the workflow log assert log_filter(contains='Cannot apply broadcast') assert bc_mgr.broadcasts == {'1': {}} # the bad setting should be reported assert good == [] assert bad == { 'settings': [ {'remote': {'host': 'bar'}}, ] } async def test_reject_valid_broadcast_is_remote_clash_with_broadcast( one_conf, flow, start, scheduler, log_filter ): """`put_broadcast` gracefully rejects invalid broadcast: Existing Broadcast = [task][remote]host = foo New Broadcast = [task]platform = bar https://github.com/cylc/cylc-flow/pull/6711/files#r2033457964 """ schd = scheduler(flow(one_conf)) async with start(schd): bc_mgr = schd.broadcast_mgr _, bad = bc_mgr.put_broadcast( point_strings=['1'], namespaces=['one'], settings=[{'remote': {'host': 'bar'}}] ) assert bad == {} # broadcast should be successful # this should not be allowed, if it is the scheduler will crash # when unpaused: good, bad = bc_mgr.put_broadcast( point_strings=['1'], namespaces=['one'], settings=[{'platform': 'foo'}] ) # the error should be reported in the workflow log assert log_filter(contains='Cannot apply broadcast') assert bc_mgr.broadcasts == {'1': {'one': {'remote': {'host': 'bar'}}}} # the bad setting should be reported assert good == [] assert bad == { 'settings': [ {'platform': 'foo'}, ] } @pytest.mark.parametrize('cycling_mode', ('integer', 'gregorian', '360_day')) async def test_broadcast_expire_limit( cycling_mode, flow, scheduler, run, complete, capcall, ): """Test automatic broadcast expiry. To prevent broadcasts from piling up and causing a memory leak, we expire (aka clear) them. The broadcast expiry limit is the oldest active cycle MINUS the longest cycling sequence. See https://github.com/cylc/cylc-flow/pull/6964 """ # capture broadcast expiry calls _expires = capcall('cylc.flow.broadcast_mgr.BroadcastMgr.expire_broadcast') def expires(): """Return a list of the cycle limit expired since the last call.""" ret = [x[0][1] for x in _expires] _expires.clear() return ret def cycle(number): """Return a cycle point object in the relevant format.""" if cycling_mode == 'integer': return IntegerPoint(str(number)) else: return ISO8601Point(f'000{number}') def interval(number): """Return an integer object in the relevant format.""" if cycling_mode == 'integer': return IntegerInterval(sequence(number)) else: return ISO8601Interval(sequence(number)) def sequence(number): """Return a sequence string in the relevant format.""" if cycling_mode == 'integer': return f'P{number}' else: return f'P{number}Y' # a workflow with a sequential task id_ = flow({ 'scheduler': { 'cycle point format': 'CCYY' } if cycling_mode != 'integer' else {}, 'scheduling': { 'cycling mode': cycling_mode, 'initial cycle point': cycle(1), 'graph': { # the sequence with the sequential task sequence(1): f'a[-{sequence(1)}] => a', # a longer sequence to make the offset more interesting sequence(3): 'a', } } }) schd = scheduler(id_, paused_start=False) async with run(schd): # the longest cycling sequence has a step of "3" assert schd.config.interval_of_longest_sequence == interval(3) # no broadcast expires should happen on startup assert expires() == [] # when a cycle closes, auto broadcast expiry should happen # NOTE: datetimes cannot be negative, so this expiry will be skipped # for datetimetime cycling workflows await complete(schd, f'{cycle(1)}/a') assert expires() in ([], [cycle(-1)]) await complete(schd, f'{cycle(2)}/a') assert expires() == [cycle(0)] await complete(schd, f'{cycle(3)}/a') assert expires() == [cycle(1)] async def test_broadcast_expiry_async( one_conf, flow, scheduler, run, complete, capcall ): """Test auto broadcast expiry with async workflows. Auto broadcast expiry should not happen in async workflows as there is only one cycle so it doesn't make sense. See https://github.com/cylc/cylc-flow/pull/6964 """ # capture broadcast expiry calls expires = capcall('cylc.flow.broadcast_mgr.BroadcastMgr.expire_broadcast') id_ = flow(one_conf) schd = scheduler(id_, paused_start=False) async with run(schd): # this is an async workflow so the longest cycling interval is a # null interval assert ( schd.config.interval_of_longest_sequence == IntegerInterval.get_null() ) await complete(schd) # no auto-expiry should take place assert expires == [] async def test_broadcast_old_cycle(flow, scheduler, run, complete): """It should not expire broadcasts whilst the scheduler is paused. This tests the use case of broadcasting to a historical cycle (whilst the workflow is paused) before triggering it to run to ensure that the broadcast is not expired before the operator is able to run the trigger command. For context, see https://github.com/cylc/cylc-flow/pull/6499 and https://github.com/cylc/cylc-flow/pull/6192#issuecomment-2486785465 """ id_ = flow({ 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'graph': { 'P1': 'a[-P1] => a', }, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): # issue a broadcast into the first cycle schd.broadcast_mgr.put_broadcast( point_strings=['1'], namespaces=['a'], settings=[{'environment': {'ANSWER': '42'}}] ) assert list(schd.broadcast_mgr.broadcasts) == ['1'] # the broadcast should expire after the workflow passes cycle "3" await complete(schd, '3/a') assert list(schd.broadcast_mgr.broadcasts) == [] # pause the workflow await commands.run_cmd(commands.pause(schd)) # issue a broadcast into the first cycle (now behind the broadcast # expire point) schd.broadcast_mgr.put_broadcast( point_strings=['1'], namespaces=['a'], settings=[{'simulation': {'fail cycle points': '1'}}] ) # this should not be expired whilst the scheduler is paused await schd._main_loop() # run one iteration of the main loop assert list(schd.broadcast_mgr.broadcasts) == ['1'] # trigger the first cycle and resume the workflow await commands.run_cmd(commands.force_trigger_tasks(schd, ['1'], [])) await commands.run_cmd(commands.resume(schd)) # the broadcast should still be there await schd._main_loop() # run one iteration of the main loop assert list(schd.broadcast_mgr.broadcasts) == ['1'] # and should take effect a_1 = schd.pool._get_task_by_id('1/a') async with asyncio.timeout(5): while True: await asyncio.sleep(0.1) if a_1.state(TASK_STATUS_FAILED): break cylc-flow-8.6.4/tests/integration/test_scan_api.py0000664000175000017500000002505615202510242022470 0ustar alastairalastair# 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 . """Test the cylc scan Python API (which is equivalent to the CLI).""" import json from pathlib import Path from shutil import ( copytree, rmtree ) import pytest from cylc.flow.scripts.scan import ( main, ScanOptions ) from cylc.flow.workflow_files import ( ContactFileFields, WorkflowFiles, dump_contact_file, load_contact_file ) @pytest.fixture(scope='module') async def flows(mod_flow, mod_scheduler, mod_run, mod_one_conf): """Three workflows in different states. One stopped, one paused and one that thinks its running. TODO: Start one of the workflows with tasks in funny states in order to test the state totals functionality properly. Requires: https://github.com/cylc/cylc-flow/pull/3668 """ # a simple workflow we will leave stopped mod_flow(mod_one_conf, name='-stopped-') # a simply hierarchically registered workflow we will leave stopped mod_flow(mod_one_conf, name='a/b/c') # a simple workflow we will leave paused reg1 = mod_flow(mod_one_conf, name='-paused-') schd1 = mod_scheduler(reg1, paused_start=True) # a workflow with some metadata we will make look like it's running reg2 = mod_flow( { 'meta': { 'title': 'Foo', 'description': ''' Here we find a multi line description ''' }, 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'foo' } }, 'runtime': { 'foo': { 'simulation': {'default run length': 'PT10S'} } } }, name='-running-' ) schd2 = mod_scheduler(reg2, paused_start=False) # run cylc run async with mod_run(schd1): async with mod_run(schd2): yield async def test_state_filter(flows, mod_test_dir): """It should filter flows by state.""" # one stopped flow opts = ScanOptions(states='stopped', sort=True) lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 2 assert '-stopped-' in lines[0] assert 'a/b/c' in lines[1] # one paused flow opts = ScanOptions(states='paused') lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 1 assert '-paused-' in lines[0] # one running flow opts = ScanOptions(states='running') lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 1 assert '-running-' in lines[0] # two active flows opts = ScanOptions(states='paused,running') lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 2 # three registered flows opts = ScanOptions(states='paused,running,stopped') lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 4 async def test_name_filter(flows, mod_test_dir): """It should filter flows by name regex.""" # one stopped flow opts = ScanOptions(states='all', name=['.*paused.*']) lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 1 assert '-paused-' in lines[0] async def test_name_sort(flows, mod_test_dir): """It should sort flows by name.""" # one stopped flow opts = ScanOptions(states='all', sort=True) lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) assert len(lines) == 4 assert '-paused-' in lines[0] assert '-running-' in lines[1] assert '-stopped-' in lines[2] assert 'a/b/c' in lines[3] async def test_format_json(flows, mod_test_dir): """It should dump results in json format.""" # one stopped flow opts = ScanOptions(states='all', format='json') lines = [] await main(opts, write=lines.append, scan_dir=mod_test_dir) data = json.loads(lines[0]) assert len(data) == 4 assert data[0]['name'] async def test_format_tree(flows, run_dir, ses_test_dir, mod_test_dir): """It should dump results in an ascii tree format.""" # one stopped flow opts = ScanOptions(states='running', format='tree') workflows = [] await main(opts, write=workflows.append, scan_dir=mod_test_dir) assert len(workflows) == 1 lines = workflows[0].splitlines() # this flow is hierarchically registered in the run dir already # it should be registered as // assert ses_test_dir.name in lines[0] assert mod_test_dir.name in lines[1] assert '-running-' in lines[2] async def test_format_rich(flows, mod_test_dir): """It should print results in a long human-friendly format.""" # one stopped flow (--colour-blind) opts = ScanOptions(states='running', format='rich', colour_blind=True) workflows = [] await main(opts, write=workflows.append, scan_dir=mod_test_dir) assert len(workflows) == 1 lines = workflows[0].splitlines() # test that the multi-line description was output correctly # with trailing lines indented correctly desc_lines = [ 'Here we find a', 'multi', 'line', 'description' ] prev_ind = -1 prev_offset = -1 for expected in desc_lines: for ind, line in enumerate(lines): if expected in line: offset = line.index(expected) if prev_ind < 1: prev_ind = ind prev_offset = offset else: if ind != prev_ind + 1: raise Exception( f'Lines found in wrong order: {line}') if offset != prev_offset: raise Exception('Line incorrectly indented: {line}') break else: raise Exception(f'Missing line: {line}') # test that the state totals show one task running (colour_blind mode) for line in lines: if 'running:1' in line: break else: raise Exception('missing state totals line (colour_blind)') # one stopped flow (--colour=always) opts = ScanOptions(states='running', format='rich') workflows = [] await main( opts, write=workflows.append, scan_dir=mod_test_dir, color='always' ) assert len(workflows) == 1 lines = workflows[0].splitlines() # test that the state totals show one task running (colour mode) for line in lines: if '1 ■' in line: break else: raise Exception('missing state totals line (colourful)') async def test_scan_cleans_stuck_contact_files( start, scheduler, flow, one_conf, run_dir, test_dir, ): """Ensure scan tidies up contact files from crashed flows.""" # create a flow id_ = flow(one_conf, name='-crashed-') schd = scheduler(id_) srv_dir = Path(run_dir, id_, WorkflowFiles.Service.DIRNAME) tmp_dir = test_dir / 'srv' cont = srv_dir / WorkflowFiles.Service.CONTACT # run the flow, copy the contact, stop the flow, copy back the contact async with start(schd): copytree(srv_dir, tmp_dir) rmtree(srv_dir) copytree(tmp_dir, srv_dir) rmtree(tmp_dir) # the old contact file check uses the CLI command that the flow was run # with to check whether the flow is running. Because this is an # integration test the process is the pytest process and it is still # running so we need to change the command so that Cylc sees the flow as # having crashed contact_info = load_contact_file(id_) contact_info[ContactFileFields.COMMAND] += 'xyz' dump_contact_file(id_, contact_info) # make sure this flow shows for a regular filesystem-only scan opts = ScanOptions(states='running,paused', format='name') flows = [] await main(opts, write=flows.append, scan_dir=test_dir) assert len(flows) == 1 assert '-crashed-' in flows[0] # the contact file should still be there assert cont.exists() # make sure this flow shows for a regular filesystem-only scan opts = ScanOptions(states='running,paused', format='name', ping=True) flows = [] await main(opts, write=flows.append, scan_dir=test_dir) assert len(flows) == 0 # the contact file should have been removed by the scan assert not cont.exists() async def test_scan_fail_well_when_client_unreachable( start, scheduler, flow, one_conf, run_dir, test_dir, caplog, ): """It handles WorkflowRuntimeClient.async_request raising a WorkflowStopped elegently. """ # create a flow id_ = flow(one_conf, name='-crashed-') schd = scheduler(id_) srv_dir = Path(run_dir, id_, WorkflowFiles.Service.DIRNAME) tmp_dir = test_dir / 'srv' # run the flow, copy the contact, stop the flow, copy back the contact async with start(schd): copytree(srv_dir, tmp_dir) rmtree(srv_dir) copytree(tmp_dir, srv_dir) rmtree(tmp_dir) # the old contact file check uses the CLI command that the flow was run # with to check whether the flow is running. Because this is an # integration test the process is the pytest process and it is still # running so we need to change the command so that Cylc sees the flow as # having crashed contact_info = load_contact_file(id_) contact_info[ContactFileFields.COMMAND] += 'xyz' dump_contact_file(id_, contact_info) # Run Cylc Scan opts = ScanOptions(states='all', format='rich', ping=True) flows = [] await main(opts, write=flows.append, scan_dir=test_dir) # Check that the records contain a message but not an error rec = caplog.records[-1] assert not rec.exc_text assert 'Workflow not running' in rec.msg cylc-flow-8.6.4/tests/integration/test_config.py0000664000175000017500000005721415202510242022161 0ustar alastairalastair# 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 . import logging from pathlib import Path import sqlite3 from textwrap import dedent from typing import Any import pytest from cylc.flow import commands from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.cfgspec.globalcfg import GlobalConfig from cylc.flow.exceptions import ( InputError, PointParsingError, ServiceFileError, WorkflowConfigError, XtriggerConfigError, ) from cylc.flow.parsec.exceptions import ListValueError from cylc.flow.parsec.fileparse import read_and_proc from cylc.flow.pathutil import get_workflow_run_pub_db_path from cylc.flow.scheduler import Scheduler Fixture = Any param = pytest.param @pytest.mark.parametrize( 'task_name,valid', [ # valid task names ('a', True), ('a-b', True), ('a_b', True), ('foo', True), ('0aA-+%', True), # invalid task names ('a b', False), ('a£b', False), ('+ab', False), ('@ab', False), # not valid in [runtime] ('_cylc', False), ('_cylcy', False), ] ) def test_validate_task_name( flow, one_conf, validate, task_name: str, valid: bool ): """It should raise errors for invalid task names in the runtime section.""" id_ = flow({ **one_conf, 'runtime': { task_name: {} } }) if valid: validate(id_) else: with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert task_name in str(exc_ctx.value) @pytest.mark.parametrize( 'task_name', [ 'root', '_cylc', '_cylcy', ] ) def test_validate_implicit_task_name( flow, validate, task_name: str, ): """It should validate implicit task names in the graph. Note that most invalid task names get caught during graph parsing. Here we ensure that names which look like valid graph node names but which are blacklisted get caught and raise errors. """ id_ = flow({ 'scheduling': { 'graph': { 'R1': task_name } }, 'runtime': { # having one item in the runtime allows "root" to be expanded # which makes this test more thorough 'whatever': {} } }) with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert str(exc_ctx.value).splitlines()[0] == ( f'invalid task name "{task_name}"' ) @pytest.mark.parametrize( 'env_var,valid', [ ('foo', True), ('FOO', True), ('+foo', False), ] ) def test_validate_env_vars(flow, one_conf, validate, env_var, valid): """It should validate environment variable names.""" id_ = flow({ **one_conf, 'runtime': { 'foo': { 'environment': { env_var: 'value' } } } }) if valid: validate(id_) else: with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert env_var in str(exc_ctx.value) @pytest.mark.parametrize( 'env_val', [ '%(x)s', # valid template but no such parameter x '%(a)123', # invalid template ] ) def test_validate_param_env_templ( flow, one_conf, validate, env_val, log_filter, ): """It should validate parameter environment templates.""" id_ = flow({ **one_conf, 'runtime': { 'foo': { 'environment': { 'foo': env_val } } } }) validate(id_) assert log_filter(contains='bad parameter environment template') assert log_filter(contains=env_val) def test_no_graph(flow, validate): """It should fail for missing graph sections.""" id_ = flow({ 'scheduling': {}, }) with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert 'missing [scheduling][[graph]] section.' in str(exc_ctx.value) @pytest.mark.parametrize( 'section', [ 'external-trigger', 'clock-trigger', 'clock-expire', ] ) def test_parse_special_tasks_invalid(flow, validate, section): """It should fail for invalid "special tasks".""" id_ = flow({ 'scheduling': { 'initial cycle point': 'now', 'special tasks': { section: 'foo (', # missing closing bracket }, 'graph': { 'R1': 'foo', }, } }) with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert f'Illegal {section} spec' in str(exc_ctx.value) assert 'foo' in str(exc_ctx.value) def test_parse_special_tasks_interval(flow, validate): """It should fail for invalid durations in clock-triggers.""" id_ = flow({ 'scheduling': { 'initial cycle point': 'now', 'special tasks': { 'clock-trigger': 'foo(PT1Y)', # invalid ISO8601 duration }, 'graph': { 'R1': 'foo' } } }) with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert 'Illegal clock-trigger spec' in str(exc_ctx.value) assert 'PT1Y' in str(exc_ctx.value) @pytest.mark.parametrize( 'section', [ 'external-trigger', 'clock-trigger', 'clock-expire', ] ) def test_parse_special_tasks_families(flow, scheduler, validate, section): """It should expand families in special tasks.""" id_ = flow({ 'scheduling': { 'initial cycle point': 'now', 'special tasks': { section: 'FOO(P1D)', }, 'graph': { 'R1': 'foo & foot', } }, 'runtime': { # family 'FOO': {}, # nested family 'FOOT': { 'inherit': 'FOO', }, 'foo': { 'inherit': 'FOO', }, 'foot': { 'inherit': 'FOOT', }, } }) if section == 'external-trigger': # external triggers cannot be used for multiple tasks so if family # expansion is completed correctly, validation should fail with pytest.raises(WorkflowConfigError) as exc_ctx: config = validate(id_) assert 'external triggers must be used only once' in str(exc_ctx.value) else: config = validate(id_) assert set(config.cfg['scheduling']['special tasks'][section]) == { # the family FOO has been expanded to the tasks foo, foot 'foo(P1D)', 'foot(P1D)' } def test_queue_treated_as_implicit(flow, validate, caplog, log_filter): """Tasks in queues but not in runtime generate a warning. https://github.com/cylc/cylc-flow/issues/5260 """ id_ = flow( { "scheduling": { "queues": {"my_queue": {"members": "task1, task2"}}, "graph": {"R1": "task2"}, }, "runtime": {"task2": {}}, } ) validate(id_) assert log_filter(contains='Queues contain tasks not defined in runtime') def test_queue_treated_as_comma_separated(flow, validate): """Tasks listed in queue should be separated with commas, not spaces. https://github.com/cylc/cylc-flow/issues/5260 """ id_ = flow( { "scheduling": { "queues": {"my_queue": {"members": "task1 task2"}}, "graph": {"R1": "task2"}, }, "runtime": {"task1": {}, "task2": {}}, } ) with pytest.raises(ListValueError, match="cannot contain a space"): validate(id_) def test_validate_incompatible_db(one_conf, flow, validate): """Validation should fail for an incompatible DB due to not being able to load template vars.""" wid = flow(one_conf) # Create fake outdated DB db_file = Path(get_workflow_run_pub_db_path(wid)) db_file.parent.mkdir(parents=True, exist_ok=True) db_file.touch() conn = sqlite3.connect(db_file) try: conn.execute( 'CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key))' ) conn.commit() finally: conn.close() with pytest.raises( ServiceFileError, match="Workflow database is incompatible" ): validate(wid) # No tables should have been created stmt = "SELECT name FROM sqlite_master WHERE type='table'" conn = sqlite3.connect(db_file) try: tables = [i[0] for i in conn.execute(stmt)] finally: conn.close() assert tables == ['suite_params'] def test_xtrig_validation_wall_clock( flow: 'Fixture', validate: 'Fixture', ): """If an xtrigger module has a `validate()` function is called. https://github.com/cylc/cylc-flow/issues/5448 """ id_ = flow({ 'scheduling': { 'initial cycle point': '1012', 'xtriggers': {'myxt': 'wall_clock(offset=PT7MH)'}, 'graph': {'R1': '@myxt => foo'}, } }) with pytest.raises(WorkflowConfigError, match=( r'\[@myxt\] wall_clock\(offset=PT7MH\)\n' r'Invalid offset: PT7MH' )): validate(id_) def test_xtrig_implicit_wall_clock(flow: Fixture, validate: Fixture): """Test @wall_clock is allowed in graph without explicit xtrigger definition. """ wid = flow({ 'scheduling': { 'initial cycle point': '2024', 'graph': {'R1': '@wall_clock => foo'}, } }) validate(wid) def test_xtrig_validation_echo( flow: 'Fixture', validate: 'Fixture', ): """If an xtrigger module has a `validate()` function is called. https://github.com/cylc/cylc-flow/issues/5448 """ id_ = flow({ 'scheduling': { 'xtriggers': {'myxt': 'echo()'}, 'graph': {'R1': '@myxt => foo'}, } }) with pytest.raises( WorkflowConfigError, match=r'Requires \'succeed=True/False\' arg' ): validate(id_) def test_xtrig_validation_xrandom( flow: 'Fixture', validate: 'Fixture', ): """If an xtrigger module has a `validate()` function it is called. https://github.com/cylc/cylc-flow/issues/5448 """ id_ = flow({ 'scheduling': { 'xtriggers': {'myxt': 'xrandom(200)'}, 'graph': {'R1': '@myxt => foo'}, } }) with pytest.raises( XtriggerConfigError, match=r"'percent' should be a float between 0 and 100" ): validate(id_) def test_xtrig_validation_custom( flow: 'Fixture', validate: 'Fixture', monkeypatch: 'Fixture', ): """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: def kustom_xt(feature): return True, {} def kustom_validate(args): raise Exception('This is only a test.') # Patch xtrigger func & its validate func monkeypatch.setattr( 'cylc.flow.xtrigger_mgr.get_xtrig_func', lambda *args: kustom_validate if "validate" in args else kustom_xt ) id_ = flow({ 'scheduling': { 'initial cycle point': '1012', 'xtriggers': {'myxt': 'kustom_xt(feature=42)'}, 'graph': {'R1': '@myxt => foo'}, } }) Path(id_) with pytest.raises(XtriggerConfigError, match=r'This is only a test.'): validate(id_) @pytest.mark.parametrize('xtrig_call, expected_msg', [ pytest.param( 'xrandom()', r"missing a required argument: 'percent'", id="missing-arg" ), pytest.param( 'wall_clock(alan_grant=1)', r"unexpected keyword argument 'alan_grant'", id="unexpected-arg" ), ]) def test_xtrig_signature_validation( flow: 'Fixture', validate: 'Fixture', xtrig_call: str, expected_msg: str ): """Test automatic xtrigger function signature validation.""" id_ = flow({ 'scheduling': { 'initial cycle point': '2024', 'xtriggers': {'myxt': xtrig_call}, 'graph': {'R1': '@myxt => foo'}, } }) with pytest.raises(XtriggerConfigError, match=expected_msg): validate(id_) @pytest.mark.parametrize( 'left', ( param('@xrandom | @echo', id='xtrig-or-xtrig'), param('@xrandom | task', id='xtrig-or-task'), param('task | @echo', id='task-or-xtrig'), param('@xrandom | foo & bar', id='xtrig-or-complex'), param('@xrandom & bar | foo', id='complex-or-xtrig'), ) ) def test_xtrig_or_fails_validation( flow: "Fixture", validate: "Fixture", left: str ): """Xtriggers cannot be chained with the 'or' https://github.com/cylc/cylc-flow/issues/6771 https://github.com/cylc/cylc-flow/issues/2712 """ id_ = flow( { "scheduling": { "initial cycle point": "2024", "xtriggers": { "xrandom": "xrandom(100)", "echo": "echo(succeed=True)" }, "graph": {"R1": f"{left} => fin"}, } } ) expected_msg = ( "Xtriggers cannot be used in conditional graph expressions:\n") with pytest.raises(WorkflowConfigError, match=expected_msg): validate(id_) def test_special_task_non_word_names(flow: Fixture, validate: Fixture): """Test validation of special tasks names with non-word characters""" wid = flow({ 'scheduling': { 'initial cycle point': '2020', 'special tasks': { 'clock-trigger': 't-1, t+1, t%1, t@1', }, 'graph': { 'P1D': 't-1 & t+1 & t%1 & t@1', }, }, 'runtime': { 't-1, t+1, t%1, t@1': {'script': True}, }, }) validate(wid) async def test_glbl_cfg(monkeypatch, tmp_path, caplog): """Test accessing the global config via the glbl_cfg wrapper. Test the "cached" and "reload" kwargs to glbl_cfg. Also assert that accessing the global config during a reload operation does not cause issues. See https://github.com/cylc/cylc-flow/issues/6244 """ # wipe any previously cached config monkeypatch.setattr( 'cylc.flow.cfgspec.globalcfg.GlobalConfig._DEFAULT', None ) # load the global config from the test tmp directory monkeypatch.setenv('CYLC_CONF_PATH', str(tmp_path)) def write_global_config(cfg_str): """Write the global.cylc file.""" Path(tmp_path, 'global.cylc').write_text(cfg_str) def get_platforms(cfg_obj): """Return the platforms defined in the provided config instance.""" return set(cfg_obj.get(['platforms']).keys()) def expect_platforms_during_reload(platforms): """Test the platforms defined in glbl_cfg() during reload. Assert that the platforms defined in glbl_cfg() match the expected value, whilst the global config is in the process of being reloaded. In other words, this tests that the cached instance is not changed until after the reload has completed. See https://github.com/cylc/cylc-flow/issues/6244 """ caplog.set_level(logging.INFO) def _capture(fcn): def _inner(*args, **kwargs): cfg = glbl_cfg() assert get_platforms(cfg) == platforms logging.getLogger('test').info( 'ran expect_platforms_during_reload test' ) return fcn(*args, **kwargs) return _inner monkeypatch.setattr( 'cylc.flow.cfgspec.globalcfg.GlobalConfig._load', _capture(GlobalConfig._load) ) # write a global config write_global_config(''' [platforms] [[foo]] ''') # test the platforms defined in it assert get_platforms(glbl_cfg()) == {'localhost', 'foo'} # add a new platform the config write_global_config(''' [platforms] [[foo]] [[bar]] ''') # the new platform should not appear (due to the cached instance) assert get_platforms(glbl_cfg()) == {'localhost', 'foo'} # if we request an uncached instance, the new platform should appear assert get_platforms(glbl_cfg(cached=False)) == {'localhost', 'foo', 'bar'} # however, this should not affect the cached instance assert get_platforms(glbl_cfg()) == {'localhost', 'foo'} # * if we reload the cached instance, the new platform should appear # * but during the reload itself, the old config should persist # see https://github.com/cylc/cylc-flow/issues/6244 expect_platforms_during_reload({'localhost', 'foo'}) assert get_platforms(glbl_cfg(reload=True)) == {'localhost', 'foo', 'bar'} assert 'ran expect_platforms_during_reload test' in caplog.messages # the cache should have been updated by the reload assert get_platforms(glbl_cfg()) == {'localhost', 'foo', 'bar'} def test_nonlive_mode_validation(flow, validate, caplog, log_filter): """Nonlive tasks return a warning at validation. """ caplog.set_level(logging.INFO) msg1 = dedent( 'The following tasks are set to run in skip mode:\n * skip' ) wid = flow({ 'scheduling': { 'graph': { 'R1': 'live => skip => simulation => dummy => default' } }, 'runtime': { 'default': {}, 'live': {'run mode': 'live'}, 'skip': { 'run mode': 'skip', 'skip': {'outputs': 'started, submitted'} }, }, }) validate(wid) assert log_filter(contains=msg1) def test_skip_forbidden_as_output(flow, validate): """Run mode names are forbidden as task output names.""" wid = flow({ 'scheduling': {'graph': {'R1': 'task'}}, 'runtime': {'task': {'outputs': {'skip': 'message for skip'}}} }) with pytest.raises( WorkflowConfigError, match='Invalid task output .* cannot be: `skip`' ): validate(wid) def test_validate_workflow_run_mode( flow: Fixture, validate: Fixture, caplog: Fixture ): """Test that Cylc validate will only check simulation mode settings if validate --mode simulation or dummy. Discovered in: https://github.com/cylc/cylc-flow/pull/6213#issuecomment-2225365825 """ wid = flow( { 'scheduling': {'graph': {'R1': 'mytask'}}, 'runtime': { 'mytask': { 'simulation': {'fail cycle points': 'invalid'}, } }, } ) validate(wid) # It fails with run mode simulation: with pytest.raises(PointParsingError, match='Incompatible value'): validate(wid, run_mode='simulation') # It fails with run mode dummy: with pytest.raises(PointParsingError, match='Incompatible value'): validate(wid, run_mode='dummy') async def test_invalid_starttask(one_conf, flow, scheduler, start): """It should reject invalid starttask arguments.""" id_ = flow(one_conf) schd = scheduler(id_, starttask=['a///b']) with pytest.raises(InputError, match='a///b'): async with start(schd): pass async def test_CYLC_WORKFLOW_SRC_DIR_correctly_set(tmp_path, install, run_dir): """CYLC_WORKFLOW_SRC_DIR is set correctly: * In source dir * In installed dir (Not testing different permutations of installed dir as these are covered by testing of `get_workflow_source_dir`) * Created directly in the run dir. """ def process_file(target): """Run config through read_and_proc (~= cylc view --process) """ return read_and_proc( target, viewcfg={ 'mark': False, 'single': False, 'label': False, 'jinja2': True, 'contin': True, 'inline': True, }, ) # Setup a source directory: (tmp_path / 'flow.cylc').write_text( '#!jinja2\n{{ CYLC_WORKFLOW_SRC_DIR }}' ) # Check that the CYLC_SRC_DIRECTORY # points to the source directory (tmp_path): processed = process_file(tmp_path / 'flow.cylc') assert processed[0] == str(tmp_path) # After installation the CYLC_WORKFLOW_SRC_DIR # *still* points back to tmp_path: wid = await install(tmp_path) processed = process_file(run_dir / wid / 'flow.cylc') assert processed[0] == str(tmp_path) async def test_task_event_bad_custom_template( flow, validate, scheduler, start, log_filter ): """Validation fails if task event handler has a bad custom template. """ exception = ( r"bad task event handler template t1: echo %\(rubbish\)s:" r" KeyError\('rubbish'\)" ) events = { 'handlers': 'echo %(rubbish)s', 'handler events': 'succeeded' } wid = flow({ 'scheduling': {'graph': {'R1': 't1'}}, 'runtime': {'t1': {'events': events}}, }) with pytest.raises(WorkflowConfigError, match=exception): validate(wid) schd = scheduler(wid) with pytest.raises(WorkflowConfigError, match=exception): async with start(schd): pass async def test_icp_now_reload( flow, scheduler, start, monkeypatch: pytest.MonkeyPatch, log_filter ): """initial cycle point = 'now' should not change from original value on reload/restart, and sequences should remain intact. https://github.com/cylc/cylc-flow/issues/7047 """ def set_time(value): monkeypatch.setattr( 'cylc.flow.config.get_current_time_string', lambda *a, **k: f"2005-01-01T{value}Z", ) wid = flow({ 'scheduling': { 'initial cycle point': 'now', 'graph': { 'R1': 'cold => foo', 'PT15M': 'foo[-PT15M] => foo', }, }, }) schd: Scheduler = scheduler(wid) def main_check(icp): assert str(schd.config.initial_point) == icp assert schd.pool.get_task_ids() == { f'{icp}/cold', } assert {str(seq) for seq in schd.config.sequences} == { f'R1/{icp}/P0Y', f'R/{icp}/PT15M', } set_time('06:00') async with start(schd): expected_icp = '20050101T0600Z' main_check(expected_icp) set_time('06:03') await commands.run_cmd(commands.reload_workflow(schd)) main_check(expected_icp) await commands.run_cmd( commands.set_prereqs_and_outputs( schd, [f'{expected_icp}/cold'], [] ) ) # Downstream task should have spawned on sequence: assert schd.pool.get_task_ids() == { f'{expected_icp}/foo', } assert not log_filter(level=logging.WARNING) cylc-flow-8.6.4/tests/integration/test_task_pool.py0000664000175000017500000024013715202510242022705 0ustar alastairalastair# 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 . from json import loads import logging from typing import ( TYPE_CHECKING, AsyncGenerator, Callable, Iterable, List, Tuple, Union, cast, ) import pytest from pytest import param import re from cylc.flow import ( CYLC_LOG, commands, ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.data_messages_pb2 import PbPrerequisite from cylc.flow.data_store_mgr import TASK_PROXIES from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.flow_mgr import FLOW_NONE from cylc.flow.id import TaskTokens, Tokens from cylc.flow.run_modes import RunMode from cylc.flow.task_events_mgr import TaskEventsManager from cylc.flow.task_outputs import ( TASK_OUTPUT_FAILED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.task_pool import TaskPool from cylc.flow.task_state import ( TASK_STATUS_EXPIRED, TASK_STATUS_FAILED, TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, ) if TYPE_CHECKING: from cylc.flow.cycling import PointBase from cylc.flow.scheduler import Scheduler # NOTE: foo and bar have no parents so at start-up (even with the workflow # paused) they get spawned out to the runahead limit. 2/pub spawns # immediately too, because we spawn autospawn absolute-triggered tasks as # well as parentless tasks. 3/asd does not spawn at start, however. EXAMPLE_FLOW_CFG = { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': 1, 'final cycle point': 10, 'runahead limit': 'P3', 'graph': { 'P1': 'foo & bar', 'R1/2': 'foo[1] => pub', 'R1/3': 'foo[-P1] => asd' } }, 'runtime': { 'FAM': {}, 'bar': {'inherit': 'FAM'} } } EXAMPLE_FLOW_2_CFG = { 'scheduler': { 'UTC mode': True }, 'scheduling': { 'initial cycle point': '2001', 'runahead limit': 'P3Y', 'graph': { 'P1Y': 'foo', 'R/2025/P1Y': 'foo => bar', } }, } def get_task_ids( name_point_list: Iterable[Tuple[str, Union['PointBase', str, int]]] ) -> List[str]: """Helper function to return sorted task identities from a list of (name, point) tuples.""" return sorted(f'{point}/{name}' for name, point in name_point_list) def assert_expected_log( caplog_instance: pytest.LogCaptureFixture, expected_log_substrings: List[str] ) -> List[str]: """Helper function to check that expected (substrings of) log messages are actually in the log. Returns the list of actual logged messages. Args: caplog_instance: The instance of the caplog fixture for the particular test. expected_log_substrings: The expected, possibly partial, log messages. """ logged_messages = [i[2] for i in caplog_instance.record_tuples] assert len(logged_messages) == len(expected_log_substrings) for actual, expected in zip( sorted(logged_messages), sorted(expected_log_substrings)): assert expected in actual return logged_messages @pytest.fixture(scope='module') async def mod_example_flow( mod_flow: Callable, mod_scheduler: Callable, mod_run: Callable ) -> AsyncGenerator['Scheduler', None]: """Return a scheduler for interrogating its task pool. This is module-scoped so faster than example_flow, but should only be used where the test does not mutate the state of the scheduler or task pool. """ id_ = mod_flow(EXAMPLE_FLOW_CFG) schd: 'Scheduler' = mod_scheduler(id_, paused_start=True) async with mod_run(schd, level=logging.DEBUG): yield schd @pytest.fixture async def example_flow( flow: Callable, scheduler: Callable, start, caplog: pytest.LogCaptureFixture, ) -> AsyncGenerator['Scheduler', None]: """Return a scheduler for interrogating its task pool. This is function-scoped so slower than mod_example_flow; only use this when the test mutates the scheduler or task pool. """ # The run(schd) fixture doesn't work for modifying the DB, so have to # set up caplog and do schd.install()/.initialise()/.configure() instead caplog.set_level(logging.INFO, CYLC_LOG) id_ = flow(EXAMPLE_FLOW_CFG) schd: 'Scheduler' = scheduler(id_) async with start(schd, level=logging.DEBUG): yield schd @pytest.fixture(scope='module') async def mod_example_flow_2( mod_flow: Callable, mod_scheduler: Callable, mod_run: Callable ) -> AsyncGenerator['Scheduler', None]: """Return a scheduler for interrogating its task pool. This is module-scoped so faster than example_flow, but should only be used where the test does not mutate the state of the scheduler or task pool. """ id_ = mod_flow(EXAMPLE_FLOW_2_CFG) schd: 'Scheduler' = mod_scheduler(id_, paused_start=True) async with mod_run(schd): yield schd @pytest.mark.parametrize( 'ids, expected_tasks_to_hold_ids', [ param( ['1/foo', '3/asd'], ['1/foo', '3/asd'], id="Active & inactive tasks", ), param( ['1/FAM', '2/FAM', '6/FAM'], ['1/bar', '2/bar', '6/bar'], id="Family names hold active and future tasks", ), param( ['1/grogu', 'H/foo', '20/foo', '1/pub'], [], id="Non-existent task name or invalid cycle point", ), param( ['1/foo:waiting', '1/foo:failed', '6/bar:waiting'], ['1/foo'], id=( "Specifying task state works for active tasks," " not inactive tasks" ), ), ], ) async def test_hold_tasks( ids: List[str], expected_tasks_to_hold_ids: List[str], example_flow: 'Scheduler', caplog: pytest.LogCaptureFixture, db_select: Callable ) -> None: """Test TaskPool.hold_tasks(). Also tests TaskPool_explicit_match_tasks_to_hold() in the process; kills 2 birds with 1 stone. Params: items: Arg passed to hold_tasks(). expected_tasks_to_hold_ids: Expected IDs of the tasks that get put in the TaskPool.tasks_to_hold set, of the form "{point}/{name}"/ expected_warnings: Expected to be logged. """ expected_tasks_to_hold_ids = sorted(expected_tasks_to_hold_ids) caplog.set_level(logging.WARNING, CYLC_LOG) task_pool = example_flow.pool task_pool.hold_tasks( {cast('TaskTokens', Tokens(id_, relative=True)) for id_ in ids} ) for itask in task_pool.get_tasks(): hold_expected = itask.identity in expected_tasks_to_hold_ids assert itask.state.is_held is hold_expected assert get_task_ids(task_pool.tasks_to_hold) == expected_tasks_to_hold_ids db_held_tasks = db_select(example_flow, True, 'tasks_to_hold') assert get_task_ids(db_held_tasks) == expected_tasks_to_hold_ids async def test_release_held_tasks( example_flow: 'Scheduler', db_select: Callable ) -> None: """Test TaskPool.release_held_tasks(). For a workflow with held active tasks 1/foo & 1/bar, and held inactive task 3/asd. We skip testing the matching logic here because it would be slow using the function-scoped example_flow fixture, and it would repeat what is covered in test_hold_tasks(). """ # Setup task_pool = example_flow.pool expected_tasks_to_hold_ids = sorted(['1/foo', '1/bar', '3/asd']) task_pool.hold_tasks( { TaskTokens('1', 'foo'), TaskTokens('1', 'bar'), TaskTokens('3', 'asd'), } ) for itask in task_pool.get_tasks(): hold_expected = itask.identity in expected_tasks_to_hold_ids assert itask.state.is_held is hold_expected assert get_task_ids(task_pool.tasks_to_hold) == expected_tasks_to_hold_ids db_tasks_to_hold = db_select(example_flow, True, 'tasks_to_hold') assert get_task_ids(db_tasks_to_hold) == expected_tasks_to_hold_ids # Test task_pool.release_held_tasks( {TaskTokens('1', 'foo'), TaskTokens('3', 'asd')} ) for itask in task_pool.get_tasks(): assert itask.state.is_held is (itask.identity == '1/bar') expected_tasks_to_hold_ids = sorted(['1/bar']) assert get_task_ids(task_pool.tasks_to_hold) == expected_tasks_to_hold_ids db_tasks_to_hold = db_select(example_flow, True, 'tasks_to_hold') assert get_task_ids(db_tasks_to_hold) == expected_tasks_to_hold_ids @pytest.mark.parametrize( 'hold_after_point, expected_held_task_ids', [ ( '0', [ '1/foo', '1/bar', '2/foo', '2/bar', '2/pub', '3/foo', '3/bar', '4/foo', '4/bar', '5/foo', '5/bar', ], ), ( '1', [ '2/foo', '2/bar', '2/pub', '3/foo', '3/bar', '4/foo', '4/bar', '5/foo', '5/bar', ], ), ], ) async def test_hold_point( hold_after_point: str, expected_held_task_ids: List[str], example_flow: 'Scheduler', db_select: Callable ) -> None: """Test TaskPool.set_hold_point() and .release_hold_point()""" expected_held_task_ids = sorted(expected_held_task_ids) task_pool = example_flow.pool # Test hold task_pool.set_hold_point(IntegerPoint(hold_after_point)) assert ('holdcp', str(hold_after_point)) in db_select( example_flow, True, 'workflow_params') for itask in task_pool.get_tasks(): hold_expected = itask.identity in expected_held_task_ids assert itask.state.is_held is hold_expected assert get_task_ids(task_pool.tasks_to_hold) == expected_held_task_ids db_tasks_to_hold = db_select(example_flow, True, 'tasks_to_hold') assert get_task_ids(db_tasks_to_hold) == expected_held_task_ids # Test release task_pool.release_hold_point() assert db_select(example_flow, True, 'workflow_params', key='holdcp') == [ ('holdcp', None) ] for itask in task_pool.get_tasks(): assert itask.state.is_held is False assert task_pool.tasks_to_hold == set() assert db_select(example_flow, True, 'tasks_to_hold') == [] @pytest.mark.parametrize( 'status,should_trigger', [ (TASK_STATUS_WAITING, True), (TASK_STATUS_PREPARING, False), (TASK_STATUS_SUBMITTED, False), (TASK_STATUS_RUNNING, False), (TASK_STATUS_SUCCEEDED, True), ] ) async def test_trigger_states( status: str, should_trigger: bool, one: 'Scheduler', start: Callable ): """It should only trigger tasks in compatible states.""" async with start(one): itask = one.pool.get_task(IntegerPoint('1'), 'one') # reset task a to the provided state itask.state.reset(status) # try triggering the task await commands.run_cmd( commands.force_trigger_tasks(one, ['1/one'], [])) # retrieve the task again - the original may have been removed itask = one.pool.get_task(IntegerPoint('1'), 'one') # check whether the task triggered assert itask.is_manual_submit == should_trigger async def test_preparing_tasks_on_restart(one_conf, flow, scheduler, start): """Preparing tasks should be reset to waiting on restart. This forces preparation to be re-done on restart so that it uses the new configuration. See discussion on: https://github.com/cylc/cylc-flow/pull/4668 """ id_ = flow(one_conf) # start the workflow, reset a task to preparing one = scheduler(id_) async with start(one): itask = one.pool.get_tasks()[0] itask.state.reset(TASK_STATUS_PREPARING) # when we restart the task should have been reset to waiting one = scheduler(id_) async with start(one): itask = one.pool.get_tasks()[0] assert itask.state(TASK_STATUS_WAITING) itask.state.reset(TASK_STATUS_SUCCEEDED) # whereas if we reset the task to succeeded the state is not reset one = scheduler(id_) async with start(one): itask = one.pool.get_tasks()[0] assert itask.state(TASK_STATUS_SUCCEEDED) async def test_reload_stopcp( flow: Callable, scheduler: Callable, start: Callable ): """Test that the task pool stopping point does not revert to the final cycle point on reload.""" cfg = { 'scheduler': { 'allow implicit tasks': True, 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': 2010, 'stop after cycle point': 2020, 'final cycle point': 2030, 'graph': { 'P1Y': 'anakin' } } } schd: 'Scheduler' = scheduler(flow(cfg)) async with start(schd): assert str(schd.pool.stop_point) == '2020' await commands.run_cmd(commands.reload_workflow(schd)) assert str(schd.pool.stop_point) == '2020' async def test_runahead_after_remove( example_flow: 'Scheduler' ) -> None: """The runahead limit should be recomputed after tasks are removed. """ task_pool = example_flow.pool assert int(task_pool.runahead_limit_point) == 4 # No change after removing an intermediate cycle. await commands.run_cmd(commands.remove_tasks(example_flow, ['3/*'], ["1"])) assert int(task_pool.runahead_limit_point) == 4 # Should update after removing the first point. await commands.run_cmd(commands.remove_tasks(example_flow, ['1/*'], ["1"])) assert int(task_pool.runahead_limit_point) == 5 async def test_load_db_bad_platform( flow: Callable, scheduler: Callable, start: Callable, one_conf: Callable ): """Test that loading an unavailable platform from the database doesn't cause calamitous failure.""" schd: 'Scheduler' = scheduler(flow(one_conf)) async with start(schd): result = schd.pool.load_db_task_pool_for_restart(0, ( '1', 'one', '{"1": 1}', "0", False, False, "failed", False, 1, '', 'culdee-fell-summit', '', '', '', '{}' )) assert result == 'culdee-fell-summit' def list_tasks(schd): """Return a sorted list of task pool tasks. Returns a list in the format: [ (cycle, task, state) ] """ return sorted( (itask.tokens['cycle'], itask.tokens['task'], itask.state.status) for itask in schd.pool.get_tasks() ) @pytest.mark.parametrize( 'graph_1, graph_2, ' 'expected_1, expected_2, expected_3, expected_4', [ param( # Restart after adding a prerequisite to task z '''a => z b => z''', '''a => z b => z c => z''', [ ('1', 'a', 'running'), ('1', 'b', 'running'), ], [ ('1', 'b', 'running'), ('1', 'z', 'waiting'), ], [ ('1', 'b', 'running'), ('1', 'z', 'waiting'), ], [ {('1', 'a', 'succeeded'): 'satisfied naturally'}, {('1', 'b', 'succeeded'): False}, {('1', 'c', 'succeeded'): False}, ], id='added' ), param( # Restart after removing a prerequisite from task z '''a => z b => z c => z''', '''a => z b => z''', [ ('1', 'a', 'running'), ('1', 'b', 'running'), ('1', 'c', 'running'), ], [ ('1', 'b', 'running'), ('1', 'c', 'running'), ('1', 'z', 'waiting'), ], [ ('1', 'b', 'running'), ('1', 'c', 'running'), ('1', 'z', 'waiting'), ], [ {('1', 'a', 'succeeded'): 'satisfied naturally'}, {('1', 'b', 'succeeded'): False}, ], id='removed' ) ] ) async def test_restart_prereqs( flow, scheduler, start, graph_1, graph_2, expected_1, expected_2, expected_3, expected_4 ): """It should handle graph prerequisites change on restart. Prerequisite changes must be applied to tasks already in the pool. See https://github.com/cylc/cylc-flow/pull/5334 """ conf = { 'scheduler': {'allow implicit tasks': 'True'}, 'scheduling': { 'graph': { 'R1': graph_1 } } } id_ = flow(conf) schd: Scheduler = scheduler(id_, paused_start=False) async with start(schd): # Release tasks 1/a and 1/b schd.pool.release_runahead_tasks() schd.release_tasks_to_run() assert list_tasks(schd) == expected_1 # Mark 1/a as succeeded and spawn 1/z task_a = schd.pool.get_tasks()[0] schd.pool.task_events_mgr.process_message(task_a, 1, 'succeeded') assert list_tasks(schd) == expected_2 # Save our progress schd.workflow_db_mgr.put_task_pool(schd.pool) # Edit the workflow to add a new dependency on "z" conf['scheduling']['graph']['R1'] = graph_2 id_ = flow(conf, workflow_id=id_) # Restart it schd = scheduler(id_, run_mode='simulation', paused_start=False) async with start(schd): # Load jobs from db schd.workflow_db_mgr.pri_dao.select_jobs_for_restart( schd.data_store_mgr.insert_db_job ) assert list_tasks(schd) == expected_3 # To cover some code for loading prereqs from the DB at restart: schd.data_store_mgr.update_data_structure() # Check resulting dependencies of task z task_z = [ t for t in schd.pool.get_tasks() if t.tdef.name == "z" ][0] assert sorted( ( p._satisfied for p in task_z.state.prerequisites ), key=lambda d: tuple(d.keys())[0], ) == expected_4 @pytest.mark.parametrize( 'graph_1, graph_2, ' 'expected_1, expected_2, expected_3, expected_4', [ param( # Reload after adding a prerequisite to task z '''a => z b => z''', '''a => z b => z c => z''', [ ('1', 'a', 'running'), ('1', 'b', 'running'), ], [ ('1', 'b', 'running'), ('1', 'z', 'waiting'), ], [ ('1', 'b', 'running'), ('1', 'z', 'waiting'), ], [ {('1', 'a', 'succeeded'): 'satisfied naturally'}, {('1', 'b', 'succeeded'): False}, {('1', 'c', 'succeeded'): False}, ], id='added' ), param( # Reload after removing a prerequisite from task z '''a => z b => z c => z''', '''a => z b => z''', [ ('1', 'a', 'running'), ('1', 'b', 'running'), ('1', 'c', 'running'), ], [ ('1', 'b', 'running'), ('1', 'c', 'running'), ('1', 'z', 'waiting'), ], [ ('1', 'b', 'running'), ('1', 'c', 'running'), ('1', 'z', 'waiting'), ], [ {('1', 'a', 'succeeded'): 'satisfied naturally'}, {('1', 'b', 'succeeded'): False}, ], id='removed' ) ] ) async def test_reload_prereqs( flow, scheduler, start, graph_1, graph_2, expected_1, expected_2, expected_3, expected_4 ): """It should handle graph prerequisites change on reload. Prerequisite changes must be applied to tasks already in the pool. See https://github.com/cylc/cylc-flow/pull/5334 """ conf = { 'scheduler': {'allow implicit tasks': 'True'}, 'scheduling': { 'graph': { 'R1': graph_1 } } } id_ = flow(conf) schd: Scheduler = scheduler(id_, paused_start=False) async with start(schd): # Release tasks 1/a and 1/b schd.pool.release_runahead_tasks() schd.release_tasks_to_run() assert list_tasks(schd) == expected_1 # Mark 1/a as succeeded and spawn 1/z task_a = schd.pool.get_tasks()[0] schd.pool.task_events_mgr.process_message(task_a, 1, 'succeeded') assert list_tasks(schd) == expected_2 # Modify flow.cylc to add a new dependency on "z" conf['scheduling']['graph']['R1'] = graph_2 flow(conf, workflow_id=id_) # Reload the workflow config await commands.run_cmd(commands.reload_workflow(schd)) assert list_tasks(schd) == expected_3 # Check resulting dependencies of task z task_z = [ t for t in schd.pool.get_tasks() if t.tdef.name == "z" ][0] assert sorted( ( p._satisfied for p in task_z.state.prerequisites ), key=lambda d: tuple(d.keys())[0], ) == expected_4 async def _test_restart_prereqs_sat(): schd: Scheduler # YIELD: the workflow has now started... schd = yield await schd.update_data_structure() # Release tasks 1/a and 1/b schd.pool.release_runahead_tasks() schd.release_tasks_to_run() assert list_tasks(schd) == [ ('1', 'a', 'running'), ('1', 'b', 'running') ] # Mark both as succeeded and spawn 1/c for itask in schd.pool.get_tasks(): schd.pool.task_events_mgr.process_message(itask, 1, 'succeeded') schd.workflow_db_mgr.put_update_task_outputs(itask) schd.pool.remove_if_complete(itask) schd.workflow_db_mgr.process_queued_ops() assert list_tasks(schd) == [ ('1', 'c', 'waiting') ] # YIELD: the workflow has now restarted or reloaded with the new config... schd = yield await schd.update_data_structure() assert list_tasks(schd) == [ ('1', 'c', 'waiting') ] # Check resulting dependencies of task z task_c = schd.pool.get_tasks()[0] assert sorted( (*key, satisfied) for prereq in task_c.state.prerequisites for key, satisfied in prereq.items() ) == [ ('1', 'a', 'succeeded', 'satisfied naturally'), ('1', 'b', 'succeeded', 'satisfied from database') ] # The prereqs in the data store should have been updated too # await schd.update_data_structure() tasks = ( schd.data_store_mgr.data[schd.data_store_mgr.workflow_id][TASK_PROXIES] ) task_c_prereqs: List[PbPrerequisite] = tasks[ schd.data_store_mgr.id_.duplicate(cycle='1', task='c').id ].prerequisites assert sorted( (condition.task_proxy, condition.satisfied, condition.message) for prereq in task_c_prereqs for condition in prereq.conditions ) == [ ('1/a', True, 'satisfied naturally'), ('1/b', True, 'satisfied from database'), ] # and we're done, yield back control and return yield @pytest.mark.parametrize('do_restart', [True, False]) async def test_graph_change_prereq_satisfaction( flow, scheduler, start, do_restart ): """It should handle graph prerequisites change on reload/restart. If the graph is changed to add a dependency which has been previously satisfied, then Cylc should perform a DB check and mark the prerequsite as satisfied accordingly. See https://github.com/cylc/cylc-flow/pull/5334 """ conf = { 'scheduler': {'allow implicit tasks': 'True'}, 'scheduling': { 'graph': { 'R1': ''' a => c b ''' } } } id_ = flow(conf) schd = scheduler(id_, run_mode='simulation', paused_start=False) test = _test_restart_prereqs_sat() await test.asend(None) if do_restart: async with start(schd): # start the workflow and run part 1 of the tests await test.asend(schd) # shutdown and change the workflow definiton conf['scheduling']['graph']['R1'] += '\nb => c' flow(conf, workflow_id=id_) schd = scheduler(id_, run_mode='simulation', paused_start=False) async with start(schd): # restart the workflow and run part 2 of the tests await test.asend(schd) else: async with start(schd): await test.asend(schd) # Modify flow.cylc to add a new dependency on "b" conf['scheduling']['graph']['R1'] += '\nb => c' flow(conf, workflow_id=id_) # Reload the workflow config await commands.run_cmd(commands.reload_workflow(schd)) await test.asend(schd) async def test_runahead_limit_for_sequence_before_start_cycle( flow, scheduler, start, ): """It should obey the runahead limit. Ensure the runahead limit is computed correctly for sequences before the start cycle See https://github.com/cylc/cylc-flow/issues/5603 """ id_ = flow({ 'scheduler': {'allow implicit tasks': 'True'}, 'scheduling': { 'initial cycle point': '2000', 'runahead limit': 'P2Y', 'graph': { 'R1/2000': 'a', 'P1Y': 'b[-P1Y] => b', }, } }) schd = scheduler(id_, startcp='2005') async with start(schd): assert str(schd.pool.runahead_limit_point) == '20070101T0000Z' def list_pool_from_db(schd): """Returns the task pool table as a sorted list.""" db_task_pool = [] schd.workflow_db_mgr.pri_dao.select_task_pool( lambda _, row: db_task_pool.append(row) ) return sorted(db_task_pool) async def test_db_update_on_removal( flow, scheduler, start, ): """It should updated the task_pool table when tasks complete. There was a bug where the task_pool table was only being updated when tasks in the pool were updated. This meant that if a task was removed the DB would not reflect this change and would hold a record of the task in the wrong state. This test ensures that the DB is updated when a task is removed from the pool. See: https://github.com/cylc/cylc-flow/issues/5598 """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'true', }, 'scheduling': { 'graph': { 'R1': 'a', }, }, }) schd = scheduler(id_) async with start(schd): task_a = schd.pool.get_tasks()[0] # set the task to running schd.pool.task_events_mgr.process_message(task_a, 1, 'started') # update the db await schd.update_data_structure() schd.workflow_db_mgr.process_queued_ops() # the task should appear in the DB assert list_pool_from_db(schd) == [ ['1', 'a', 'running', 0], ] # mark the task as succeeded and allow it to be removed from the pool schd.pool.task_events_mgr.process_message(task_a, 1, 'succeeded') schd.pool.remove_if_complete(task_a) # update the DB, note no new tasks have been added to the pool await schd.update_data_structure() schd.workflow_db_mgr.process_queued_ops() # the task should be gone from the DB assert list_pool_from_db(schd) == [] async def test_no_flow_tasks_dont_spawn( flow, scheduler, start, ): """Ensure no-flow tasks don't spawn downstreams. No-flow tasks (i.e `--flow=none`) are not attached to any "flow". See https://github.com/cylc/cylc-flow/issues/5613 """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a => b => c' } }, }) schd: Scheduler = scheduler(id_) async with start(schd): task_a = schd.pool.get_tasks()[0] # set as no-flow: task_a.flow_nums = set() # Set as completed: should not spawn children. schd.pool.set_prereqs_and_outputs( {task_a.tokens}, [], [], [FLOW_NONE] ) assert not schd.pool.get_tasks() for flow_nums, expected_pool in ( # outputs yielded from a no-flow task should not spawn downstreams (set(), []), # outputs yielded from a task with flow numbers should spawn # downstreams in the same flow ({1}, [('1/b', {1})]), ): # set the flow-nums on 1/a task_a.flow_nums = flow_nums # spawn on the succeeded output schd.pool.spawn_on_output(task_a, TASK_OUTPUT_SUCCEEDED) schd.pool.spawn_on_all_outputs(task_a) # ensure the pool is as expected assert [ (itask.identity, itask.flow_nums) for itask in schd.pool.get_tasks() ] == expected_pool async def test_task_proxy_remove_from_queues( flow, one_conf, scheduler, start, ): """TaskPool.remove should delete task proxies from queues. See https://github.com/cylc/cylc-flow/pull/5573 """ # Set up a scheduler with a non-default queue: one_conf['scheduling'] = { 'queues': {'queue_two': {'members': 'one, control'}}, 'graph': {'R1': 'two & one & control'}, } schd = scheduler(flow(one_conf)) async with start(schd): # Get a list of itasks: itasks = schd.pool.get_tasks() for itask in itasks: id_ = itask.identity # The meat of the test - remove itask from pool if it # doesn't have "control" in the name: if 'control' not in id_: schd.pool.remove(itask) # Look at the queues afterwards: queues_after = { name: [itask.identity for itask in queue.deque] for name, queue in schd.pool.task_queue_mgr.queues.items()} assert queues_after['queue_two'] == ['1/control'] async def test_runahead_offset_start( mod_example_flow_2: 'Scheduler' ) -> None: """Late-start recurrences should not break the runahead limit at start-up. See GitHub #5708 """ task_pool = mod_example_flow_2.pool assert task_pool.runahead_limit_point == ISO8601Point('2004') async def test_detect_incomplete_tasks( flow, scheduler, start, log_filter, ): """Finished but incomplete tasks should be retained as incomplete.""" incomplete_final_task_states = { # final task states that would leave a task with # completion=succeeded incomplete TASK_STATUS_FAILED: TaskEventsManager.EVENT_FAILED, TASK_STATUS_EXPIRED: TaskEventsManager.EVENT_EXPIRED, TASK_STATUS_SUBMIT_FAILED: TaskEventsManager.EVENT_SUBMIT_FAILED } id_ = flow({ 'scheduling': { 'graph': { # a workflow with one task for each of the final task states 'R1': '\n'.join(incomplete_final_task_states.keys()) } } }) schd = scheduler(id_) async with start(schd, level=logging.DEBUG): itasks = schd.pool.get_tasks() for itask in itasks: itask.state_reset(is_queued=False) # spawn the output corresponding to the task schd.pool.task_events_mgr.process_message( itask, 1, incomplete_final_task_states[itask.tdef.name] ) # ensure that it is correctly identified as incomplete assert not itask.state.outputs.is_complete() assert log_filter( contains=( f"[{itask}] did not complete the required outputs:" ), ) # the task should not have been removed assert itask in schd.pool.get_tasks() async def test_trigger_icp_fcp_syntax( flow, scheduler, start, log_filter, ): """It should support the ^/$ syntax for referencing the initial/final cp. See https://github.com/cylc/cylc-flow/issues/6537 """ cfg = { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': 1, 'final cycle point': 2, 'graph': { 'R1/^': 'start', 'R1/$': 'end', }, }, } # trigger tasks at both the initial and final cycle points id_ = flow(cfg) schd = scheduler(id_) async with start(schd) as log: await commands.run_cmd( commands.force_trigger_tasks(schd, ['^/start', '$/end'], ['1']) ) assert log_filter(contains='[1/start:waiting(queued)] => waiting') assert log_filter(contains='[2/end:waiting(queued)] => waiting') log.clear() # clear the final cycle point del cfg['scheduling']['final cycle point'] del cfg['scheduling']['graph']['R1/$'] # try triggering a task at the (non existent) final cycle point id_ = flow(cfg) schd = scheduler(id_) async with start(schd): await commands.run_cmd( commands.force_trigger_tasks(schd, ['^/start', '$/end'], ['1']) ) assert log_filter(contains='[1/start:waiting(queued)] => waiting') assert not log_filter(contains='[2/end:waiting(queued)] => waiting') assert log_filter( contains='ID references final cycle point, but none is set: $/end' ) async def test_future_trigger_final_point( flow, scheduler, start, log_filter, ): """Check spawning of future-triggered tasks: foo[+P1] => bar. Don't spawn if a prerequisite reaches beyond the final cycle point. """ id_ = flow( { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': 1, 'final cycle point': 1, 'graph': { 'P1': "foo\n foo[+P1] & bar => baz" } } } ) schd = scheduler(id_) async with start(schd): for itask in schd.pool.get_tasks(): schd.pool.spawn_on_output(itask, "succeeded") assert log_filter( regex=( ".*1/baz.*not spawned: a prerequisite is beyond" r" the workflow stop point \(1\)" ) ) async def test_set_failed_complete( flow, scheduler, start, one_conf, log_filter, db_select: Callable ): """Test manual completion of an incomplete failed task.""" id_ = flow(one_conf) schd: Scheduler = scheduler(id_) async with start(schd, level=logging.DEBUG): one = schd.pool.get_tasks()[0] one.state_reset(is_queued=False) schd.pool.task_events_mgr.process_message(one, 1, "failed") assert log_filter( regex="1/one.* setting implied output: submitted") assert log_filter( regex="1/one.* setting implied output: started") assert log_filter( regex="failed.* did not complete the required outputs") # Set failed task complete via default "set" args. schd.pool.set_prereqs_and_outputs({one.tokens}, [], [], []) assert log_filter( contains=f'[{one}] removed from the n=0 window: completed') db_outputs = db_select( schd, True, 'task_outputs', 'outputs', **{'name': 'one'} ) assert ( sorted(loads((db_outputs[0])[0])) == [ "failed", "started", "submitted", "succeeded" ] ) async def test_set_prereqs( flow, scheduler, start, log_filter, ): """Check manual setting of prerequisites (task and xtrigger). """ id_ = flow( { 'scheduling': { 'initial cycle point': '2040', 'xtriggers': { 'x': 'xrandom(0)' }, 'graph': { 'R1': """ foo & bar & baz => qux", @x => bar """ } }, 'runtime': { 'foo': { 'outputs': { 'a': 'drugs and money', } } } } ) schd: Scheduler = scheduler(id_) async with start(schd): # it should start up with foo, bar, baz assert schd.pool.get_task_ids() == { "20400101T0000Z/bar", "20400101T0000Z/baz", "20400101T0000Z/foo", } # try to set an invalid prereq of qux schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'qux')}, [], ["20400101T0000Z/foo:a", "xtrigger/x"], [], ) assert log_filter( contains=( '20400101T0000Z/qux does not depend on "20400101T0000Z/foo:a"' ) ) assert log_filter( contains=( '20400101T0000Z/qux does not depend on xtrigger "x"' ) ) # it should not add 20400101T0000Z/qux to the pool assert schd.pool.get_task_ids() == { "20400101T0000Z/bar", "20400101T0000Z/baz", "20400101T0000Z/foo", } # set an xtrigger (see also test_xtrigger_mgr, and test_data_store_mgr) bar = schd.pool._get_task_by_id('20400101T0000Z/bar') assert bar.state.prerequisites_all_satisfied() assert not bar.state.xtriggers_all_satisfied() schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'bar')}, [], ["xtrigger/x:succeeded"], [], ) assert bar.state.xtriggers_all_satisfied() assert log_filter( contains=('prerequisite force-satisfied: x = xrandom(0)')) # set xtrigger in the wrong task schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'baz')}, [], ["xtrigger/x:succeeded"], [], ) assert log_filter( contains='20400101T0000Z/baz does not depend on xtrigger "x"') # set one prereq of inactive task 20400101T0000Z/qux schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'qux')}, [], ["20400101T0000Z/foo:succeeded"], []) # it should add 20400101T0000Z/qux to the pool assert schd.pool.get_task_ids() == { "20400101T0000Z/bar", "20400101T0000Z/baz", "20400101T0000Z/foo", "20400101T0000Z/qux", } # get the 20400101T0000Z/qux task proxy qux = schd.pool.get_task(ISO8601Point("20400101T0000Z"), "qux") assert not qux.state.prerequisites_all_satisfied() # set its other prereqs (test implicit "succeeded" and "succeed") # and truncated cycle point schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'qux')}, [], ["2040/bar", "2040/baz:succeed"], [], ) assert log_filter( contains=('prerequisite force-satisfied: 20400101T0000Z/bar')) assert log_filter( contains=('prerequisite force-satisfied: 20400101T0000Z/baz')) # it should now be fully satisfied assert qux.state.prerequisites_all_satisfied() # set one again schd.pool.set_prereqs_and_outputs( {TaskTokens('20400101T0000Z', 'qux')}, [], ["2040/bar"], []) assert log_filter( contains=('prerequisite already satisfied: 20400101T0000Z/bar')) async def test_set_bad_prereqs( flow, scheduler, start, log_filter, ): """Check manual setting of prerequisites. """ id_ = flow({ 'scheduler': { 'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': '2040', 'graph': {'R1': "foo => bar"}}, }) schd: Scheduler = scheduler(id_) def set_prereqs(prereqs): """Shorthand so only variable under test given as arg""" schd.pool.set_prereqs_and_outputs( {TaskTokens('2040', 'bar')}, [], prereqs, []) async with start(schd): # Invalid: task name wildcard: set_prereqs(["2040/*"]) assert log_filter(contains='Invalid prerequisite task name') # Invalid: cycle point wildcard. set_prereqs(["*/foo"]) assert log_filter(contains='Invalid prerequisite cycle point') async def test_set_outputs_live( flow, scheduler, start, log_filter, ): """Check manual set outputs in an active (spawned) task. """ id_ = flow( { 'scheduling': { 'graph': { 'R1': """ foo:x => bar foo => baz foo:y """ } }, 'runtime': { 'foo': { 'outputs': { 'x': 'xylophone', 'y': 'yacht' } } } } ) schd: Scheduler = scheduler(id_) async with start(schd): # it should start up with just 1/foo assert schd.pool.get_task_ids() == {"1/foo"} # fake failed foo = schd.pool.get_task(IntegerPoint("1"), "foo") foo.state_reset(is_queued=False) schd.pool.task_events_mgr.process_message(foo, 1, 'failed') # set foo:x: it should spawn bar but not baz schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, ["x"], [], [] ) assert schd.pool.get_task_ids() == {"1/bar", "1/foo"} # Foo should have been removed from the queue: assert '1/foo' not in [ i.identity for i in schd.pool.task_queue_mgr.queues['default'].deque ] # set foo:succeed: it should spawn baz but foo remains incomplete. schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, ["succeeded"], [], [] ) assert schd.pool.get_task_ids() == {"1/bar", "1/baz", "1/foo"} # it should complete implied outputs (submitted, started) too assert log_filter(contains="setting implied output: submitted") assert log_filter(contains="setting implied output: started") # set foo (default: all required outputs) to complete y. schd.pool.set_prereqs_and_outputs({TaskTokens('1', 'foo')}, [], [], []) assert log_filter(contains="output 1/foo:succeeded completed") assert schd.pool.get_task_ids() == {"1/bar", "1/baz"} async def test_set_outputs_live2( flow, scheduler, start, log_filter, ): """Assert that optional outputs are satisfied before completion outputs to prevent incomplete task warnings. """ id_ = flow( { 'scheduling': {'graph': { 'R1': """ foo:a => apple foo:b => boat """}}, 'runtime': {'foo': {'outputs': { 'a': 'xylophone', 'b': 'yacht'}}} } ) schd: Scheduler = scheduler(id_) async with start(schd): schd.pool.set_prereqs_and_outputs({TaskTokens('1', 'foo')}, [], [], []) assert not log_filter( contains="did not complete required outputs: ['a', 'b']" ) async def test_set_outputs_future( flow, scheduler, start, log_filter, ): """Check manual setting of inactive task outputs. """ id_ = flow( { 'scheduling': { 'graph': { 'R1': "a:x & a:y => b => c" } }, 'runtime': { 'a': { 'outputs': { 'x': 'xylophone', 'y': 'yacht' } } } } ) schd: Scheduler = scheduler(id_) async with start(schd): # it should start up with just 1/a assert schd.pool.get_task_ids() == {"1/a"} # setting inactive task b succeeded should spawn c but not b schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'b')}, ["succeeded"], [], []) assert schd.pool.get_task_ids() == {"1/a", "1/c"} schd.pool.set_prereqs_and_outputs( items={TaskTokens('1', 'a')}, outputs=["x", "y", "cheese"], prereqs=[], flow=[] ) assert log_filter(contains="Output 1/a:cheese not found") assert log_filter(contains="completed output x") assert log_filter(contains="completed output y") async def test_set_outputs_from_skip_settings( flow, scheduler, start, log_filter, validate ): """Check working of ``cylc set --out=skip``: 1. --out=skip can be used to set all required outputs. 2. --out=skip,other_output can be used to set other outputs. """ id_ = flow( { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': 1, 'final cycle point': 2, 'graph': { 'P1': """ a => after_asucceeded a:x => after_ax a:y? => after_ay """ } }, 'runtime': { 'a': { 'outputs': { 'x': 'xebec', 'y': 'yacht' }, 'skip': {'outputs': 'x'} } } } ) validate(id_) schd: Scheduler = scheduler(id_) async with start(schd): # it should start up with just tasks a: assert schd.pool.get_task_ids() == {'1/a', '2/a'} # setting 1/a output to skip should set output x, but not # y (because y is optional). schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, ['skip'], [], []) assert schd.pool.get_task_ids() == { '1/after_asucceeded', '1/after_ax', '2/a', } # Check that the presence of "skip" in outputs doesn't # trigger a warning: assert not log_filter(level=30) # You should be able to set skip as part of a list of outputs: schd.pool.set_prereqs_and_outputs( {TaskTokens('2', 'a')}, ['skip', 'y'], [], []) assert schd.pool.get_task_ids() == { '1/after_asucceeded', '1/after_ax', '2/after_asucceeded', '2/after_ax', '2/after_ay', } async def test_prereq_satisfaction( flow, scheduler, start, log_filter, ): """Check manual setting of task prerequisites. """ id_ = flow( { 'scheduling': { 'graph': { 'R1': "a:x & a:y => b" } }, 'runtime': { 'a': { 'outputs': { 'x': 'xylophone', 'y': 'yacht' } } } } ) schd: Scheduler = scheduler(id_) async with start(schd): # it should start up with just 1/a assert schd.pool.get_task_ids() == {"1/a"} # spawn b schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, ["x"], [], [] ) assert schd.pool.get_task_ids() == {"1/a", "1/b"} b = schd.pool.get_task(IntegerPoint("1"), "b") assert not b.prereqs_are_satisfied() # set valid and invalid prerequisites, by label and message. schd.pool.set_prereqs_and_outputs( prereqs=["1/a:xylophone", "1/a:y", "1/a:w", "1/a:z"], items={TaskTokens('1', 'b')}, outputs=[], flow=[], ) assert log_filter(contains="1/a:z not found") assert log_filter(contains="1/a:w not found") # FIXME: testing that something is *not* logged is extremely fragile: assert not log_filter(regex='.*does not depend on.*') assert b.prereqs_are_satisfied() @pytest.mark.parametrize('compat_mode', ['compat-mode', 'normal-mode']) @pytest.mark.parametrize('cycling_mode', ['integer', 'datetime']) @pytest.mark.parametrize('runahead_format', ['P3Y', 'P3']) async def test_compute_runahead( cycling_mode, compat_mode, runahead_format, flow, scheduler, start, monkeypatch, ): """Test the calculation of the runahead limit. This test ensures that: * Runahead tasks are excluded from computations see https://github.com/cylc/cylc-flow/issues/5825 * Tasks are initiated with the correct is_runahead status on startup. * Behaviour in compat/regular modes is same unless failed tasks are present * Behaviour is the same for integer/datetime cycling modes. """ if cycling_mode == 'integer': config = { 'scheduler': { 'allow implicit tasks': 'True', }, 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'runahead limit': 'P3', 'graph': { 'P1': 'a' }, } } point = lambda point: IntegerPoint(str(int(point))) else: config = { 'scheduler': { 'allow implicit tasks': 'True', 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': '0001', 'runahead limit': runahead_format, 'graph': { 'P1Y': 'a' }, } } point = ISO8601Point monkeypatch.setattr( 'cylc.flow.flags.cylc7_back_compat', compat_mode == 'compat-mode', ) id_ = flow(config) schd = scheduler(id_) async with start(schd): schd.pool.compute_runahead(force=True) assert int(str(schd.pool.runahead_limit_point)) == 4 # ensure task states are initiated with is_runahead status assert schd.pool.get_task(point('0001'), 'a').state(is_runahead=False) assert schd.pool.get_task(point('0005'), 'a').state(is_runahead=True) # mark the first three cycles as running for cycle in range(1, 4): schd.pool.get_task(point(f'{cycle:04}'), 'a').state.reset( TASK_STATUS_RUNNING ) schd.pool.compute_runahead(force=True) assert int(str(schd.pool.runahead_limit_point)) == 4 # no change # In Cylc 8 all incomplete tasks hold back runahead. # In Cylc 7, submit-failed tasks hold back runahead.. schd.pool.get_task(point('0001'), 'a').state.reset( TASK_STATUS_SUBMIT_FAILED ) schd.pool.compute_runahead(force=True) assert int(str(schd.pool.runahead_limit_point)) == 4 # ... but failed ones don't. Go figure. schd.pool.get_task(point('0001'), 'a').state.reset( TASK_STATUS_FAILED ) schd.pool.compute_runahead(force=True) if compat_mode == 'compat-mode': assert int(str(schd.pool.runahead_limit_point)) == 5 else: assert int(str(schd.pool.runahead_limit_point)) == 4 # no change # mark cycle 1 as complete # (via task message so the task gets removed before runahead compute) schd.task_events_mgr.process_message( schd.pool.get_task(point('0001'), 'a'), logging.INFO, TASK_OUTPUT_SUCCEEDED ) schd.pool.compute_runahead(force=True) assert int(str(schd.pool.runahead_limit_point)) == 5 # +1 async def test_compute_runahead_with_no_tasks(flow, scheduler, run): """It should handle the case of an empty workflow. See https://github.com/cylc/cylc-flow/issues/6225 """ id_ = flow( { 'scheduling': { 'initial cycle point': '2000', 'graph': {'R1': 'foo'}, }, } ) schd = scheduler(id_, startcp='2002', paused_start=False) async with run(schd): assert schd.pool.compute_runahead() is False assert schd.pool.runahead_limit_point is None assert schd.pool.get_tasks() == [] async def test_compute_runahead_with_no_sequences( flow, scheduler, start, run, complete ): """It should handle no sequences within the start-stop cycle range. See https://github.com/cylc/cylc-flow/issues/6154 """ cfg = { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': '1', 'graph': { 'P1': 'foo[-P1] => foo', }, }, } id_ = flow(cfg) schd = scheduler(id_, paused_start=False) async with run(schd): await complete(schd, '2/foo') cfg['scheduling']['graph']['R1'] = cfg['scheduling']['graph']['P1'] cfg['scheduling']['graph'].pop('P1') flow(cfg, workflow_id=id_) schd = scheduler(id_, paused_start=False) async with start(schd): schd.pool.compute_runahead() assert schd.pool.runahead_limit_point == IntegerPoint('3') @pytest.mark.parametrize('rhlimit', ['P2D', 'P2']) @pytest.mark.parametrize('compat_mode', ['compat-mode', 'normal-mode']) async def test_runahead_future_trigger( flow, scheduler, start, monkeypatch, rhlimit, compat_mode, ): """Equivalent time interval and cycle count runahead limits should yield the same limit point, even if there is a future trigger. See https://github.com/cylc/cylc-flow/pull/5893 """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'True', 'cycle point format': 'CCYYMMDD', }, 'scheduling': { 'initial cycle point': '2001', 'runahead limit': rhlimit, 'graph': { 'P1D': ''' a a[+P1D] => b ''', }, } }) monkeypatch.setattr( 'cylc.flow.flags.cylc7_back_compat', compat_mode == 'compat-mode', ) schd = scheduler(id_,) async with start(schd, level=logging.DEBUG): assert str(schd.pool.runahead_limit_point) == '20010103' schd.pool.release_runahead_tasks() for itask in schd.pool.get_tasks(): schd.pool.spawn_on_output(itask, 'succeeded') # future trigger raises the limit by one cycle point assert str(schd.pool.runahead_limit_point) == '20010104' @pytest.fixture(scope='module') async def mod_blah( mod_flow: Callable, mod_scheduler: Callable, mod_run: Callable ) -> 'Scheduler': """Return a scheduler for interrogating its task pool. This is module-scoped so faster than example_flow, but should only be used where the test does not mutate the state of the scheduler or task pool. """ config = { 'scheduler': { 'allow implicit tasks': 'True', 'cycle point format': '%Y', }, 'scheduling': { 'initial cycle point': '0001', 'runahead limit': 'P1Y', 'graph': { 'P1Y': 'a' }, } } id_ = mod_flow(config) schd: 'Scheduler' = mod_scheduler(id_, paused_start=True) async with mod_run(schd): yield schd @pytest.mark.parametrize( 'status, expected', [ # (Status, Are we expecting an update?) (TASK_STATUS_WAITING, False), (TASK_STATUS_EXPIRED, False), (TASK_STATUS_PREPARING, False), (TASK_STATUS_SUBMIT_FAILED, False), (TASK_STATUS_SUBMITTED, False), (TASK_STATUS_RUNNING, False), (TASK_STATUS_FAILED, True), (TASK_STATUS_SUCCEEDED, True) ] ) async def test_runahead_c7_compat_task_state( status, expected, mod_blah, monkeypatch, ): """For each task status check whether changing the oldest task to that status will cause compute_runahead to make a change. Compat mode: Cylc 7 ignored failed tasks but not submit-failed! """ def max_cycle(tasks): return max([int(t.tokens.get("cycle")) for t in tasks]) monkeypatch.setattr( 'cylc.flow.flags.cylc7_back_compat', True) monkeypatch.setattr( 'cylc.flow.task_events_mgr.TaskEventsManager._insert_task_job', lambda *_: True) mod_blah.pool.compute_runahead() before_pt = max_cycle(mod_blah.pool.get_tasks()) before = mod_blah.pool.runahead_limit_point itask = mod_blah.pool.get_task(ISO8601Point(f'{before_pt - 2:04}'), 'a') itask.state_reset(status, is_queued=False) mod_blah.pool.compute_runahead() after = mod_blah.pool.runahead_limit_point assert bool(before != after) == expected async def test_fast_respawn( example_flow: 'Scheduler', caplog: pytest.LogCaptureFixture, ) -> None: """Immediate re-spawn of removed tasks is not allowed. An immediate DB update is required to stop the respawn. https://github.com/cylc/cylc-flow/pull/6067 """ task_pool = example_flow.pool # find task 1/foo in the pool foo = task_pool.get_task(IntegerPoint("1"), "foo") # remove it from the pool task_pool.remove(foo) assert foo not in task_pool.get_tasks() # attempt to spawn it again itask = task_pool.spawn_task("foo", IntegerPoint("1"), {1}) assert itask is None assert "Not respawning 1/foo - task was removed" in caplog.text async def test_remove_active_task( example_flow: 'Scheduler', log_filter: Callable, ) -> None: """Test warning on removing an active task.""" task_pool = example_flow.pool # find task 1/foo in the pool foo = task_pool.get_task(IntegerPoint("1"), "foo") foo.state_reset(TASK_STATUS_RUNNING) task_pool.remove(foo, "request") assert foo not in task_pool.get_tasks() assert log_filter( regex=( "1/foo.*removed from the n=0 window:" " request - active job orphaned" ), level=logging.WARNING ) async def test_remove_by_expire_trigger( flow, validate, scheduler, start, log_filter ): """Test task removal by suicide trigger. * Suicide triggers should remove tasks from the pool. * It should be possible to bring them back by manually triggering them. * Removing a task manually (cylc remove) should work the same. """ def _get_id(b_completion: str = "succeeded"): return flow({ 'scheduler': { 'experimental': { 'expire triggers': 'True', } }, 'scheduling': { 'graph': { 'R1': ''' a? => b a:failed? => !b ''' }, }, 'runtime': { 'b': { 'completion': b_completion } } }) with pytest.raises( WorkflowConfigError, match=re.escape( "This may be due to use of an expire (formerly suicide) trigger" ) ): validate(_get_id()) id_ = _get_id("succeeded or expired") validate(id_) schd: 'Scheduler' = scheduler(id_, paused_start=False) async with start(schd, level=logging.DEBUG) as log: # it should start up with 1/a assert schd.pool.get_task_ids() == {"1/a"} a = schd.pool.get_task(IntegerPoint("1"), "a") # mark 1/a as failed and check that 1/b expires schd.pool.spawn_on_output(a, TASK_OUTPUT_FAILED) assert log_filter(regex="1/b.*=> expired") assert schd.pool.get_task_ids() == {"1/a"} # 1/b should not be resurrected if it becomes ready schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'b')}, [], ["1/a"], ["1"], ) assert log_filter(regex="1/b:expired.* already finished and completed") # but we can still resurrect 1/b by triggering it log.clear() await commands.run_cmd( commands.force_trigger_tasks(schd, ['1/b'], ['1'])) assert log_filter(regex='1/b.*added to the n=0 window') # remove 1/b with "cylc remove"" await commands.run_cmd( commands.remove_tasks(schd, ['1/b'], []) ) assert log_filter( regex='1/b.*removed from the n=0 window: request', ) # and bring 1/b back again by triggering it again log.clear() await commands.run_cmd( commands.force_trigger_tasks(schd, ['1/b'], ['1'])) assert log_filter(regex='1/b.*added to the n=0 window',) async def test_remove_by_suicide( flow, scheduler, start, log_filter ): """Test task removal by suicide trigger. * Suicide triggers should remove tasks from the pool. * It should be possible to bring them back by manually triggering them. * Removing a task manually (cylc remove) should work the same. """ id_ = flow({ 'scheduling': { 'graph': { 'R1': ''' a? & b a:failed? => !b ''' }, } }) schd: 'Scheduler' = scheduler(id_) async with start(schd, level=logging.DEBUG) as log: # it should start up with 1/a and 1/b assert schd.pool.get_task_ids() == {"1/a", "1/b"} a = schd.pool.get_task(IntegerPoint("1"), "a") # mark 1/a as failed and ensure 1/b is removed by suicide trigger schd.pool.spawn_on_output(a, TASK_OUTPUT_FAILED) assert log_filter( regex="1/b.*removed from the n=0 window: suicide trigger" ) assert schd.pool.get_task_ids() == {"1/a"} # ensure that we are able to bring 1/b back by triggering it log.clear() await commands.run_cmd( commands.force_trigger_tasks(schd, ['1/b'], ['1'])) assert log_filter( regex='1/b.*added to the n=0 window', ) # remove 1/b by request (cylc remove) await commands.run_cmd( commands.remove_tasks(schd, ['1/b'], []) ) assert log_filter( regex='1/b.*removed from the n=0 window: request', ) # ensure that we are able to bring 1/b back by triggering it log.clear() await commands.run_cmd( commands.force_trigger_tasks(schd, ['1/b'], ['1'])) assert log_filter(regex='1/b.*added to the n=0 window',) async def test_set_future_flow(flow, scheduler, start, log_filter): """Manually-set outputs for new flow num must be recorded in the DB. See https://github.com/cylc/cylc-flow/pull/6186 To trigger the bug, the flow must be new but the task must have been spawned before in an earlier flow. """ # Scenario: after flow 1, set c1:succeeded in a future flow so # when b succeeds in the new flow it will spawn c2 but not c1. id_ = flow({ 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'cycling mode': 'integer', 'graph': { 'R1': 'b => c1 & c2', }, }, }) schd: 'Scheduler' = scheduler(id_) async with start(schd, level=logging.DEBUG): assert schd.pool.get_task(IntegerPoint("1"), "b") is not None, ( '1/b should be spawned on startup' ) # set b, c1, c2 succeeded in flow 1 schd.pool.set_prereqs_and_outputs( { TaskTokens('1', 'b'), TaskTokens('1', 'c1'), TaskTokens('1', 'c2'), }, prereqs=[], outputs=[], flow=['1'], ) schd.workflow_db_mgr.process_queued_ops() # set task c1:succeeded in flow 2 schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'c1')}, prereqs=[], outputs=[], flow=['2'] ) schd.workflow_db_mgr.process_queued_ops() # set b:succeeded in flow 2 and check downstream spawning schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'b')}, prereqs=[], outputs=[], flow=['2'] ) assert schd.pool.get_task(IntegerPoint("1"), "c1") is None, ( '1/c1 (flow 2) should not be spawned after 1/b:succeeded' ) assert schd.pool.get_task(IntegerPoint("1"), "c2") is not None, ( '1/c2 (flow 2) should be spawned after 1/b:succeeded' ) async def test_trigger_queue(one, run, db_select, complete): """It should handle triggering tasks in the queued state. Triggering a queued task with a new flow number should result in the task running with merged flow numbers. See https://github.com/cylc/cylc-flow/pull/6241 """ async with run(one): # the workflow should start up with one task in the original flow task = one.pool.get_tasks()[0] assert task.state(TASK_STATUS_WAITING, is_queued=True) assert task.flow_nums == {1} # trigger this task even though is already queued in flow 1 await commands.run_cmd( commands.force_trigger_tasks(one, [task.identity], ['2'])) # the merged flow should continue one.resume_workflow() await complete(one, timeout=2) assert db_select(one, False, 'task_outputs', 'flow_nums') == [ ('[1, 2]',), ('[1]',), ] async def test_reload_xtriggers(flow, scheduler, start): """It should rebuild xtriggers when the workflow is reloaded. See https://github.com/cylc/cylc-flow/pull/6263 """ config = { 'scheduling': { 'initial cycle point': '2000', 'graph': { 'R1': ''' @a => foo @b => foo ''' }, 'xtriggers': { 'a': 'wall_clock(offset="P0D")', 'b': 'wall_clock(offset="P5D")', }, } } id_ = flow(config) schd: Scheduler = scheduler(id_) def list_xtrig_mgr(): """List xtrigs from the xtrigger_mgr.""" return { key: repr(value) for key, value in schd.xtrigger_mgr.xtriggers.functx_map.items() } async def list_data_store(): """List xtrigs from the data_store_mgr.""" await schd.update_data_structure() return { value.label: value.id for key, value in schd.data_store_mgr.data[schd.tokens.id][ TASK_PROXIES ][ schd.tokens.duplicate(cycle='20000101T0000Z', task='foo').id ].xtriggers.items() } async with start(schd): # check xtrigs on startup assert list_xtrig_mgr() == { 'a': '', 'b': '', } assert await list_data_store() == { 'a': 'wall_clock(trigger_time=946684800)', 'b': 'wall_clock(trigger_time=947116800)', } # remove @a config['scheduling']['xtriggers'].pop('a') # modify @b config['scheduling']['xtriggers']['b'] = 'wall_clock(offset="PT12H")' # add @c config['scheduling']['xtriggers']['c'] = 'wall_clock(offset="PT1H")' config['scheduling']['graph']['R1'] = config['scheduling']['graph'][ 'R1' ].replace('@a', '@c') # reload flow(config, workflow_id=id_) await commands.run_cmd(commands.reload_workflow(schd)) # check xtrigs post-reload assert list_xtrig_mgr() == { 'b': '', 'c': '', } assert await list_data_store() == { 'b': 'wall_clock(trigger_time=946728000)', 'c': 'wall_clock(trigger_time=946688400)', } @pytest.mark.parametrize( 'expire_type', ['clock-expire', 'manual-expire', 'expire-trigger'] ) async def test_expire_dequeue_with_retries( flow, scheduler, start, expire_type ): """An expired waiting task should be removed from any queues. See https://github.com/cylc/cylc-flow/issues/6284 """ conf = { 'scheduler': { 'experimental': { 'expire triggers': True, }, }, 'scheduling': { 'initial cycle point': '2000', 'graph': { 'R1': 'foo' }, }, 'runtime': { 'foo': { 'execution retry delays': 'PT0S', 'outputs': { 'x': 'xxx', }, }, }, } if expire_type == 'clock-expire': # configure foo to clock-expire conf['scheduling']['special tasks'] = {'clock-expire': 'foo(PT0S)'} # run the clock-expire logic def method(schd): schd.pool.clock_expire_tasks() elif expire_type == 'manual-expire': # run the "cylc set" command to expire "foo" def method(schd): schd.pool.set_prereqs_and_outputs( {TaskTokens('20000101T0000Z', 'foo')}, prereqs=[], outputs=['expired'], flow=['1'], ) else: # configure the task to be expire-triggered once "bar" succeeds conf['scheduling']['graph']['R1'] = ''' foo:x => bar foo:x & bar => !foo ''' # run the "cylc set" command to succeed "bar" def method(schd): schd.pool.set_prereqs_and_outputs( {TaskTokens('20000101T0000Z', 'bar')}, prereqs=[], outputs=['succeeded'], flow=['1'], ) id_ = flow(conf) schd = scheduler(id_, run_mode='live') schd: Scheduler async with start(schd): foo = schd.pool._get_task_by_id('20000101T0000Z/foo') assert foo # fake a real submission failure # NOTE: yes, all of these things are needed for a valid test! # Try removing the "force=True" added in this commit and ensure the # "clock-expire" test fails before changing anything here! foo.submit_num += 1 foo.run_mode = RunMode.LIVE schd.task_job_mgr._set_retry_timers(foo, foo.tdef.rtconfig) schd.task_events_mgr.process_message(foo, 0, 'started') schd.task_events_mgr.process_message(foo, 0, 'xxx') schd.task_events_mgr.process_message(foo, 0, 'failed') schd.task_events_mgr._retry_task(foo, 0) # the task should start as "waiting(queued)" assert foo.state(TASK_STATUS_WAITING, is_queued=True) # expire the task via whichever method we are testing method(schd) # the task should enter the "expired" state assert foo.state(TASK_STATUS_EXPIRED, is_queued=False) # the task should also have been removed from the queue assert not schd.pool.task_queue_mgr.remove_task(foo) async def test_clock_expire_with_sequential_xtriggers( flow, scheduler, run, complete, log_filter, ): """Clock expire should play nicely with sequential xtriggers. See https://github.com/cylc/cylc-flow/issues/7103 """ id_ = flow({ 'scheduler': { 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': '2000', # v prevent this test passing if sequential spawning gets broken 'runahead limit': 'P2', 'special tasks': { 'clock-expire': 'a', }, 'graph': { 'P1Y': '@wall_clock => a => b => c' }, }, 'runtime': { 'a': { 'completion': 'succeeded or expired', }, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): # each cycle should expire and spawn in turn await complete(schd, '2005/a') for cycle in range(2000, 2006): assert log_filter(regex=rf'{cycle}/a:waiting.*expired') # * the next instance should have been spawned # * it should be the only task in the pool assert { itask.tokens.relative_id for itask in schd.pool.get_tasks() } == {'2006/a'} async def test_downstream_complete_before_upstream( flow, scheduler, start, db_select ): """It should handle an upstream task completing before a downstream task. See https://github.com/cylc/cylc-flow/issues/6315 """ id_ = flow( { 'scheduling': { 'graph': { 'R1': 'a => b', }, }, } ) schd = scheduler(id_) async with start(schd): # 1/a should be pre-spawned (parentless) a_1 = schd.pool.get_task(IntegerPoint('1'), 'a') assert a_1 # spawn 1/b (this can happens as the result of request e.g. trigger) b_1 = schd.pool.spawn_task('b', IntegerPoint('1'), {1}) schd.pool.add_to_pool(b_1) assert b_1 # mark 1/b as succeeded schd.task_events_mgr.process_message(b_1, 'INFO', 'succeeded') # 1/b should be removed from the pool (completed) assert schd.pool.get_tasks() == [a_1] # as a side effect the DB should have been updated assert ( TASK_OUTPUT_SUCCEEDED in db_select( schd, # "False" means "do not run the DB update before checking it" False, # do not change this to "True" 'task_outputs', 'outputs', name='b', cycle='1', )[0][0] ) # mark 1/a as succeeded schd.task_events_mgr.process_message(a_1, 'INFO', 'succeeded') # 1/a should be removed from the pool (completed) # 1/b should not be re-spawned by the success of 1/a assert schd.pool.get_tasks() == [] async def test_job_insert_on_crash(one_conf, flow, scheduler, start): """Ensure that a job can be inserted if its config is not known. It is possible, though very difficult, to create the circumstances where the configuration for the latest job is not held in `itask.jobs`. This should not happen under normal circumstances, but should be handled elegantly if it does occur. See https://github.com/cylc/cylc-flow/issues/6314 """ id_ = flow(one_conf) schd: Scheduler = scheduler(id_, run_mode='live') async with start(schd): task_1 = schd.pool.get_tasks()[0] # make it look like the task submitted but without storing the job # config in TaskProxy.jobs task_1.submit_num += 1 task_1.state.reset('preparing') schd.task_events_mgr.process_message( task_1, 'INFO', 'submitted', ) # the task state should be updated correctly assert task_1.state.status == 'submitted' # and a job entry should be added assert len(task_1.jobs) == 1 async def test_start_tasks( flow, scheduler, start, capture_submission, ): """Check starting from "start-tasks" with and without clock-triggers. """ id_ = flow( { 'scheduler': { 'cycle point format': '%Y', }, 'scheduling': { 'initial cycle point': '2040', 'runahead limit': 'P0Y', 'xtriggers': { 'wall_clock_satisfied': "wall_clock(offset='-P100Y')" }, 'graph': { 'P1Y': """ foo @wall_clock => bar @wall_clock_satisfied => baz qux """ } } } ) schd = scheduler( id_, starttask=['2050/foo', '2050/bar', '2050/baz'], paused_start=False ) async with start(schd): # capture any job submissions submitted_tasks = capture_submission(schd) assert submitted_tasks == set() # It should start up with: # - 2050/foo and 2051/foo (spawned to runahead limit) # - 2050/bar waiting on its (unsatisfied) clock-trigger # - 2050/baz waiting on its (satisfied) clock-trigger # - no qux instances (not listed as a start-task) itasks = schd.pool.get_tasks() assert ( set(itask.identity for itask in itasks) == { "2050/foo", "2051/foo", "2050/bar", "2050/baz", } ) # Check xtriggers for itask in itasks: schd.pool.xtrigger_mgr.call_xtriggers_async(itask) schd.pool.rh_release_and_queue(itask) # Release tasks that are ready to run. schd.release_tasks_to_run() # It should submit 2050/foo, 2051/foo, 2050/baz # It should not submit 2050/bar (waiting on clock trigger) assert ( set(itask.identity for itask in submitted_tasks) == { "2050/foo", "2051/foo", "2050/baz", } ) async def test_add_new_flow_rows_on_spawn( flow, scheduler, run, complete, db_select, capcall, ) -> None: """Task suicide should not override previously completed outputs. See https://github.com/cylc/cylc-flow/pull/6821 """ # capture all TaskPool.spawn_task() calls spawn_task_calls = capcall( 'cylc.flow.task_pool.TaskPool.spawn_task', TaskPool.spawn_task ) def list_spawn_task_calls(): """Return a list of the names of tasks which have been run through the "spawn_tasks" function so far.""" return [ args[1] for args, _kwargs in spawn_task_calls ] id_ = flow({ 'scheduling': { 'graph': { 'R1': ''' slow:fail? => foo slow? => !foo foo:x => x ''', }, }, 'runtime': { 'foo': { 'outputs': {'x': 'xxx'} }, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): # 1/slow should spawn on startup assert list_spawn_task_calls() == ['slow'] # set foo:x await commands.run_cmd( commands.set_prereqs_and_outputs( schd, ['1/foo'], ['1'], ['x'], None ) ) # 1/foo:x should be recorded in the DB: assert db_select( schd, True, 'task_outputs', 'outputs', cycle='1', name='foo' ) == [('{"x": "(manually completed)"}',)] # and 1/x should spawn: assert list_spawn_task_calls() == ['slow', 'x'] # run the workflow until completion await complete(schd, timeout=5) # 1/foo should spawn as a result of the suicide trigger assert list_spawn_task_calls() == ['slow', 'x', 'foo'] # the manually completed output should not have been overwritten by the # suicide trigger assert db_select( schd, True, 'task_outputs', 'outputs', cycle='1', name='foo' ) == [('{"x": "(manually completed)"}',)] async def test_add_to_pool( flow, scheduler, start, caplog ): """It should log attempts to add the same task again.""" id_ = flow('a') schd = scheduler(id_) async with start(schd): caplog.set_level(logging.DEBUG, CYLC_LOG) # 1/a should be pre-spawned (parentless) a_1 = schd.pool.get_task(IntegerPoint('1'), 'a') assert a_1 # add it again schd.pool.add_to_pool(a_1) assert "1/a not added to n=0: already exists" in caplog.text cylc-flow-8.6.4/tests/integration/test_force_trigger.py0000664000175000017500000007016315202510242023533 0ustar alastairalastair# 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 . import asyncio import logging from typing import ( Any as Fixture, Callable, ) import pytest from cylc.flow.commands import ( force_trigger_tasks, hold, reload_workflow, remove_tasks, resume, run_cmd, set_prereqs_and_outputs, ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.flow_mgr import FLOW_NEW from cylc.flow.scheduler import Scheduler from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_RUNNING, TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, ) from cylc.flow.workflow_status import AutoRestartMode async def test_workflow_paused_simple( one_conf, flow, scheduler, run, complete ): """It should run triggered tasks even if the workflow is paused.""" schd: Scheduler = scheduler(flow(one_conf), paused_start=True) async with run(schd): await run_cmd(force_trigger_tasks(schd, ['1/one'], ['1'])) await complete(schd, '1/one', allow_paused=True, timeout=1) async def test_workflow_paused_queues( flow: 'Fixture', scheduler: 'Fixture', start: 'Fixture', capture_submission: 'Fixture', log_filter: Callable ): """ Test manual triggering when the workflow is paused. The usual queue limiting behaviour is expected. https://github.com/cylc/cylc-flow/issues/6192 """ id_ = flow({ 'scheduling': { 'queues': { 'default': { 'limit': 1, }, }, 'graph': { 'R1': ''' a => x & y & z ''', }, }, }) schd = scheduler(id_, paused_start=True) # start the scheduler (but don't set the main loop running) async with start(schd): # capture task submissions (prevents real submissions) submitted_tasks = capture_submission(schd) # paused at start-up so no tasks should be submitted assert len(submitted_tasks) == 0 # manually trigger 1/x - it should be submitted await run_cmd(force_trigger_tasks(schd, ['1/x'], ["1"])) schd.release_tasks_to_run() assert len(submitted_tasks) == 1 # manually trigger 1/y - it should be queued but not submitted # (queue limit reached) await run_cmd(force_trigger_tasks(schd, ['1/y'], ["1"])) schd.release_tasks_to_run() assert len(submitted_tasks) == 1 # manually trigger 1/y again - it should be submitted # (triggering a queued task runs it) await run_cmd(force_trigger_tasks(schd, ['1/y'], ["1"])) schd.release_tasks_to_run() assert len(submitted_tasks) == 2 # manually trigger 1/y yet again - the trigger should be ignored # (task already active) await run_cmd(force_trigger_tasks(schd, ['1/y'], ["1"])) schd.release_tasks_to_run() assert len(submitted_tasks) == 2 assert log_filter( level=logging.WARNING, contains="Job already in process - ignoring" ) async def test_trigger_group_whilst_paused(flow, scheduler, run, complete): """Only group start tasks should run if the group is triggered whilst the scheduler is paused. Group start tasks have only off-group dependencies. Others (with in-group dependencies) should run as normal when their prerequisites are satisfied once the workflow is resumed. """ id_ = flow('a => b => c => d') schd = scheduler(id_, paused_start=True) async with run(schd): # trigger the chain await run_cmd(force_trigger_tasks(schd, ['1/a'], [])) # 1/a should run whilst the workflow is paused (group start-task) await complete(schd, '1/a', allow_paused=True, timeout=1) # 1/b should *not* run whilst the workflow is paused with pytest.raises(AssertionError): await complete(schd, '1/b', allow_paused=True, timeout=2) b = schd.pool._get_task_by_id('1/b') assert b.state.status == TASK_STATUS_WAITING # 1/b and 1/c should run once the workflow is resumed await run_cmd(resume(schd)) await complete(schd, '1/c') async def test_trigger_group( flow, scheduler, run, complete, log_filter ): """Test trigger of a sub-graph of future, then past, tasks. It should satisfy off-group task and xtrigger prerequisites automatically. """ cfg = { 'scheduling': { 'xtriggers': { 'xr': 'xrandom(0)' # never satisfied }, 'special tasks': { 'external-trigger': 'xt(cheese)' }, 'graph': { 'R1': """ # upstream: x => a # sub-graph for group trigger: a => b & c & xt => d # downstream: d => e => y # off-group prerequisites: @xr => x # stop the flow starting @xr => c # off-group xtrigger prerequisite @xr => off => b # off-group task prerequisite # (plus task xt has a push external trigger) # stop the flow from ending without intervention: @xr => y """ }, }, } id_ = flow(cfg) schd = scheduler(id_, paused_start=False) async with run(schd, level=logging.INFO) as log: # Trigger the group ahead of the flow. # It should run the group and flow on to downstream task e. await run_cmd( force_trigger_tasks(schd, ['1/a', '1/b', '1/c', '1/d', '1/xt'], []) ) await complete(schd, '1/e') # It should satisfy off-group prerequisites. assert log_filter( regex="1/c:waiting.*prerequisite force-satisfied: xr = xrandom") assert log_filter( regex="1/b:waiting.*prerequisite force-satisfied: 1/off:succeeded") assert log_filter( regex="1/a:waiting.*prerequisite force-satisfied: 1/x:succeeded") assert log_filter( regex='1/xt:waiting.*external trigger force-satisfied: "cheese"') log.clear() # Trigger the group again, now as past tasks, in the same flow. # It should erase flow 1 history to allow the rerun. # It should not flow on to e, which already ran in flow 1. # Create an active task that needs removing, to test that. await run_cmd( set_prereqs_and_outputs(schd, ['1/c'], [], [], ['all']) ) await run_cmd( force_trigger_tasks(schd, ['1/a', '1/b', '1/c', '1/d', '1/xt'], []) ) await complete(schd, '1/d') assert log_filter( contains=( "Removed tasks: 1/a (flows=1), 1/b (flows=1)," " 1/c (flows=1), 1/d (flows=1), 1/xt (flows=1)" ) ) assert log_filter( regex="1/c:waiting.*prerequisite force-satisfied: xr = xrandom") assert log_filter( regex="1/b:waiting.*prerequisite force-satisfied: 1/off:succeeded") assert log_filter( regex="1/a:waiting.*prerequisite force-satisfied: 1/x:succeeded") assert log_filter( regex='1/xt:waiting.*external trigger force-satisfied: "cheese"') log.clear() # Trigger the group again, as past tasks, in a new flow. # It should flow on to task e again, in flow 2. await run_cmd( force_trigger_tasks( schd, ['1/a', '1/b', '1/c', '1/d', '1/xt'], ['new']) ) await complete(schd, '1/e') assert log_filter( regex=( r"1/c\(flows=2\):waiting.*prerequisite" r" force-satisfied: xr = xrandom" ) ) assert log_filter( regex=( r"1/b\(flows=2\):waiting.*prerequisite" r" force-satisfied: 1/off:succeeded" ) ) assert log_filter( regex=( r"1/a\(flows=2\):waiting.*prerequisite" r" force-satisfied: 1/x:succeeded" ) ) assert log_filter( regex=( r"1/xt\(flows=2\):waiting.*external trigger" r' force-satisfied: "cheese"' ) ) # Task d (in the group) should have run 3 times. assert log_filter( contains="[1/d/03(flows=2):running] => succeeded" ) # Task e (downstream of the group) only twice (once in each flow). assert log_filter( contains="[1/e/02(flows=2):running] => succeeded" ) async def test_trigger_active_task_in_group( flow, scheduler, run, complete, log_filter, reflog, ): """It should remove (and kill) active tasks that are not group start tasks. The workflow `a => b => c` starts out like this: * a (succeeded, n=1) * b (running, n=0) * c (waiting, n=1) Then we reload to add the dependency `d => b` and trigger a, b & c. * a - should be removed and re-spawned. * b - should be removed and re-spawned with `a => b` unsatisfied but `d => b` force-satisfied. * c - should be left alone. See point (4): https://github.com/cylc/cylc-admin/blob/master/docs/proposal-group-trigger.md#details """ conf = { 'scheduling': { 'graph': {'R1': 'a => b => c'}, }, } id_ = flow(conf) schd = scheduler(id_, paused_start=False) async with run(schd): # capture triggering information triggers = reflog(schd) # run until 1/a:succeeded await complete(schd, '1/a') # check 1/b prereqs b_1 = schd.pool.get_task(IntegerPoint('1'), 'b') assert [ (prereq.task, is_satisfied) for condition in b_1.state.prerequisites for prereq, is_satisfied in condition.items() ] == [ # 1/b has a single prereq, it has been satisfied normally ('a', 'satisfied naturally'), ] # submit 1/b schd.submit_task_jobs([b_1]) # reload the workflow adding the dependency "d => b" conf['scheduling']['graph']['R1'] += '\nd => b' flow(conf, workflow_id=id_) await run_cmd(reload_workflow(schd)) # trigger the chain a => b => c await run_cmd(force_trigger_tasks(schd, ['1/a', '1/b', '1/c'], [])) # active task 1/b should be killed assert log_filter( contains=( '[1/b/01:running] removed from the n=0 window: request' ' - active job orphaned' ) ) # check 1/b prereqs b_1 = schd.pool.get_task(IntegerPoint('1'), 'b') # ret reloaded task assert [ (prereq.task, is_satisfied) for condition in b_1.state.prerequisites for prereq, is_satisfied in condition.items() ] == [ # in-group prereq has been reset ('a', False), # off-group prereq has been "force satisfied" ('d', 'force satisfied'), ] # the workflow should run the chain a => b => c as instructed await complete(schd, '1/c') assert triggers == { ('1/a', None), ('1/b', ('1/a',)), # original run ('1/b', ('1/a', '1/d')), # force-triggered run ('1/c', ('1/b',)), } async def test_trigger_group_in_flow( flow, scheduler, run, complete, reflog, db_select, ): """It should remove tasks from the triggered flow(s). Tests the following statement from the proposal: > Use the same flow numbers, as determined by the trigger command in the > usual way, throughout the operation > > -- https://cylc.github.io/cylc-admin/proposal-group-trigger.html#details """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a => b => c => d' } } }) schd = scheduler(id_, paused_start=False) async with run(schd): # prevent shutdown after 1/c completes await run_cmd(hold(schd, ['1/d'])) # run the chain, merge in flow "2" part way through triggers = reflog(schd, flow_nums=True) await complete(schd, '1/a') await run_cmd(force_trigger_tasks(schd, ['1/b'], ['2'])) await complete(schd, '1/c') assert triggers == { # (task, flow_nums, triggered_off_of) ('1/a', '[1]', None), ('1/b', '[1, 2]', ('1/a',)), # flow "2" merged in ('1/c', '[1, 2]', ('1/b',)), # flow "2" merged in } # re-run the chain in flow "2" triggers = reflog(schd, flow_nums=True) await run_cmd(force_trigger_tasks(schd, ['1/a', '1/b', '1/c'], ['2'])) await complete(schd, '1/c', timeout=10) assert triggers == { # (task, flow_nums, triggered_off_of) ('1/a', '[2]', None), ('1/b', '[2]', ('1/a',)), ('1/c', '[2]', ('1/b',)), } # ensure that flow "2" was removed from the tasks in the original run # by the group-trigger assert set(db_select( schd, True, 'task_outputs', 'name', 'flow_nums', )) == { # original run ('a', '[1]'), ('b', '[1]'), # flow "2" has been removed ('c', '[1]'), # flow "2" has been removed ('d', '[1, 2]'), # subsequent run ('a', '[2]'), ('b', '[2]'), ('c', '[2]'), } async def test_trigger_n0_tasks( flow, scheduler, run, complete, db_select, ): """It should trigger tasks within their flow if available, else all flows. * N=0 tasks already have a flow assigned. * N!=0 tasks do not yet have a flow assigned. When we are triggering n!=0 tasks, there is no appropriate flow to run them in (this would involve flow merge prediction), so we default to all active flows as the most/only sensible default. Before group trigger, we triggered tasks independently, i.e. we assumed there were no dependencies between the tasks and ran them all simultaneously. With the group trigger extension, we enhanced trigger to make it aware of interdependent tasks. Triggering independent tasks (pre group-trigger behaviour): * If we trigger a n=0 task, we leave it in the flow it is already in. * If we trigger a n!=0 task, we default to all active flows. Triggering interdependent tasks (group trigger extension): If the list of tasks being triggered contains any interdependent tasks, we treat these interdependent tasks as a group. * If we trigger a group which contains n=0 tasks, the whole group should be triggered using the set of flows possessed by these n=0 tasks. * If we trigger a group which does not contain n=0 tasks, we default to all active flows. """ id_ = flow({ 'scheduling': { 'graph': { 'R1': ''' # group 1 (we will trigger a, b & c) a => b => c => z # group 2 (we will trigger e, f & g) e => f => g => z # group 3 (we will trigger y) x => y => z ''' } } }) schd = scheduler(id_, paused_start=False) async with run(schd): # cylc hold 1/x await run_cmd(hold(schd, ['1/x'])) # group 1: spawn n>0 tasks into flows 2 & 3 await run_cmd( set_prereqs_and_outputs(schd, ['1/b'], ['2'], None, ['all']) ) await run_cmd( set_prereqs_and_outputs(schd, ['1/c'], ['3'], None, ['all']) ) # group 2: spawn n>0 tasks into flows 4 & 5 await run_cmd( set_prereqs_and_outputs(schd, ['1/f'], ['4'], None, ['all']) ) await run_cmd( set_prereqs_and_outputs(schd, ['1/g'], ['5'], None, ['all']) ) # trigger all three groups of tasks await run_cmd( force_trigger_tasks( schd, ['1/a', '1/b', '1/c', '1/e', '1/f', '1/g', '1/y'], [] ) ) await complete( schd, '1/a', '1/b', '1/c', '1/e', '1/f', '1/g', '1/y', '1/z' ) assert set(db_select( schd, True, 'task_outputs', 'name', 'flow_nums', )) == { # junk entries inserted on spawn/set ('a', '[1]'), # initial flow spawned on startup ('b', '[]'), # created by "cylc set" ('c', '[]'), # created by "cylc set" ('e', '[1]'), # initial flow spawned on startup ('f', '[]'), # created by "cylc set" ('g', '[]'), # created by "cylc set" ('x', '[1]'), # initial flow spawned on startup # group 1: contained tasks in flows 1, 2 & 3 ('a', '[1, 2, 3]'), ('b', '[1, 2, 3]'), ('c', '[1, 2, 3]'), # group 2: contained tasks in flows 1, 4 & 5 ('e', '[1, 4, 5]'), ('f', '[1, 4, 5]'), ('g', '[1, 4, 5]'), # group 3: contained tasks in flows None ('y', '[1, 2, 3, 4, 5]'), # downstream task ('z', '[1, 2, 3, 4, 5]'), } async def test_replay_outputs(flow, scheduler, start, complete, log_filter): """Triggered group start tasks re-emit (kind of) earlier outputs. https://github.com/cylc/cylc-flow/issues/6858 Example graph: a:started => b => end k:kustom => l => end k:kustom => offg If I trigger a, b, k, l AFTER a:started and k:kustom have completed: cylc trigger workflow //1/a //1/b //1/k //1/l Then I should expect outputs `k:kustom` and `a:started` to be re-used to satify b and l in the triggered flow, but NOT off-group task offg. """ msg_prereq = '[1/{}:waiting(runahead)] prerequisite force-satisfied: 1/{}' msg_spawned = "[1/{}:waiting(runahead)] => waiting" msg_removed = "Removed tasks: 1/{}" wid = flow({ 'scheduling': { 'graph': { 'R1': """ a:started => b => end k:kustom => l => end k:kustom => offg """ } }, 'runtime': { 'a': {}, 'k': { 'outputs': {'kustom': 'custom message'} } } }) schd = scheduler(wid, paused_start=True) async with start(schd): # Set initial tasks a and k to "running" so they are recognized as # live during the forthcoming trigger operation. # Complete the a:started and k:kustom outputs. await run_cmd( set_prereqs_and_outputs(schd, ['1/a'], ['1'], ['started'], None) ) await run_cmd( set_prereqs_and_outputs(schd, ['1/k'], ['1'], ['kustom'], None) ) # It should spawn b, l, and offg. for task in ['b', 'l', 'offg']: assert log_filter(contains=msg_spawned.format(task)) # Set a and k as running so they're recognized as live start tasks # by the trigger operation. for itask in schd.pool.get_tasks(): itask.state_reset(TASK_STATUS_RUNNING) # Now trigger the group. await run_cmd( force_trigger_tasks(schd, ['1/a', '1/b', '1/k', '1/l'], []) ) # It should remove b and l (in-group tasks) for task in ['b', 'l']: assert log_filter(contains=msg_removed.format(task)) # But they will be respawned immediately by re-satisfying dependence # on the earlier outputs of a and k for in-group tasks: assert log_filter(contains=msg_prereq.format('b', 'a:started')) assert log_filter(contains=msg_prereq.format('l', 'k:custom message')) # But not for the off-group task offg: assert not log_filter( contains=msg_prereq.format('offg', 'k:custom message')) async def test_trigger_with_sequential_task(flow, scheduler, run, log_filter): """It should trigger a failed sequential task. See https://github.com/cylc/cylc-flow/issues/6911 """ id_ = flow({ 'scheduling': { 'initial cycle point': '1', 'final cycle point': '2', 'cycling mode': 'integer', 'special tasks': { 'sequential': 'foo', }, 'graph': { 'R1': 'install => foo', 'P1': 'foo', }, }, 'runtime': { 'foo': { 'simulation': { 'fail cycle points': '2', }, }, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): # wait for 2/foo:failed async with asyncio.timeout(5): while True: itask = schd.pool._get_task_by_id('2/foo') if itask and itask.state.outputs.is_message_complete('failed'): break await asyncio.sleep(0) # re-trigger 2/foo await run_cmd( force_trigger_tasks( schd, ['2/foo'], [] ) ) # it should re-run async with asyncio.timeout(5): while True: if log_filter(contains='[2/foo/02:running] (received)failed'): break await asyncio.sleep(0) async def test_trigger_with_task_selector(flow, scheduler, start, monkeypatch): """Test task matching with the trigger command. This test is intended to extend the other integration tests for ID matching with a real use case to ensure the code in cylc.flow.commands (which parses and standardises IDs) is working correctly. """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a & b & c & d & e & f & g' } } }) schd: Scheduler = scheduler(id_) async with start(schd): trigger_calls = [] def _force_trigger_tasks(_schd, ids, *_, **__): trigger_calls.append( {id_.relative_id_with_selectors for id_ in ids} ) monkeypatch.setattr( 'cylc.flow.commands._force_trigger_tasks', _force_trigger_tasks ) schd.pool._get_task_by_id('1/a').state_reset(TASK_STATUS_SUBMITTED) schd.pool._get_task_by_id('1/b').state_reset(TASK_STATUS_RUNNING) schd.pool._get_task_by_id('1/c').state_reset(TASK_STATUS_SUCCEEDED) schd.pool._get_task_by_id('1/d').state_reset(TASK_STATUS_FAILED) await run_cmd(force_trigger_tasks(schd, ['*:submitted'], [])) assert trigger_calls == [{'1/a'}] trigger_calls.clear() await run_cmd(force_trigger_tasks(schd, ['*:running'], [])) assert trigger_calls == [{'1/b'}] trigger_calls.clear() await run_cmd(force_trigger_tasks(schd, ['*:succeeded'], [])) assert trigger_calls == [{'1/c'}] trigger_calls.clear() await run_cmd(force_trigger_tasks(schd, ['*:failed'], [])) assert trigger_calls == [{'1/d'}] trigger_calls.clear() async def test_pre_warm_start_group_trigger(flow, scheduler, run, complete): """Group-triggered tasks that are before the start point should run in order. This is designed to handle re-running a family of one-time setup tasks in a warm-started workflow. https://github.com/cylc/cylc-flow/pull/7101 """ schd: Scheduler = scheduler( flow({ 'scheduling': { 'cycling mode': 'integer', 'runahead limit': 'P2', 'graph': { 'R1': 'start => c1 => c2 => c3 => foo', 'P1': 'foo[-P1] => foo', }, }, 'runtime': { 'COLD': {}, **{f'c{n}': {'inherit': 'COLD'} for n in (1, 2, 3)}, }, }), paused_start=False, startcp='5' ) async with run(schd): schd.pool.set_hold_point(IntegerPoint('4')) await run_cmd(force_trigger_tasks(schd, ['1/COLD'], [])) assert schd.pool.get_task_ids() == {'1/c1', '5/foo'} await complete(schd, '1/c1', timeout=10) assert schd.pool.get_task_ids() == {'1/c2', '5/foo'} assert schd.pool._get_task_by_id('1/c2').state(TASK_STATUS_WAITING) await complete(schd, '1/c2', '1/c3', timeout=10) assert schd.pool.get_task_ids() == {'5/foo'} # Check list of pre-start tasks to trigger has been cleared: assert not schd.pool.pre_start_tasks_to_trigger async def test_pre_warm_start_trigger_flow_new( flow, scheduler, run, complete ): """In a warm-started workflow, triggering tasks that are < startcp: * Should not flow on in flow=1 * Should flow on in flow=new As we don't have any history for pre-startcp tasks in a warm start, we treat them as complete in flow=1. https://github.com/cylc/cylc-flow/pull/7148 """ schd: Scheduler = scheduler( flow({ 'scheduling': { 'cycling mode': 'integer', 'graph': { 'P1': 'foo[-P1] => foo', }, }, }), paused_start=False, startcp='10' ) async with run(schd): schd.pool.set_hold_point(IntegerPoint('9')) # flow=1 - don't flow on: await run_cmd(force_trigger_tasks(schd, ['1/foo'], [])) assert schd.pool.get_task_ids() == {'1/foo', '10/foo'} await complete(schd, '1/foo', timeout=10) assert schd.pool.get_task_ids() == {'10/foo'} # flow=new - flow on: await run_cmd(force_trigger_tasks(schd, ['3/foo'], [FLOW_NEW])) assert schd.pool.get_task_ids() == {'3/foo', '10/foo'} await complete(schd, '5/foo', timeout=10) assert schd.pool.get_task_ids() == {'6/foo', '10/foo'} async def test_pre_warm_start_interraction_with_auto_restart( flow, scheduler, start, log_filter, ): """Test interaction between warm-start pre-startcp tasks and auto-restart. The list of pre-startcp tasks to run is held in memory only, no DB backup, so get wiped out by restart. """ schd: Scheduler = scheduler( flow( { 'scheduling': { 'cycling mode': 'integer', 'graph': { 'P1': 'foo[-P1] => foo', }, }, } ), paused_start=False, startcp='10', ) async with start(schd): # trigger a pre-initial task (pre start-cycle point) await run_cmd(force_trigger_tasks(schd, ['^/foo'], [])) assert schd.pool.pre_start_tasks_to_trigger # configure the workflow to auto-retsart schd.auto_restart_mode = AutoRestartMode.RESTART_NORMAL schd.auto_restart_time = 0 # the workflow should not auto-restart whilst there are pre-initial # tasks in play await schd.workflow_shutdown() assert log_filter( contains='Waiting for pre start-cycle tasks to complete' ) assert schd.stop_mode is None # remove the pre-initial task await run_cmd(remove_tasks(schd, ['^/foo'], [])) assert schd.pool.pre_start_tasks_to_trigger == set() # the workflow should now auto-restart as normal await schd.workflow_shutdown() assert schd.stop_mode is not None cylc-flow-8.6.4/tests/integration/test_sequential_xtriggers.py0000664000175000017500000002416715202510242025165 0ustar alastairalastair# 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 . # mypy: disable-error-code=union-attr """Test interactions with sequential xtriggers.""" from unittest.mock import patch import pytest from cylc.flow.commands import ( run_cmd, force_trigger_tasks, remove_tasks ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.exceptions import XtriggerConfigError from cylc.flow.id import TaskTokens from cylc.flow.scheduler import Scheduler def list_cycles(schd: Scheduler): """List the task instance cycle points present in the pool.""" return sorted(itask.tokens['cycle'] for itask in schd.pool.get_tasks()) @pytest.fixture() def sequential(flow, scheduler): id_ = flow({ 'scheduler': { 'cycle point format': 'CCYY', }, 'scheduling': { 'runahead limit': 'P2', 'initial cycle point': '2000', 'graph': { 'P1Y': '@wall_clock => foo', } } }) return scheduler(id_) async def test_remove(sequential: Scheduler, start): """It should spawn the next instance when a task is removed. Ensure that removing a task with a sequential xtrigger does not break the chain causing future instances to be removed from the workflow. """ async with start(sequential): # the scheduler starts with one task in the pool assert list_cycles(sequential) == ['2000'] # it sequentially spawns out to the runahead limit for year in range(2000, 2010): foo = sequential.pool.get_task(ISO8601Point(f'{year}'), 'foo') if foo.state(is_runahead=True): break sequential.xtrigger_mgr.call_xtriggers_async(foo) assert list_cycles(sequential) == [ '2000', '2001', '2002', '2003', ] # remove all tasks in the pool await run_cmd(remove_tasks(sequential, ['*'], ["1"])) # the next cycle should be automatically spawned assert list_cycles(sequential) == ['2004'] # NOTE: You won't spot this issue in a functional test because the # re-spawned tasks are detected as completed and automatically removed. # So ATM not dangerous, but potentially inefficient. async def test_trigger(sequential, start): """It should spawn its next instance if triggered ahead of time. If you manually trigger a sequentially spawned task before its xtriggers have become satisfied, then the sequential spawning chain is broken. The task pool should defend against this to ensure that triggering a task doesn't cancel it's future instances. """ async with start(sequential): assert list_cycles(sequential) == ['2000'] foo = sequential.pool.get_task(ISO8601Point('2000'), 'foo') await run_cmd(force_trigger_tasks(sequential, [foo.identity], ["1"])) foo.state_reset('succeeded') sequential.pool.spawn_on_output(foo, 'succeeded') assert list_cycles(sequential) == ['2000', '2001'] async def test_set_outputs(sequential, start): """It should spawn its next instance if outputs are set ahead of time. If you set outputs of a sequentially spawned task before its xtriggers have become satisfied, then the sequential spawning chain is broken. The task pool should defend against this to ensure that setting outputs doesn't cancel it's future instances and their downstream tasks. """ async with start(sequential): assert list_cycles(sequential) == ['2000'] sequential.pool.get_task(ISO8601Point('2000'), 'foo') # set foo:succeeded it should spawn next instance sequential.pool.set_prereqs_and_outputs( {TaskTokens('2000', 'foo')}, ["succeeded"], [], []) assert list_cycles(sequential) == ['2001'] async def test_set_prereqs(sequential, start): """It should spawn next after manual xtrigger prereq satisfaction.""" async with start(sequential): assert list_cycles(sequential) == ['2000'] sequential.pool.get_task(ISO8601Point('2000'), 'foo') # satisfy foo's xtriggers - it should spawn next instance sequential.pool.set_prereqs_and_outputs( {TaskTokens('2000', 'foo')}, [], ['xtrigger/all:succeeded'], []) assert list_cycles(sequential) == ['2000', '2001'] async def test_reload(sequential, start): """It should set the is_xtrigger_sequential flag on reload. TODO: test that changes to the sequential status in the config get picked up on reload """ async with start(sequential): # the task should be marked as sequential pre_reload = sequential.pool.get_task(ISO8601Point('2000'), 'foo') assert pre_reload.is_xtrigger_sequential is True # reload the workflow sequential.pool.reload(sequential.config) # the original task proxy should have been replaced post_reload = sequential.pool.get_task(ISO8601Point('2000'), 'foo') assert id(pre_reload) != id(post_reload) # the new task should be marked as sequential assert post_reload.is_xtrigger_sequential is True @pytest.mark.parametrize('is_sequential', [True, False]) @pytest.mark.parametrize('xtrig_def', [ 'wall_clock(sequential={})', 'wall_clock(PT1H, sequential={})', 'xrandom(1, 1, sequential={})', ]) async def test_sequential_arg_ok( flow, scheduler, start, xtrig_def: str, is_sequential: bool ): """Test passing the sequential argument to xtriggers.""" wid = flow({ 'scheduler': { 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': '2000', 'runahead limit': 'P1', 'xtriggers': { 'myxt': xtrig_def.format(is_sequential), }, 'graph': { 'P1Y': '@myxt => foo', } } }) schd: Scheduler = scheduler(wid) expected_num_cycles = 1 if is_sequential else 3 async with start(schd): itask = schd.pool.get_task(ISO8601Point('2000'), 'foo') assert itask.is_xtrigger_sequential is is_sequential assert len(list_cycles(schd)) == expected_num_cycles def test_sequential_arg_bad(flow, validate): """Test validation of 'sequential' arg for custom xtrigger function def""" wid = flow({ 'scheduling': { 'xtriggers': { 'myxt': 'custom_xt(42)' }, 'graph': { 'R1': '@myxt => foo' } } }) def xtrig1(x, sequential): """This uses 'sequential' without a default value""" return True def xtrig2(x, sequential='True'): """This uses 'sequential' with a default of wrong type""" return True for xtrig in (xtrig1, xtrig2): with patch( 'cylc.flow.xtrigger_mgr.get_xtrig_func', return_value=xtrig ): with pytest.raises(XtriggerConfigError) as excinfo: validate(wid) assert ( "reserved argument 'sequential' with no boolean default" ) in str(excinfo.value) def test_sequential_arg_bad2(flow, validate): """Test validation of 'sequential' arg for xtrigger calls""" wid = flow({ 'scheduling': { 'initial cycle point': '2000', 'xtriggers': { 'clock': 'wall_clock(sequential=3)', }, 'graph': { 'R1': '@clock => foo', }, }, }) with pytest.raises(XtriggerConfigError) as excinfo: validate(wid) assert ( "invalid argument 'sequential=3' - must be boolean" ) in str(excinfo.value) @pytest.mark.parametrize('is_sequential', [True, False]) async def test_any_sequential(flow, scheduler, start, is_sequential: bool): """Test that a task is marked as sequential if any of its xtriggers are.""" wid = flow({ 'scheduling': { 'xtriggers': { 'xt1': 'custom_xt()', 'xt2': f'custom_xt(sequential={is_sequential})', 'xt3': 'custom_xt(sequential=False)', }, 'graph': { 'R1': '@xt1 & @xt2 & @xt3 => foo', } } }) with patch( 'cylc.flow.xtrigger_mgr.get_xtrig_func', return_value=lambda *a, **k: True ): schd: Scheduler = scheduler(wid) async with start(schd): itask = schd.pool.get_task(IntegerPoint('1'), 'foo') assert itask.is_xtrigger_sequential is is_sequential async def test_override(flow, scheduler, start): """Test that the 'sequential=False' arg can override a default of True.""" wid = flow({ 'scheduling': { 'sequential xtriggers': True, 'xtriggers': { 'xt1': 'custom_xt()', 'xt2': 'custom_xt(sequential=False)', }, 'graph': { 'R1': ''' @xt1 => foo @xt2 => bar ''', } } }) with patch( 'cylc.flow.xtrigger_mgr.get_xtrig_func', return_value=lambda *a, **k: True ): schd: Scheduler = scheduler(wid) async with start(schd): foo = schd.pool.get_task(IntegerPoint('1'), 'foo') assert foo.is_xtrigger_sequential is True bar = schd.pool.get_task(IntegerPoint('1'), 'bar') assert bar.is_xtrigger_sequential is False cylc-flow-8.6.4/tests/integration/test_taskdef.py0000664000175000017500000000524615202510242022333 0ustar alastairalastair# 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 . from cylc.flow.config import WorkflowConfig from cylc.flow.pathutil import get_workflow_run_dir from cylc.flow.scheduler_cli import RunOptions from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED from cylc.flow.workflow_files import WorkflowFiles async def test_almost_self_suicide(flow, scheduler, start): """Suicide triggers should not count as upstream tasks when looking to spawn parentless tasks. https://github.com/cylc/cylc-flow/issues/6594 For the example under test, pre-requisites for ``!a`` should not be considered the same as pre-requisites for ``a``. If the are then then is parentless return false for all cases of ``a`` not in the inital cycle and subsequent cycles never run. """ wid = flow({ 'scheduler': {'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': 1990, 'final cycle point': 1992, 'graph': { 'R1': 'install_cold', 'P1Y': 'install_cold[^] => a? => b?\nb:fail? => !a?' } } }) schd = scheduler(wid) async with start(schd): tasks = [str(t) for t in schd.pool.get_tasks()] for task in ['1990/a:waiting', '1991/a:waiting', '1992/a:waiting']: assert task in tasks def test_graph_children(flow): """TaskDef.graph_children should not include duplicates. https://github.com/cylc/cylc-flow/issues/6619#issuecomment-2668932069 """ wid = flow({ 'scheduling': { 'graph': { 'R1': 'foo | bar => fin', }, }, 'task parameters': { 'n': '1..3', }, }) config = WorkflowConfig( wid, get_workflow_run_dir(wid, WorkflowFiles.FLOW_FILE), RunOptions() ) foo = config.taskdefs['foo'] graph_children = list(foo.graph_children.values())[0] assert [name for name, _ in graph_children[TASK_OUTPUT_SUCCEEDED]] == [ 'fin' ] cylc-flow-8.6.4/tests/integration/network/0000775000175000017500000000000015202510242020763 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/network/__init__.py0000664000175000017500000000000015202510242023062 0ustar alastairalastaircylc-flow-8.6.4/tests/integration/network/test_zmq.py0000664000175000017500000000442015202510242023203 0ustar alastairalastair# 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 . import pytest import zmq from cylc.flow.exceptions import CylcError from cylc.flow.network import ZMQSocketBase from .key_setup import setup_keys @pytest.fixture(scope='module') def myflow(mod_flow, mod_one_conf): return mod_flow(mod_one_conf) def test_single_port(myflow, port_range): """Test server on a single port and port in use exception.""" context = zmq.Context() setup_keys(myflow) # auth keys are required for comms serv1 = ZMQSocketBase( zmq.REP, context=context, workflow=myflow, bind=True) serv2 = ZMQSocketBase( zmq.REP, context=context, workflow=myflow, bind=True) serv1._socket_bind(*port_range) port = serv1.port with pytest.raises(CylcError, match=r"Address already in use"): serv2._socket_bind(port, port) serv2.stop() serv1.stop() context.destroy() def test_start(myflow, port_range): """Test socket start.""" setup_keys(myflow) # auth keys are required for comms publisher = ZMQSocketBase(zmq.PUB, workflow=myflow, bind=True) assert publisher.loop is None assert publisher.port is None publisher.start(*port_range) assert publisher.loop is not None assert publisher.port is not None publisher.stop() def test_stop(myflow, port_range): """Test socket/thread stop.""" setup_keys(myflow) # auth keys are required for comms publisher = ZMQSocketBase(zmq.PUB, workflow=myflow, bind=True) publisher.start(*port_range) assert not publisher.socket.closed publisher.stop() assert publisher.socket.closed cylc-flow-8.6.4/tests/integration/network/test_resolvers.py0000664000175000017500000002244215202510242024424 0ustar alastairalastair# 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 . import logging from typing import AsyncGenerator, Callable from unittest.mock import Mock import pytest from cylc.flow.data_store_mgr import EDGES, TASK_PROXIES from cylc.flow.id import Tokens from cylc.flow import CYLC_LOG from cylc.flow.network.resolvers import Resolvers from cylc.flow.scheduler import Scheduler from cylc.flow.workflow_status import StopMode @pytest.fixture def flow_args(): return { 'workflows': [], 'exworkflows': [], } @pytest.fixture def node_args(): return { 'workflows': [], 'exworkflows': [], 'ids': [], 'exids': [], 'states': [], 'exstates': [], 'mindepth': -1, 'maxdepth': -1, } @pytest.fixture(scope='module') async def mock_flow( mod_flow: Callable[..., str], mod_scheduler: Callable[..., Scheduler], mod_start, ) -> AsyncGenerator[Scheduler, None]: ret = Mock() ret.id_ = mod_flow({ 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'initial cycle point': '2000', 'dependencies': { 'R1': 'prep => foo', 'PT12H': 'foo[-PT12H] => foo => bar' } } }) ret.schd = mod_scheduler(ret.id_, paused_start=True) async with mod_start(ret.schd): ret.schd.pool.release_runahead_tasks() ret.schd.data_store_mgr.initiate_data_model() ret.owner = ret.schd.owner ret.name = ret.schd.workflow ret.id = list(ret.schd.data_store_mgr.data.keys())[0] ret.resolvers = Resolvers( ret.schd.data_store_mgr, schd=ret.schd ) ret.data = ret.schd.data_store_mgr.data[ret.id] ret.node_ids = [ node.id for node in ret.data[TASK_PROXIES].values() ] ret.edge_ids = [ edge.id for edge in ret.data[EDGES].values() ] yield ret async def test_get_workflows(mock_flow, flow_args): """Test method returning workflow messages satisfying filter args.""" flow_args['workflows'].append({ 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None, }) flow_msgs = await mock_flow.resolvers.get_workflows(flow_args) assert len(flow_msgs) == 1 async def test_get_nodes_all(mock_flow, node_args): """Test method returning workflow(s) node message satisfying filter args. """ node_args['workflows'].append({ 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None, }) node_args['states'].append('failed') nodes = await mock_flow.resolvers.get_nodes_all(TASK_PROXIES, node_args) assert len(nodes) == 0 node_args['states'] = [] node_args['ids'].append(Tokens(mock_flow.node_ids[0])) nodes = [ n for n in await mock_flow.resolvers.get_nodes_all( TASK_PROXIES, node_args) if n in mock_flow.data[TASK_PROXIES].values() ] assert len(nodes) == 1 async def test_get_nodes_by_ids(mock_flow, node_args): """Test method returning workflow(s) node messages who's ID is a match to any given.""" node_args['workflows'].append({ 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None }) nodes = await mock_flow.resolvers.get_nodes_by_ids(TASK_PROXIES, node_args) assert len(nodes) == 0 node_args['native_ids'] = mock_flow.node_ids nodes = [ n for n in await mock_flow.resolvers.get_nodes_by_ids( TASK_PROXIES, node_args ) if n in mock_flow.data[TASK_PROXIES].values() ] assert len(nodes) > 0 async def test_get_node_by_id(mock_flow, node_args): """Test method returning a workflow node message who's ID is a match to that given.""" node_args['id'] = Tokens( user='me', workflow='mine', cycle='20500808T00', task='jin', ).id node_args['workflows'].append({ 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None }) node = await mock_flow.resolvers.get_node_by_id(TASK_PROXIES, node_args) assert node is None node_args['id'] = mock_flow.node_ids[0] node = await mock_flow.resolvers.get_node_by_id(TASK_PROXIES, node_args) assert node in mock_flow.data[TASK_PROXIES].values() async def test_get_edges_all(mock_flow, flow_args): """Test method returning all workflow(s) edges.""" edges = [ e for e in await mock_flow.resolvers.get_edges_all(flow_args) if e in mock_flow.data[EDGES].values() ] assert len(edges) > 0 async def test_get_edges_by_ids(mock_flow, node_args): """Test method returning workflow(s) edge messages who's ID is a match to any given edge IDs.""" edges = await mock_flow.resolvers.get_edges_by_ids(node_args) assert len(edges) == 0 node_args['native_ids'] = mock_flow.edge_ids edges = [ e for e in await mock_flow.resolvers.get_edges_by_ids(node_args) if e in mock_flow.data[EDGES].values() ] assert len(edges) > 0 async def test_mutator(mock_flow, flow_args): """Test the mutation method.""" flow_args['workflows'].append({ 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None }) args = {} meta = {} response = await mock_flow.resolvers.mutator( None, 'pause', flow_args, args, meta ) assert response[0]['id'] == mock_flow.id async def test_mutation_mapper(mock_flow): """Test the mapping of mutations to internal command methods.""" meta = {} response = await mock_flow.resolvers._mutation_mapper('pause', {}, meta) assert response[0] is True # (True, command-uuid-str) with pytest.raises(ValueError): await mock_flow.resolvers._mutation_mapper('non_exist', {}, meta) async def test_command_logging(mock_flow, caplog, log_filter): """The command log message should include non-owner name.""" meta = {} caplog.set_level(logging.INFO, CYLC_LOG) await mock_flow.resolvers._mutation_mapper( "stop", {'mode': StopMode.REQUEST_CLEAN.value}, meta, ) assert log_filter(contains='Command "stop" received') # put_messages: only log for owner kwargs = { "task_job": "1/foo/01", "event_time": "bedtime", "messages": [[logging.CRITICAL, "it's late"]] } meta["auth_user"] = mock_flow.owner await mock_flow.resolvers._mutation_mapper("put_messages", kwargs, meta) assert not log_filter(contains='Command "put_messages" received:') meta["auth_user"] = "Dr Spock" await mock_flow.resolvers._mutation_mapper("put_messages", kwargs, meta) assert log_filter(contains='Command "put_messages" received from Dr Spock') async def test_command_validation_failure( mock_flow, caplog, flow_args, monkeypatch, ): """It should log command validation failures server side.""" caplog.set_level(logging.DEBUG, None) flow_args['workflows'].append( { 'user': mock_flow.owner, 'workflow': mock_flow.name, 'workflow_sel': None, } ) # submit a command with invalid arguments: async def submit_invalid_command(verbosity=0): monkeypatch.setattr('cylc.flow.flags.verbosity', verbosity) caplog.clear() return await mock_flow.resolvers.mutator( None, 'stop', flow_args, {'task': 'cycle/task/job', 'mode': 'not-a-mode'}, {}, ) # submitting the invalid command should result in this error msg = 'This command does not take job IDs: cycle/task/job' # test submitting the command at *default* verbosity response = await submit_invalid_command() # the error should be sent back to the client: assert response[0]['response'][1] == msg # it should also be logged by the server: assert caplog.records[-1].levelno == logging.WARNING assert msg in caplog.records[-1].message # test submitting the command at *debug* verbosity response = await submit_invalid_command(verbosity=2) # the error should be sent back to the client: assert response[0]['response'][1] == msg # it should be logged at the server assert caplog.records[-2].levelno == logging.WARNING assert msg in caplog.records[-2].message # the traceback should also be logged # (note traceback gets logged at the ERROR level and shows up funny in # caplog) assert caplog.records[-1].levelno == logging.ERROR assert msg in caplog.records[-1].message cylc-flow-8.6.4/tests/integration/network/test_graphql.py0000664000175000017500000003217315202510242024040 0ustar alastairalastair# 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 . """Test the top-level (root) GraphQL queries.""" from contextlib import suppress import pytest from typing import TYPE_CHECKING from graphql import parse, MiddlewareManager from cylc.flow.data_store_mgr import create_delta_store from cylc.flow.id import TaskTokens, Tokens from cylc.flow.network.client import WorkflowRuntimeClient from cylc.flow.network.schema import schema, SUB_RESOLVER_MAPPING from cylc.flow.network.graphql import ( CylcExecutionContext, IgnoreFieldMiddleware, instantiate_middleware, ) from cylc.flow.network.graphql_subscribe import subscribe from cylc.flow.workflow_status import get_workflow_status if TYPE_CHECKING: from cylc.flow.scheduler import Scheduler # NOTE: These tests mutate the data store, so running them in isolation may # see failures when they actually pass if you run the whole file def job_config(schd): return { 'owner': schd.owner, 'submit_num': 1, 'task_id': '1/foo', 'job_runner_name': 'background', 'env-script': None, 'err-script': None, 'exit-script': None, 'execution_time_limit': None, 'init-script': None, 'post-script': None, 'pre-script': None, 'script': 'sleep 5; echo "I come in peace"', 'work_d': None, 'directives': {}, 'environment': {}, 'param_var': {}, 'platform': {'name': 'platform'}, } def gather_subscription_args(schd, request_string): kwargs = { "variable_values": {}, "operation_name": None, "context_value": { 'op_id': 1, 'resolvers': schd.server.resolvers, 'meta': {}, }, "subscribe_resolver_map": SUB_RESOLVER_MAPPING, "middleware": MiddlewareManager( *list( instantiate_middleware( [IgnoreFieldMiddleware] ) ) ), "execution_context_class": CylcExecutionContext, } document = parse(request_string) return (document, kwargs) @pytest.fixture def job_db_row(): return [ '1', 'foo', 'running', 4, '2020-04-03T13:40:18+13:00', '2020-04-03T13:40:20+13:00', '2020-04-03T13:40:30+13:00', 'background', '20542', 'localhost', ] @pytest.fixture(scope='module') async def harness(mod_flow, mod_scheduler, mod_run): flow_def = { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'a => b & c => d' } }, 'runtime': { 'A': { }, 'B': { 'inherit': 'A', }, 'b': { 'inherit': 'B', }, }, } id_: str = mod_flow(flow_def) schd: 'Scheduler' = mod_scheduler(id_) async with mod_run(schd): client = WorkflowRuntimeClient(id_) schd.pool.hold_tasks({TaskTokens('*', 'root')}) schd.resume_workflow() # Think this is needed to save the data state at first start (?) # Fails without it.. and a test needs to overwrite schd data with this. # data = schd.data_store_mgr.data[schd.data_store_mgr.workflow_id] workflow_tokens = Tokens( user=schd.owner, workflow=schd.workflow, ) yield schd, client, workflow_tokens async def test_workflows(harness): schd, client, w_tokens = harness ret = await client.async_request( 'graphql', {'request_string': 'query { workflows { id } }'} ) assert ret == { 'workflows': [ { 'id': f'{w_tokens}' } ] } async def test_tasks(harness): schd, client, w_tokens = harness # query "tasks" ret = await client.async_request( 'graphql', {'request_string': 'query { tasks { id } }'} ) ids = [ w_tokens.duplicate(cycle=f'$namespace|{namespace}').id for namespace in ('a', 'b', 'c', 'd') ] ret['tasks'].sort(key=lambda x: x['id']) assert ret == { 'tasks': [ {'id': id_} for id_ in ids ] } # query "task" for id_ in ids: ret = await client.async_request( 'graphql', {'request_string': 'query { task(id: "%s") { id } }' % id_} ) assert ret == { 'task': {'id': id_} } async def test_families(harness): schd, client, w_tokens = harness # query "tasks" ret = await client.async_request( 'graphql', {'request_string': 'query { families { id } }'} ) ids = [ w_tokens.duplicate( cycle=f'$namespace|{namespace}' ).id for namespace in ('A', 'B', 'root') ] ret['families'].sort(key=lambda x: x['id']) assert ret == { 'families': [ {'id': id_} for id_ in ids ] } # query "task" for id_ in ids: ret = await client.async_request( 'graphql', {'request_string': 'query { family(id: "%s") { id } }' % id_} ) assert ret == { 'family': {'id': id_} } async def test_task_proxies(harness): schd, client, w_tokens = harness # query "tasks" ret = await client.async_request( 'graphql', {'request_string': 'query { taskProxies { id } }'} ) ids = [ w_tokens.duplicate( cycle='1', task=namespace, ) # NOTE: task "d" is not in the n=1 window yet for namespace in ('a', 'b', 'c') ] ret['taskProxies'].sort(key=lambda x: x['id']) assert ret == { 'taskProxies': [ {'id': id_.id} for id_ in ids ] } # query "task" ret = await client.async_request( 'graphql', {'request_string': 'query { taskProxy(id: "%s") { id } }' % ids[0].id} ) assert ret == { 'taskProxy': {'id': ids[0].id} } # query "taskProxies" fragment with null stripping ret = await client.async_request( 'graphql', { 'request_string': ''' fragment wf on Workflow { taskProxies (ids: ["%s"], stripNull: true) { id } } query { workflows (ids: ["%s"]) { ...wf } } ''' % (ids[0].relative_id, ids[0].workflow_id) } ) assert ret == {'workflows': [{'taskProxies': [{'id': ids[0].id}]}]} async def test_family_proxies(harness): schd, client, w_tokens = harness # query "familys" ret = await client.async_request( 'graphql', {'request_string': 'query { familyProxies { id } }'} ) ids = [ w_tokens.duplicate( cycle='1', task=namespace, ).id # NOTE: family "d" is not in the n=1 window yet for namespace in ('A', 'B', 'root') ] ret['familyProxies'].sort(key=lambda x: x['id']) assert ret == { 'familyProxies': [ {'id': id_} for id_ in ids ] } # query "family" for id_ in ids: ret = await client.async_request( 'graphql', {'request_string': 'query { familyProxy(id: "%s") { id } }' % id_} ) assert ret == { 'familyProxy': {'id': id_} } async def test_edges(harness): schd, client, w_tokens = harness t_tokens = [ w_tokens.duplicate( cycle='1', task=namespace, ) # NOTE: task "d" is not in the n=1 window yet for namespace in ('a', 'b', 'c') ] edges = [ (t_tokens[0], t_tokens[1]), (t_tokens[0], t_tokens[2]), ] e_ids = sorted([ w_tokens.duplicate( cycle=( '$edge' f'|{left.relative_id}' f'|{right.relative_id}' ) ).id for left, right in edges ]) # query "edges" ret = await client.async_request( 'graphql', {'request_string': 'query { edges { id } }'} ) ret['edges'].sort(key=lambda x: x['id']) assert ret == { 'edges': [ {'id': id_} for id_ in e_ids ] } # query "nodesEdges" ret = await client.async_request( 'graphql', {'request_string': 'query { nodesEdges { nodes {id}\nedges {id} } }'} ) ret['nodesEdges']['nodes'].sort(key=lambda x: x['id']) ret['nodesEdges']['edges'].sort(key=lambda x: x['id']) assert ret == { 'nodesEdges': { 'nodes': [ {'id': tokens.id} for tokens in t_tokens ], 'edges': [ {'id': id_} for id_ in e_ids ], }, } async def test_jobs(harness): schd: Scheduler schd, client, w_tokens = harness # add a job itask = schd.pool._get_task_by_id('1/a') schd.data_store_mgr.insert_job(itask, 'submitted', job_config(schd)) schd.data_store_mgr.update_data_structure() j_tokens = w_tokens.duplicate( cycle='1', task='a', job='01', ) j_id = j_tokens.id # query "jobs" ret = await client.async_request( 'graphql', {'request_string': 'query { jobs { id } }'} ) assert ret == { 'jobs': [ {'id': f'{j_id}'} ] } # query "job" ret = await client.async_request( 'graphql', {'request_string': 'query { job(id: "%s") { id } }' % j_id} ) assert ret == { 'job': {'id': f'{j_id}'} } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Test the GraphQL subscription infrastructure. # (currently only used at UIS) @pytest.mark.asyncio(loop_scope="module") async def test_subscription_basic(harness): """Test a basic subscription that uses the resolver's sub_resolver code.""" schd, _, w_tokens = harness document, kwargs = gather_subscription_args( schd, 'subscription { workflows { id } }', ) subscription = await subscribe( schema.graphql_schema, document, **kwargs ) has_item = False with suppress(GeneratorExit): async for response in subscription: has_item = True assert response.data['workflows'][0]['id'] == w_tokens.id await subscription.aclose() assert has_item @pytest.mark.asyncio(loop_scope="module") async def test_subscription_deltas(one, start): """Test the full subscription with null-stripping and delta handling.""" async with start(one): document, kwargs = gather_subscription_args( one, ''' subscription { deltas (stripNull: true) { id added { workflow { id host status } } updated { workflow { id host status } } } } ''', ) subscription = await subscribe( schema.graphql_schema, document, **kwargs ) aitem = await subscription.__anext__() assert aitem.data['deltas']['added']['workflow'] == { 'id': one.id, 'host': one.host, 'status': 'paused', } # Workflow one is paused on start await one.update_data_structure() assert ( one.data_store_mgr.data[one.id]['workflow'].status == get_workflow_status(one).value ) # Get the all delta, process, then add it to the subscription queue. btopic, delta, _ = one.data_store_mgr.publish_deltas[-1] _, sub_queue = next( iter(one.data_store_mgr.delta_queues[one.id].items()) ) sub_queue.put( ( one.id, btopic.decode('utf-8'), create_delta_store(delta, one.id) ) ) aitem = await subscription.__anext__() assert aitem.data['deltas']['updated']['workflow'] == { 'id': one.id, } with suppress(GeneratorExit): await subscription.aclose() cylc-flow-8.6.4/tests/integration/network/test_server.py0000664000175000017500000001165015202510242023705 0ustar alastairalastair# 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 . import asyncio import logging from typing import Callable import pytest from cylc.flow import __version__ as CYLC_VERSION from cylc.flow.network.server import PB_METHOD_MAP from cylc.flow.scheduler import Scheduler @pytest.fixture(scope='module') async def myflow(mod_flow, mod_scheduler, mod_run, mod_one_conf): id_ = mod_flow(mod_one_conf) schd = mod_scheduler(id_) async with mod_run(schd): yield schd def test_graphql(myflow): """Test GraphQL endpoint method.""" request_string = f''' query {{ workflows(ids: ["{myflow.id}"]) {{ id }} }} ''' data = myflow.server.graphql(request_string) assert myflow.id == data['workflows'][0]['id'] def test_graphql_error(myflow): """Test GraphQL endpoint method.""" request_string = f''' query {{ workflows(ids: ["{myflow.id}"]) {{ id notafield alsonotafield }} }} ''' with pytest.raises(Exception) as excinfo: myflow.server.graphql(request_string) assert "Cannot query field 'notafield'" in excinfo assert "Cannot query field 'alsonotafield'" in excinfo def test_pb_data_elements(myflow): """Test Protobuf elements endpoint method.""" element_type = 'workflow' data = PB_METHOD_MAP['pb_data_elements'][element_type]() data.ParseFromString( myflow.server.pb_data_elements(element_type) ) assert data.added.id == myflow.id def test_pb_entire_workflow(myflow): """Test Protobuf entire workflow endpoint method.""" data = PB_METHOD_MAP['pb_entire_workflow']() data.ParseFromString( myflow.server.pb_entire_workflow() ) assert data.workflow.id == myflow.id async def test_stop(one: Scheduler, start): """Test stop.""" async with start(one): async with asyncio.timeout(2): # Wait for the server to consume the STOP signal. # If it doesn't, the test will fail with a asyncio.timeout error. await one.server.stop('TESTING') assert one.server.stopped async def test_receiver_basic(one: Scheduler, start, log_filter): """Test the receiver with different message objects.""" async with asyncio.timeout(5): async with start(one): # start with a message that works msg = {'command': 'api', 'user': 'bono', 'args': {}} res = one.server.receiver(msg) assert not res.get('error') assert res['data'] assert res['cylc_version'] == CYLC_VERSION # simulate a command failure with the original message # (the one which worked earlier) - should error def _api(*args, **kwargs): raise Exception('oopsie') one.server.api = _api res = one.server.receiver(msg) assert res == { 'error': {'message': 'oopsie'}, 'cylc_version': CYLC_VERSION, } assert log_filter(logging.ERROR, 'oopsie') @pytest.mark.parametrize( 'msg, expected', [ pytest.param( {'user': 'bono', 'args': {}}, "Request missing field 'command' required for" f" Cylc {CYLC_VERSION}", id='missing-command', ), pytest.param( {'command': 'foobar', 'user': 'bono', 'args': {}}, f"No method by the name 'foobar' at Cylc {CYLC_VERSION}", id='bad-command', ), ], ) async def test_receiver_bad_requests(one: Scheduler, start, msg, expected): """Test the receiver with different bad requests.""" async with asyncio.timeout(5): async with start(one): res = one.server.receiver(msg) assert res == { 'error': {'message': expected}, 'cylc_version': CYLC_VERSION, } async def test_publish_before_shutdown( one: Scheduler, start: Callable ): """Test that the server publishes final deltas before shutting down.""" async with start(one): one.server.publish_queue.put([(b'fake', b'blah')]) await one.server.stop('i said stop!') assert not one.server.publish_queue.qsize() cylc-flow-8.6.4/tests/integration/network/test_client.py0000664000175000017500000001001215202510242023644 0ustar alastairalastair# 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 . """Test cylc.flow.client.WorkflowRuntimeClient.""" import json from unittest.mock import Mock import pytest from cylc.flow.exceptions import ClientError from cylc.flow.network.client import WorkflowRuntimeClient from cylc.flow.network.server import PB_METHOD_MAP @pytest.fixture(scope='module') async def harness(mod_flow, mod_scheduler, mod_run, mod_one_conf): id_ = mod_flow(mod_one_conf) schd = mod_scheduler(id_) async with mod_run(schd): client = WorkflowRuntimeClient(id_) yield schd, client async def test_graphql(harness): """It should return True if running.""" schd, client = harness ret = await client.async_request( 'graphql', {'request_string': 'query { workflows { id } }'} ) workflows = ret['workflows'] assert len(workflows) == 1 workflow = workflows[0] assert schd.workflow in workflow['id'] async def test_protobuf(harness): """It should return True if running.""" schd, client = harness ret = await client.async_request('pb_entire_workflow') pb_data = PB_METHOD_MAP['pb_entire_workflow']() pb_data.ParseFromString(ret) assert schd.workflow in pb_data.workflow.id async def test_command_validation_failure(harness): """It should send the correct response if a command fails validation. Command arguments are validated before the command is queued. Any issues at this stage will be communicated back via the mutation "result". See https://github.com/cylc/cylc-flow/pull/6112 """ schd, client = harness # run a mutation that will fail validation response = await client.async_request( 'graphql', { 'request_string': ''' mutation { set( workflows: ["*"], tasks: ["*"], # this list of prerequisites fails validation: prerequisites: ["1/a", "all"] ) { result } } ''' }, ) # the validation error should be returned to the client assert response['set']['result'] == [ { 'id': schd.id, 'response': [False, '--pre=all must be used alone'], } ] @pytest.mark.parametrize( 'sock_response, expected', [ pytest.param({'error': 'message'}, r"^message$", id="basic"), pytest.param( {'foo': 1}, r"^Received invalid response for" r" Cylc 8\.[\w.]+: \{'foo': 1[^}]*\}$", id="no-err-field", ), pytest.param( {'cylc_version': '8.x.y'}, r"^Received invalid.+\n\(Workflow is running in Cylc 8.x.y\)$", id="no-err-field-with-version", ), ], ) async def test_async_request_err( one, start, monkeypatch: pytest.MonkeyPatch, sock_response, expected ): async def mock_recv(): return json.dumps(sock_response).encode() async with start(one): client = WorkflowRuntimeClient(one.workflow) with monkeypatch.context() as mp: mp.setattr(client, 'socket', Mock(recv=mock_recv)) mp.setattr(client, 'poller', Mock()) with pytest.raises(ClientError, match=expected): await client.async_request('graphql') cylc-flow-8.6.4/tests/integration/network/test_scan.py0000664000175000017500000003271715202510242023332 0ustar alastairalastair# 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 . """Test file-system interaction aspects of scan functionality.""" from contextlib import suppress from pathlib import Path import re from shutil import rmtree from tempfile import TemporaryDirectory from typing import List import pytest from cylc.flow.network.scan import ( filter_name, graphql_query, is_active, scan, scan_multi, workflow_params, ) from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager from cylc.flow.workflow_files import WorkflowFiles SRV_DIR = Path(WorkflowFiles.Service.DIRNAME) CONTACT = Path(WorkflowFiles.Service.CONTACT) RUN_N = Path(WorkflowFiles.RUN_N) INSTALL = Path(WorkflowFiles.Install.DIRNAME) def init_flows(tmp_run_path=None, running=None, registered=None, un_registered=None, tmp_src_path=None, src=None): """Create some dummy workflows for scan to discover. Assume "run1, run2, ..., runN" structure if flow name constains "run". Optionally create workflow source dirs in a give location too. """ def make_registered(name, running=False): run_d = Path(tmp_run_path, name) run_d.mkdir(parents=True, exist_ok=True) (run_d / "flow.cylc").touch() if "run" in name: root = Path(tmp_run_path, name).parent with suppress(FileExistsError): (root / "runN").symlink_to(run_d, target_is_directory=True) else: root = run_d (root / INSTALL).mkdir(parents=True, exist_ok=True) srv_d = (run_d / SRV_DIR) srv_d.mkdir(parents=True, exist_ok=True) if running: (srv_d / CONTACT).touch() def make_src(name): src_d = Path(tmp_src_path, name) src_d.mkdir(parents=True, exist_ok=True) (src_d / "flow.cylc").touch() for name in (running or []): make_registered(name, running=True) for name in (registered or []): make_registered(name) for name in (un_registered or []): Path(tmp_run_path, name).mkdir(parents=True, exist_ok=True) for name in (src or []): make_src(name) @pytest.fixture(scope='session') def sample_run_dir(): tmp_path = Path(TemporaryDirectory().name) tmp_path.mkdir() init_flows( tmp_path, running=('foo', 'bar/pub', 'cheese/run2'), registered=('baz', 'cheese/run1'), un_registered=('qux',) ) yield tmp_path rmtree(tmp_path) @pytest.fixture def badly_messed_up_cylc_run_dir( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> Path: monkeypatch.setattr('cylc.flow.pathutil._CYLC_RUN_DIR', tmp_path) # one regular workflow init_flows( tmp_path, running=('foo',) ) # and an erroneous service dir at the top level for no reason Path(tmp_path, SRV_DIR).mkdir() return tmp_path @pytest.fixture(scope='session') def run_dir_with_symlinks(): tmp_path = Path(TemporaryDirectory().name) tmp_path.mkdir() # one regular workflow init_flows( tmp_path, running=('foo',) ) # one symlinked workflow tmp_path2 = Path(TemporaryDirectory().name) tmp_path2.mkdir() init_flows( tmp_path2, # make it nested to prove that the link is followed running=('bar/baz',) ) Path(tmp_path, 'bar').symlink_to(Path(tmp_path2, 'bar')) yield tmp_path rmtree(tmp_path) @pytest.fixture(scope='session') def run_dir_with_nasty_symlinks(): tmp_path = Path(TemporaryDirectory().name) tmp_path.mkdir() # one regular workflow init_flows( tmp_path, running=('foo',) ) # and a symlink pointing back at it in the same dir Path(tmp_path, 'bar').symlink_to(Path(tmp_path, 'foo')) yield tmp_path rmtree(tmp_path) @pytest.fixture(scope='session') def nested_dir(): tmp_path = Path(TemporaryDirectory().name) tmp_path.mkdir() init_flows( tmp_path, running=('a', 'b/c', 'd/e/f', 'g/h/i/j'), ) yield tmp_path rmtree(tmp_path) @pytest.fixture def source_dirs(mock_glbl_cfg): src = Path(TemporaryDirectory().name) src.mkdir() src1 = src / '1' src1.mkdir() init_flows( tmp_src_path=src1, src=('a', 'b/c') ) src2 = src / '2' src2.mkdir() init_flows( tmp_src_path=src2, src=('d', 'e/f') ) mock_glbl_cfg( 'cylc.flow.scripts.scan.glbl_cfg', f''' [install] source dirs = {src1}, {src2} ''' ) yield [src1, src2] rmtree(src) async def listify(async_gen, field='name'): """Convert an async generator into a list.""" ret = [] async for item in async_gen: ret.append(item[field]) ret.sort() return ret async def test_scan(sample_run_dir): """It should list all flows.""" assert await listify( scan(sample_run_dir) ) == [ 'bar/pub', 'baz', 'cheese/run1', 'cheese/run2', 'foo' ] async def test_scan_with_files(sample_run_dir): """It shouldn't be perturbed by arbitrary files.""" Path(sample_run_dir, 'abc').touch() Path(sample_run_dir, 'def').touch() assert await listify( scan(sample_run_dir) ) == [ 'bar/pub', 'baz', 'cheese/run1', 'cheese/run2', 'foo', ] async def test_scan_horrible_mess(badly_messed_up_cylc_run_dir): """It shouldn't be affected by erroneous cylc files/dirs. How could you end up with a .service dir in ~/cylc-run? Well misuse of Cylc7 can result in this situation so this test ensures Cylc7 workflows can't mess up a Cylc8 scan. """ assert await listify( scan(badly_messed_up_cylc_run_dir) ) == [ 'foo' ] async def test_scan_symlinks(run_dir_with_symlinks): """It should follow symlinks to flows in other dirs.""" assert await listify( scan(run_dir_with_symlinks) ) == [ 'bar/baz', 'foo' ] async def test_scan_nasty_symlinks(run_dir_with_nasty_symlinks): """It should handle strange symlinks because users can be nasty.""" assert await listify( scan(run_dir_with_nasty_symlinks) ) == [ 'bar', # well you got what you asked for 'foo' ] async def test_scan_non_exist(tmp_path: Path): """Calling scan() on a scan_dir that doesn't exist should not raise.""" assert await listify( scan(scan_dir=(tmp_path / 'HORSE')) ) == [] async def test_is_active(sample_run_dir): """It should filter flows by presence of a contact file.""" # running flows assert await is_active.func( {'path': sample_run_dir / 'foo'}, True ) assert await is_active.func( {'path': sample_run_dir / 'bar/pub'}, True ) # registered flows assert not await is_active.func( {'path': sample_run_dir / 'baz'}, True ) # unregistered flows assert not await is_active.func( {'path': sample_run_dir / 'qux'}, True ) # non-existent flows assert not await is_active.func( {'path': sample_run_dir / 'elephant'}, True ) @pytest.mark.parametrize( 'depth, expected', [ (1, ['a']), (3, ['a', 'b/c', 'd/e/f']) ] ) async def test_max_depth(nested_dir, depth: int, expected: List[str]): """It should descend only as far as permitted.""" assert await listify( scan(nested_dir, max_depth=depth) ) == expected async def test_max_depth_configurable(nested_dir, mock_glbl_cfg): """Default scan depth should be configurable in global.cylc.""" mock_glbl_cfg( 'cylc.flow.network.scan.glbl_cfg', ''' [install] max depth = 2 ''' ) assert await listify( scan(nested_dir) ) == [ 'a', 'b/c', ] async def test_scan_one(one, start, test_dir): """Ensure that a running workflow appears in the scan results.""" async with start(one): pipe = ( # scan just this workflow scan(scan_dir=test_dir) | filter_name(rf'^{re.escape(one.workflow)}$') | is_active(True) | workflow_params ) async for flow in pipe: assert flow['name'] == one.workflow break else: raise Exception('Expected one scan result') async def test_workflow_params( one, start, one_conf, run_dir, mod_test_dir ): """It should extract workflow params from the workflow database. Note: For this test we ensure that the workflow UUID is present in the params table. """ async with start(one): pipe = ( # scan just this workflow scan(scan_dir=mod_test_dir) | filter_name(rf'^{re.escape(one.workflow)}$') | is_active(True) | workflow_params ) async for flow in pipe: # check the workflow_params field has been provided assert 'workflow_params' in flow # check the workflow uuid key has been read from the DB uuid_key = WorkflowDatabaseManager.KEY_UUID_STR assert uuid_key in flow['workflow_params'] # check the workflow uuid key matches the scheduler value assert flow['workflow_params'][uuid_key] == one.uuid_str break else: raise Exception('Expected one scan result') async def test_source_dirs(source_dirs): """It should list uninstalled workflows from configured source dirs.""" src1, src2 = source_dirs assert await listify( scan_multi(source_dirs, max_depth=3) ) == [ # NOTE: flow names from scan_multi are full paths (src1 / 'a'), (src1 / 'b/c'), (src2 / 'd'), (src2 / 'e/f'), ] async def test_scan_sigstop( flow, scheduler, start, one_conf, test_dir, caplog, ): """It should log warnings if workflows are un-contactable. Note: This replaces tests/functional/cylc-scan/02-sigstop.t last found in Cylc Flow 8.0a2 which used sigstop to make the flow unresponsive. """ # run a workflow id_ = flow(one_conf) schd = scheduler(id_) async with start(schd): # stop the server to make the flow un-responsive await schd.server.stop('make-unresponsive') # try scanning the workflow pipe = scan(test_dir) | graphql_query(['status']) caplog.clear() async for flow in pipe: raise Exception("There shouldn't be any scan results") # there should, however, be a warning name = Path(id_).name assert ( (30, f'Workflow not running: {name}') in [(level, msg) for _, level, msg in caplog.record_tuples] ) @pytest.fixture def cylc7_run_dir(tmp_path): """A run directory containing three Cylc 7 workflows.""" # a workflow that has not yet been run # (could be run by either cylc 7 or 8 so should appear in scan results) either = tmp_path / 'either' either.mkdir() (either / WorkflowFiles.SUITE_RC).touch() # a Cylc 7 workflow that has been / is being run by Cylc 7 # (should not appear in scan results) cylc7 = tmp_path / 'cylc7' cylc7.mkdir() (cylc7 / WorkflowFiles.SUITE_RC).touch() Path(cylc7, WorkflowFiles.LogDir.DIRNAME, 'suite').mkdir(parents=True) Path(cylc7, WorkflowFiles.LogDir.DIRNAME, 'suite', 'log').touch() # a Cylc 7 workflow running under Cylc 8 in compatibility mode # (should appear in scan results) cylc8 = tmp_path / 'cylc8' cylc8.mkdir() (cylc8 / WorkflowFiles.SUITE_RC).touch() Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'scheduler').mkdir(parents=True) Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'scheduler', 'log').touch() # a Cylc 7 workflow installed by Cylc 8 but not run yet. # (should appear in scan results) cylc8a = tmp_path / 'cylc8a' cylc8a.mkdir() (cylc8a / WorkflowFiles.SUITE_RC).touch() Path(cylc8a, WorkflowFiles.LogDir.DIRNAME, 'install').mkdir(parents=True) # crazy niche case of a Cylc 7 workflow that has had its DB removed # and re-run under Cylc 8 # (should appear in scan results) cylc8 = tmp_path / 'cylc78' cylc8.mkdir() (cylc8 / WorkflowFiles.SUITE_RC).touch() Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'suite').mkdir(parents=True) Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'suite', 'log').touch() Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'scheduler').mkdir(parents=True) Path(cylc8, WorkflowFiles.LogDir.DIRNAME, 'scheduler', 'log').touch() return tmp_path async def test_scan_cylc7(cylc7_run_dir): """It should exclude Cylc 7 workflows from scan results. Unless they are running under Cylc 8 in Cylc 7 compatibility mode. """ assert await listify( scan(cylc7_run_dir) ) == [ 'cylc78', 'cylc8', 'cylc8a', 'either' ] cylc-flow-8.6.4/tests/integration/network/test_replier.py0000664000175000017500000000364315202510242024044 0ustar alastairalastair# 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 . import asyncio import pytest from cylc.flow import __version__ as CYLC_VERSION from cylc.flow.network import deserialize from cylc.flow.network.client import WorkflowRuntimeClient from cylc.flow.scheduler import Scheduler async def test_listener(one: Scheduler, start): """Test listener.""" async with start(one): # Test listener handles an invalid message from client # (without directly calling listener): client = WorkflowRuntimeClient(one.workflow) client.socket.send_string(r'Not JSON') res = deserialize( (await client.socket.recv()).decode() ) assert res['error'] assert 'data' not in res # Check other fields are present: assert res['cylc_version'] == CYLC_VERSION one.server.replier.queue.put('STOP') async with asyncio.timeout(2): # wait for the server to consume the STOP item from the queue while not one.server.replier.queue.empty(): await asyncio.sleep(0.01) # ensure the server is "closed" one.server.replier.queue.put('foobar') with pytest.raises(ValueError): one.server.replier.listener() cylc-flow-8.6.4/tests/integration/network/test_publisher.py0000664000175000017500000000334715202510242024400 0ustar alastairalastair# 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 . import asyncio from cylc.flow.network.subscriber import ( WorkflowSubscriber, process_delta_msg, ) async def test_publisher(flow, scheduler, run, one_conf, port_range): """It should publish deltas when the flow starts.""" id_ = flow(one_conf) schd = scheduler(id_, paused_start=False) async with run(schd): # create a subscriber subscriber = WorkflowSubscriber( schd.workflow, host=schd.host, port=schd.server.pub_port, topics=[b'workflow'] ) async with asyncio.timeout(2): # wait for the first delta from the workflow btopic, msg = await subscriber.socket.recv_multipart() _, delta = process_delta_msg(btopic, msg, None) for key in ('added', 'updated'): if getattr(getattr(delta, key), 'id', None): assert schd.id == getattr(delta, key).id break else: raise Exception("Delta wasn't added or updated") cylc-flow-8.6.4/tests/integration/network/key_setup.py0000664000175000017500000000364715202510242023357 0ustar alastairalastair# THIS FILE IS PART OF THE CYLC WORKFLOW 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 . """Creates authentication keys for use in testing""" from cylc.flow.workflow_files import ( create_server_keys, get_workflow_srv_dir, KeyInfo, KeyOwner, KeyType, remove_keys_on_server) from cylc.flow.task_remote_cmd import ( remove_keys_on_client, create_client_keys) def setup_keys(workflow_name): workflow_srv_dir = get_workflow_srv_dir(workflow_name) server_keys = { "client_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.CLIENT, workflow_srv_dir=workflow_srv_dir), "client_private_key": KeyInfo( KeyType.PRIVATE, KeyOwner.CLIENT, workflow_srv_dir=workflow_srv_dir), "server_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.SERVER, workflow_srv_dir=workflow_srv_dir), "server_private_key": KeyInfo( KeyType.PRIVATE, KeyOwner.SERVER, workflow_srv_dir=workflow_srv_dir) } remove_keys_on_server(server_keys) remove_keys_on_client(workflow_srv_dir, None, full_clean=True) create_server_keys(server_keys, workflow_srv_dir) create_client_keys(workflow_srv_dir, None) cylc-flow-8.6.4/tests/integration/test_increment_graph_window.py0000664000175000017500000003231015202510242025436 0ustar alastairalastair# 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 . from contextlib import suppress from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.data_store_mgr import ( TASK_PROXIES, ) from cylc.flow.id import Tokens def increment_graph_window(schd, task): """Increment the graph window about the active task.""" tokens = schd.tokens.duplicate(cycle='1', task=task) schd.data_store_mgr.increment_graph_window( tokens, IntegerPoint('1'), is_manual_submit=False, ) def get_deltas(schd): """Return the ids and graph-window values in the delta store. Note, call before get_n_window as this clears the delta store. Returns: (added, updated, pruned) """ # populate added deltas schd.data_store_mgr.gather_delta_elements( schd.data_store_mgr.added, 'added', ) # populate pruned deltas schd.data_store_mgr.prune_data_store() # Run depth finder schd.data_store_mgr.window_depth_finder() # populate updated deltas schd.data_store_mgr.gather_delta_elements( schd.data_store_mgr.updated, 'updated', ) return ( { # added Tokens(tb_task_proxy.id)['task']: tb_task_proxy.graph_depth for tb_task_proxy in schd.data_store_mgr.deltas[TASK_PROXIES].added }, { # updated Tokens(tb_task_proxy.id)['task']: tb_task_proxy.graph_depth for tb_task_proxy in schd.data_store_mgr.deltas[ TASK_PROXIES ].updated # only include those updated nodes whose depths have been set if 'graph_depth' in {sub_field.name for sub_field, _ in tb_task_proxy.ListFields()} }, { # pruned Tokens(id_)['task'] for id_ in schd.data_store_mgr.deltas[TASK_PROXIES].pruned }, ) async def get_n_window(schd): """Read out the graph window of the workflow.""" await schd.update_data_structure() data = schd.data_store_mgr.data[schd.data_store_mgr.workflow_id] return { t.name: t.graph_depth for t in data[TASK_PROXIES].values() } async def complete_task(schd, task): """Mark a task as completed.""" schd.data_store_mgr.remove_pool_node(task, IntegerPoint('1')) def add_task(schd, task): """Add a waiting task to the pool.""" schd.data_store_mgr.add_pool_node(task, IntegerPoint('1')) def get_graph_walk_cache(schd): """Return the head task names of cached graph walks.""" # prune graph walk cache schd.data_store_mgr.prune_data_store() # fetch the cached walks n_window_node_walks = sorted( Tokens(task_id)['task'] for task_id in schd.data_store_mgr.n_window_node_walks ) n_window_completed_walks = sorted( Tokens(task_id)['task'] for task_id in schd.data_store_mgr.n_window_completed_walks ) # the IDs in set and keys of dict are only the same at n<2 window. assert n_window_node_walks == n_window_completed_walks return n_window_completed_walks async def test_increment_graph_window_blink(flow, scheduler, start): """Test with a task which drifts in and out of the n-window. This workflow presents a fiendish challenge for the graph window algorithm. The test runs in the n=3 window and simulates running each task in the chain a - s one by one. The task "blink" is dependent on multiple tasks in the chain awkwardly spaced so that the "blink" task routinely disappears from the n-window, only to re-appear again later. The expansion of the window around the "blink" task is difficult to get right as it can be influenced by caches from previous graph walks. """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'True', }, 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': '1', 'graph': { 'R1': ''' # the "abdef" chain of tasks which run one after another a => b => c => d => e => f => g => h => i => j => k => l => m => n => o => p => q => r => s # these dependencies cause "blink" to disappear and # reappear at set intervals a => blink g => blink m => blink s => blink ''', } } }) schd = scheduler(id_) # the tasks traversed via the "blink" task when... blink = { 1: { # the blink task is n=1 'blink': 1, 'a': 2, 'g': 2, 'm': 2, 's': 2, 'b': 3, 'f': 3, 'h': 3, 'l': 3, 'n': 3, 'r': 3, }, 2: { # the blink task is n=2 'blink': 2, 'a': 3, 'g': 3, 'm': 3, 's': 3, }, 3: { # the blink task is n=3 'blink': 3, }, 4: { # the blink task is n=4 }, } def advance(): """Advance to the next task in the workflow. This works its way down the chain of tasks between "a" and "s" inclusive, yielding what the n-window should look like for this workflow at each step. Yields: tuple - (previous_task, active_task, n_window) previous_task: The task which has just "succeeded". active_task: The task which is about to run. n_window: Dictionary of {task_name: graph_depth} for the n=3 window. """ # the initial window on startup (minus the nodes traversed via "blink") window = { 'a': 0, 'b': 1, 'c': 2, 'd': 3, } # the tasks we will run in order letters = 'abcdefghijklmnopqrs' # the graph-depth of the "blink" task at each stage of the workflow blink_distances = [1] + [*range(2, 5), *range(3, 0, -1)] * 3 for ind, blink_distance in zip(range(len(letters)), blink_distances): previous_task = letters[ind - 1] if ind > 0 else None active_task = letters[ind] yield ( previous_task, active_task, { # the tasks traversed via the "blink" task **blink[blink_distance], # the tasks in the main "abcdefg" chain **{key: abs(value) for key, value in window.items()}, } ) # move each task in the "abcdef" chain down one window = {key: value - 1 for key, value in window.items()} # add the n=3 task in the "abcdef" chain into the window with suppress(IndexError): window[letters[ind + 4]] = 3 # pull out anything which is not supposed to be in the n=3 window window = { key: value for key, value in window.items() if abs(value) < 4 } async with start(schd): schd.data_store_mgr.set_graph_window_extent(3) await schd.update_data_structure() previous_n_window = {} for previous_task, active_task, expected_n_window in advance(): # mark the previous task as completed await complete_task(schd, previous_task) # add the next task to the pool add_task(schd, active_task) # run the graph window algorithm increment_graph_window(schd, active_task) # get the deltas which increment_graph_window created added, updated, pruned = get_deltas(schd) # compare the n-window in the store to what we were expecting n_window = await get_n_window(schd) assert n_window == expected_n_window # compare the deltas to what we were expecting if active_task != 'a': # skip the first task as this is complicated by startup logic assert added == { key: value for key, value in expected_n_window.items() if key not in previous_n_window } # Skip added as depth isn't updated # (the manager only updates those that need it) assert updated == { key: value for key, value in expected_n_window.items() if key not in added } assert pruned == { key for key in previous_n_window if key not in expected_n_window } previous_n_window = n_window async def test_window_resize_rewalk(flow, scheduler, start): """The window resize method should wipe and rebuild the n-window.""" id_ = flow({ 'scheduler': { 'allow implicit tasks': 'true', }, 'scheduling': { 'graph': { 'R1': 'a => b => c => d => e => f => g' } }, }) schd = scheduler(id_) async with start(schd): # start with an empty pool schd.pool.remove(schd.pool.get_tasks()[0]) # the n-window should be empty assert await get_n_window(schd) == {} # expand the window around 1/d add_task(schd, 'd') increment_graph_window(schd, 'd') # set the graph window to n=3 schd.data_store_mgr.set_graph_window_extent(3) assert set(await get_n_window(schd)) == { 'a', 'b', 'c', 'd', 'e', 'f', 'g' } # set the graph window to n=1 schd.data_store_mgr.set_graph_window_extent(1) schd.data_store_mgr.window_resize_rewalk() assert set(await get_n_window(schd)) == { 'c', 'd', 'e' } # set the graph window to n=2 schd.data_store_mgr.set_graph_window_extent(2) schd.data_store_mgr.window_resize_rewalk() assert set(await get_n_window(schd)) == { 'b', 'c', 'd', 'e', 'f' } async def test_cache_pruning(flow, scheduler, start): """It should remove graph walks from the cache when no longer needed. The algorithm caches graph walks for efficiency. This test is designed to ensure we don't introduce a memory leak by failing to clear cached walks at the correct point. """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'True', }, 'scheduling': { 'graph': { 'R1': ''' # a chain of tasks a => b1 & b2 => c => d1 & d2 => e => f # force "a" to drift into an out of the window a => c a => e ''' } }, }) schd = scheduler(id_) async with start(schd): schd.data_store_mgr.set_graph_window_extent(1) # work through this workflow, step by step checking the cached items... # active: a add_task(schd, 'a') increment_graph_window(schd, 'a') assert get_graph_walk_cache(schd) == ['a'] # active: b1, b2 await complete_task(schd, 'a') add_task(schd, 'b1') add_task(schd, 'b2') increment_graph_window(schd, 'b1') increment_graph_window(schd, 'b2') assert get_graph_walk_cache(schd) == ['a', 'b1', 'b2'] # active: c await complete_task(schd, 'b1') await complete_task(schd, 'b2') add_task(schd, 'c') increment_graph_window(schd, 'c') assert get_graph_walk_cache(schd) == ['a', 'b1', 'b2', 'c'] # active: d1, d2 await complete_task(schd, 'c') add_task(schd, 'd1') add_task(schd, 'd2') increment_graph_window(schd, 'd1') increment_graph_window(schd, 'd2') assert get_graph_walk_cache(schd) == ['c', 'd1', 'd2'] # active: e await complete_task(schd, 'd1') await complete_task(schd, 'd2') add_task(schd, 'e') increment_graph_window(schd, 'e') assert get_graph_walk_cache(schd) == ['d1', 'd2', 'e'] # active: f await complete_task(schd, 'e') add_task(schd, 'f') increment_graph_window(schd, 'f') assert get_graph_walk_cache(schd) == ['e', 'f'] # active: None await complete_task(schd, 'f') increment_graph_window(schd, 'f') assert get_graph_walk_cache(schd) == [] cylc-flow-8.6.4/tests/integration/test_workflow_files.py0000664000175000017500000002216615202510242023746 0ustar alastairalastair# 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 . from itertools import product import logging from os import unlink from pathlib import Path from textwrap import dedent from uuid import uuid4 import pytest from cylc.flow import CYLC_LOG from cylc.flow.exceptions import ( SchedulerAlive, CylcError, ) from cylc.flow.workflow_files import ( ContactFileFields as CFF, WorkflowFiles, _is_process_running, detect_old_contact_file, dump_contact_file, load_contact_file, load_contact_file_async, ) @pytest.fixture(scope='module') async def myflow(mod_flow, mod_scheduler, mod_run, mod_one_conf): id_ = mod_flow(mod_one_conf) schd = mod_scheduler(id_) async with mod_run(schd): yield schd def test_load_contact_file(myflow): cont = load_contact_file(myflow.workflow) assert cont[CFF.HOST] == myflow.host async def test_load_contact_file_async(myflow): cont = await load_contact_file_async(myflow.workflow) assert cont[CFF.HOST] == myflow.host # compare the async interface to the sync interface cont2 = load_contact_file(myflow.workflow) assert cont == cont2 @pytest.fixture async def workflow(flow, scheduler, one_conf, run_dir): id_ = flow(one_conf) schd = scheduler(id_) await schd.install() from collections import namedtuple Server = namedtuple('Server', ['port', 'pub_port']) schd.server = Server(1234, pub_port=2345) schd.uuid_str = str(uuid4()) contact_data = schd.get_contact_data() contact_file = Path( run_dir, id_, WorkflowFiles.Service.DIRNAME, WorkflowFiles.Service.CONTACT ) def dump_contact(**kwargs): dump_contact_file( id_, { **contact_data, **kwargs } ) assert contact_file.exists() dump_contact() Fixture = namedtuple( 'TextFixture', [ 'id_', 'contact_file', 'contact_data', 'dump_contact', ] ) return Fixture(id_, contact_file, contact_data, dump_contact) def test_detect_old_contact_file_running(workflow): """It should raise an error if the workflow is running.""" # the workflow is running so we should get a ServiceFileError with pytest.raises(SchedulerAlive): detect_old_contact_file(workflow.id_) # the contact file is valid so should be left alone assert workflow.contact_file.exists() def test_detect_old_contact_file_network_issue(workflow): """It should raise an error if there are network issues.""" # modify the contact file to make it look like the PID has changed workflow.dump_contact( **{ # set the HOST to a non existent host CFF.HOST: 'not-a-host.no-such.domain' } ) # detect_old_contact_file should report that it can't tell if the workflow # is running or not with pytest.raises(CylcError) as exc_ctx: detect_old_contact_file(workflow.id_) assert ( 'Cannot determine whether workflow is running' in str(exc_ctx.value) ) # the contact file should be left alone assert workflow.contact_file.exists() def test_detect_old_contact_file_old_run(workflow, caplog, log_filter): """It should remove the contact file from an old run.""" # modify the contact file to make it look like the COMMAND has changed workflow.dump_contact( **{ CFF.COMMAND: 'foo bar baz' } ) caplog.set_level(logging.INFO, logger=CYLC_LOG) # the workflow should not appear to be running (according to the contact # data) so detect_old_contact_file should not raise any errors detect_old_contact_file(workflow.id_) # as a side effect the contact file should have been removed assert not workflow.contact_file.exists() assert log_filter(contains='Removed contact file') def test_detect_old_contact_file_none(workflow): """It should do nothing if there is no contact file.""" # remove the contact file workflow.contact_file.unlink() assert not workflow.contact_file.exists() # detect_old_contact_file should return detect_old_contact_file(workflow.id_) # it should not recreate the contact file assert not workflow.contact_file.exists() @pytest.mark.parametrize( 'process_running,contact_present_after,raises_error', filter( lambda x: x != (False, False, True), # logically impossible product([True, False], repeat=3), ) ) def test_detect_old_contact_file_removal_errors( workflow, monkeypatch, caplog, log_filter, process_running, contact_present_after, raises_error, ): """Test issues with removing the contact file are handled correctly. Args: process_running: If True we will make it look like the workflow process is still running (i.e. the workflow is still running). In this case detect_old_contact_file should *not* attempt to remove the contact file. contact_present_after: If False we will make the contact file disappear midway through the operation. This can happen because: * detect_old_contact_file in another client. * cylc clean. * Aliens. This is fine, nothing should be logged. raises_error: If True we will make it look like removing the contact file resulted in an OS error (not a FileNotFoundError). This error should be logged. """ # patch the is_process_running method def mocked_is_process_running(*args): if not contact_present_after: # remove the contact file midway through detect_old_contact_file unlink(workflow.contact_file) return process_running monkeypatch.setattr( 'cylc.flow.workflow_files._is_process_running', mocked_is_process_running, ) # patch the contact file removal def _unlink(*args): raise OSError('mocked-os-error') if raises_error: # force os.unlink to raise an arbitrary error monkeypatch.setattr( 'cylc.flow.workflow_files.os.unlink', _unlink, ) caplog.set_level(logging.INFO, logger=CYLC_LOG) # try to remove the contact file if process_running: # this should error if the process is running with pytest.raises(SchedulerAlive): detect_old_contact_file(workflow.id_) else: detect_old_contact_file(workflow.id_) # decide which log messages we should expect to see if process_running: remove_succeeded = False remove_failed = False else: if contact_present_after: if raises_error: remove_succeeded = False remove_failed = True else: remove_succeeded = True remove_failed = False else: remove_succeeded = False remove_failed = False # check the appropriate messages were logged assert bool(log_filter( contains='Removed contact file', )) is remove_succeeded assert bool(log_filter( contains=( f'Failed to remove contact file for {workflow.id_}:' '\nmocked-os-error' ), )) is remove_failed def test_is_process_running_dirty_output(monkeypatch, caplog): """Ensure _is_process_running can handle polluted output. E.G. this can happen if there is an echo statement in the `.bashrc`. """ stdout = None class Popen(): def __init__(self, *args, **kwargs): self.returncode = 0 def communicate(self, *args, **kwargs): return (stdout, '') monkeypatch.setattr( 'cylc.flow.workflow_files.Popen', Popen, ) # respond with something Cylc should be able to make sense of stdout = dedent(''' % simulated stdout pollution % [["expected", "command"]] ''') caplog.set_level(logging.WARN, logger=CYLC_LOG) assert _is_process_running('localhost', 1, 'expected command') assert not caplog.record_tuples assert not _is_process_running('localhost', 1, 'slartibartfast') assert not caplog.record_tuples # respond with something totally non-sensical stdout = 'sir henry' with pytest.raises(CylcError): _is_process_running('localhost', 1, 'expected command') # the command output should be in the debug message assert 'sir henry' in caplog.records[0].message cylc-flow-8.6.4/tests/integration/test_task_remote_mgr.py0000664000175000017500000001436715202510242024100 0ustar alastairalastair# 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 . import cylc from cylc.flow.task_remote_mgr import ( REMOTE_FILE_INSTALL_DONE, REMOTE_FILE_INSTALL_FAILED ) async def test_remote_tidy( flow, scheduler, start, mock_glbl_cfg, one_conf, monkeypatch ): """Remote tidy gets platforms for install targets. In particular, referencing https://github.com/cylc/cylc-flow/issues/5429, ensure that install targets defined implicitly by platform name are found. Mock remote init map: - Include an install target (quiz) with message != REMOTE_FILE_INSTALL_DONE to ensure that this is picked out. - Install targets where we can get a platform - foo - Install target is implicitly the platfrom name. - bar9 - The install target is implicitly the plaform name, and the platform name matches a platform regex. - baz - Install target is set explicitly. - An install target (qux) where we cannot get a platform: Ensure that we get the desired error. Test that platforms with no good hosts (no host not in bad hosts). """ # Monkeypatch away subprocess.Popen calls - prevent any interaction with # remotes actually happening: class MockProc: def __init__(self, *args, **kwargs): self.poll = lambda: True if ( 'baum' in args[0] or 'bay' in args[0] ): self.returncode = 255 else: self.returncode = 0 self.communicate = lambda: ('out', 'err') monkeypatch.setattr( cylc.flow.task_remote_mgr, 'Popen', lambda *args, **kwargs: MockProc(*args, **kwargs) ) # Monkeypath function to add a sort order which we don't need in the # real code but rely to prevent the test becoming flaky: def mock_get_install_target_platforms_map(*args, **kwargs): """Add sort to original function to ensure test consistency""" from cylc.flow.platforms import get_install_target_to_platforms_map result = get_install_target_to_platforms_map(*args, **kwargs) sorted_result = {} for key in sorted(result): sorted_result[key] = sorted( result[key], key=lambda x: x['name'], reverse=True) return sorted_result monkeypatch.setattr( cylc.flow.task_remote_mgr, 'get_install_target_to_platforms_map', mock_get_install_target_platforms_map ) # Set up global config mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[foo]] # install target = foo (implicit) # hosts = foo (implicit) [[bar.]] # install target = bar1 to bar9 (implicit) # hosts = bar1 to bar9 (implicit) [[baz]] install target = baz # baum and bay should be uncontactable: hosts = baum, bay, baz [[[selection]]] method = definition order [[notthisone]] install target = baz hosts = baum, bay [[bay]] ''', ) # Get a scheduler: id_ = flow(one_conf) schd = scheduler(id_) async with start(schd) as log: # Write database with 6 tasks using 3 platforms: platforms = ['baz', 'bar9', 'foo', 'notthisone', 'bay'] line = r"('', '', {}, 0, 1, '', '', 0,'', '', '', 0, '', '{}', 4, '')" stmt = r"INSERT INTO task_jobs VALUES" + r','.join([ line.format(i, platform) for i, platform in enumerate(platforms) ]) schd.workflow_db_mgr.pri_dao.connect().execute(stmt) schd.workflow_db_mgr.pri_dao.connect().commit() # Mock a remote init map. schd.task_job_mgr.task_remote_mgr.remote_init_map = { 'baz': REMOTE_FILE_INSTALL_DONE, # Should match platform baz 'bar9': REMOTE_FILE_INSTALL_DONE, # Should match platform bar. 'foo': REMOTE_FILE_INSTALL_DONE, # Should match plaform foo 'qux': REMOTE_FILE_INSTALL_DONE, # Should not match a plaform 'quiz': REMOTE_FILE_INSTALL_FAILED, # Should not be considered 'bay': REMOTE_FILE_INSTALL_DONE, # Should return NoPlatforms } # Clear the log, run the test: log.clear() schd.task_job_mgr.task_remote_mgr.bad_hosts.update(['baum', 'bay']) schd.task_job_mgr.task_remote_mgr.remote_tidy() pass records = [str(r.msg) for r in log.records] # We can't get qux, no defined platform has a matching install target: qux_msg = 'No platforms available to remote tidy install targets:\n * qux' assert qux_msg in records # We can get foo bar baz, and we try to remote tidy them. # (This will ultimately fail, but past the point we are testing). for target in ['foo', 'bar9', 'baz']: msg = f'platform: {target} - remote tidy (on {target})' assert msg in records # We haven't done anything with Quiz because we're only looking # at cases where platform == REMOTE_FILE_INSTALL_DONE assert not [r for r in records if 'quiz' in r] notthisone_msg = ( 'platform: notthisone - clean up did not complete' '\nUnable to find valid host for notthisone' ) assert notthisone_msg in records bay_msg = ( 'Unable to find a platform from install target' ' bay during remote tidy.') assert bay_msg in records cylc-flow-8.6.4/tests/integration/scripts/0000775000175000017500000000000015202510242020761 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/scripts/test_list.py0000664000175000017500000002156615202510242023357 0ustar alastairalastair# 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 . """Test the "cylc list" command.""" import pytest from cylc.flow.exceptions import InputError from cylc.flow.option_parsers import Options from cylc.flow.scripts.list import ( get_option_parser, _main, ) ListOptions = Options(get_option_parser()) @pytest.fixture(scope='module') async def cylc_list(mod_flow, mod_scheduler, mod_start): id_ = mod_flow( { 'scheduling': { # NOTE: all "a*" tasks are in the graph, but not "b*" tasks 'initial cycle point': 1, 'cycling mode': 'integer', 'graph': {'P1': 'a12 & a111 & a112 & a121 & a2'}, }, 'runtime': { 'A': {'meta': {'title': 'Title For A'}}, 'A1': {'inherit': 'A'}, 'A11': {'inherit': 'A1', 'meta': {'title': 'Title For A11'}}, 'A12': {'inherit': 'A1'}, 'a2': {'inherit': 'A'}, 'a12': {'inherit': 'A1'}, 'a111': {'inherit': 'A11'}, 'a112': {'inherit': 'A11'}, 'a121': {'inherit': 'A12'}, 'B': {}, 'b1': {'inherit': 'B'}, }, } ) schd = mod_scheduler(id_) async def _list(capsys, **kwargs): capsys.readouterr() await _main(ListOptions(**kwargs), schd.workflow) out, err = capsys.readouterr() return out.splitlines() async with mod_start(schd): yield _list @pytest.fixture def supports_utf8(monkeypatch): monkeypatch.setenv('LANG', 'C.utf-8') @pytest.fixture def does_not_support_utf8(monkeypatch): monkeypatch.setenv('LANG', 'C') async def test_plain(cylc_list, supports_utf8, capsys): """Test the default output format.""" assert await cylc_list(capsys) == [ 'a111', 'a112', 'a12', 'a121', 'a2', ] assert await cylc_list(capsys, all_tasks=True) == [ 'a111', 'a112', 'a12', 'a121', 'a2', 'b1', # <= in the runtime but not in the graph ] assert await cylc_list(capsys, all_namespaces=True) == [ 'A', 'A1', 'A11', 'A12', 'B', 'a111', 'a112', 'a12', 'a121', 'a2', 'b1', 'root', ] with pytest.raises(InputError): await cylc_list(capsys, all_tasks=True, all_namespaces=True) async def test_mro(cylc_list, supports_utf8, capsys): """Test the --mro (method resolution order) option.""" assert await cylc_list(capsys, mro=True) == [ 'a111 a111 A11 A1 A root', 'a112 a112 A11 A1 A root', 'a12 a12 A1 A root', 'a121 a121 A12 A1 A root', 'a2 a2 A root', ] assert await cylc_list(capsys, mro=True, all_tasks=True) == [ 'a111 a111 A11 A1 A root', 'a112 a112 A11 A1 A root', 'a12 a12 A1 A root', 'a121 a121 A12 A1 A root', 'a2 a2 A root', 'b1 b1 B root', ] assert await cylc_list(capsys, mro=True, all_namespaces=True) == [ 'A A root', 'A1 A1 A root', 'A11 A11 A1 A root', 'A12 A12 A1 A root', 'B B root', 'a111 a111 A11 A1 A root', 'a112 a112 A11 A1 A root', 'a12 a12 A1 A root', 'a121 a121 A12 A1 A root', 'a2 a2 A root', 'b1 b1 B root', 'root root', ] with pytest.raises(InputError): await cylc_list(capsys, titles=True, mro=True) async def test_tree(cylc_list, supports_utf8, capsys): """Test the --tree option.""" assert ( await cylc_list(capsys, tree=True) # NOTE: the --all-tasks and --all-namespaces opts should be ignored == await cylc_list(capsys, tree=True, all_tasks=True) == await cylc_list(capsys, tree=True, all_namespaces=True) == [ 'root ', ' `-A ', ' |-A1 ', ' | |-A11 ', ' | | |-a111 ', ' | | `-a112 ', ' | |-A12 ', ' | | `-a121 ', ' | `-a12 ', ' `-a2 ', ] ) assert ( await cylc_list(capsys, tree=True, box=True) == await cylc_list(capsys, box=True) # --tree implicit with --box == [ 'root ', ' └─A ', ' ├─A1 ', ' │ ├─A11 ', ' │ │ ├─a111 ', ' │ │ └─a112 ', ' │ ├─A12 ', ' │ │ └─a121 ', ' │ └─a12 ', ' └─a2 ', ] ) assert await cylc_list(capsys, tree=True, titles=True) == [ 'root ', ' `-A ', ' |-A1 ', ' | |-A11 ', ' | | |-a111 Title For A11', ' | | `-a112 Title For A11', ' | |-A12 ', ' | | `-a121 Title For A', ' | `-a12 Title For A', ' `-a2 Title For A', ] assert await cylc_list(capsys, tree=True, box=True, titles=True) == [ 'root ', ' └─A ', ' ├─A1 ', ' │ ├─A11 ', ' │ │ ├─a111 Title For A11', ' │ │ └─a112 Title For A11', ' │ ├─A12 ', ' │ │ └─a121 Title For A', ' │ └─a12 Title For A', ' └─a2 Title For A', ] async def test_box_with_lang_c(cylc_list, does_not_support_utf8, capsys): """It falls back to plain output if unicode support isn't there.""" assert await cylc_list(capsys, tree=True, box=True) == [ 'a111', 'a112', 'a12', 'a121', 'a2', ] async def test_titles(cylc_list, supports_utf8, capsys): """Test the --titles option.""" assert await cylc_list(capsys, titles=True) == [ 'a111 Title For A11', 'a112 Title For A11', 'a12 Title For A', 'a121 Title For A', 'a2 Title For A', ] assert await cylc_list(capsys, titles=True, all_tasks=True) == [ 'a111 Title For A11', 'a112 Title For A11', 'a12 Title For A', 'a121 Title For A', 'a2 Title For A', 'b1 ', ] assert await cylc_list(capsys, titles=True, all_namespaces=True) == [ 'A Title For A', 'A1 Title For A', 'A11 Title For A11', 'A12 Title For A', 'B ', 'a111 Title For A11', 'a112 Title For A11', 'a12 Title For A', 'a121 Title For A', 'a2 Title For A', 'b1 ', 'root ', ] async def test_points(cylc_list, supports_utf8, capsys): """Test the --points option.""" # specify start and stop points assert await cylc_list(capsys, prange='1,2') == [ '1/a111', '1/a112', '1/a12', '1/a121', '1/a2', '2/a111', '2/a112', '2/a12', '2/a121', '2/a2', ] # leave start and stop points implicit assert await cylc_list(capsys, prange=',') == [ '1/a111', '1/a112', '1/a12', '1/a121', '1/a2', '2/a111', '2/a112', '2/a12', '2/a121', '2/a2', '3/a111', '3/a112', '3/a12', '3/a121', '3/a2', ] # leave start point implicit assert await cylc_list(capsys, prange=',2') == [ '1/a111', '1/a112', '1/a12', '1/a121', '1/a2', '2/a111', '2/a112', '2/a12', '2/a121', '2/a2', ] # leave stop point implicit assert await cylc_list(capsys, prange='4,') == [ '4/a111', '4/a112', '4/a12', '4/a121', '4/a2', '5/a111', '5/a112', '5/a12', '5/a121', '5/a2', '6/a111', '6/a112', '6/a12', '6/a121', '6/a2', ] with pytest.raises(InputError): await cylc_list(capsys, prange='1,2', all_tasks=True) with pytest.raises(InputError): await cylc_list(capsys, prange='1,2', all_namespaces=True) cylc-flow-8.6.4/tests/integration/scripts/__init__.py0000664000175000017500000000135715202510242023100 0ustar alastairalastair# 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 . cylc-flow-8.6.4/tests/integration/scripts/test_validate_integration.py0000664000175000017500000001761715202510242026602 0ustar alastairalastair#!/usr/bin/env python3 # 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 . """Integration tests for Cylc Validate CLI script.""" import logging import pytest from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.parsec.exceptions import IllegalItemError, Jinja2Error async def test_validate_against_source_checks_source( capsys, validate, workflow_source, install, one_conf ): """Validation fails if validating against source with broken config. """ src_dir = workflow_source(one_conf) workflow_id = await install(src_dir) # Check that the original installation validates OK: validate(workflow_id, against_source=True) # Break the source config: with open(src_dir / 'flow.cylc', 'a') as handle: handle.write('\n[runtime]\n[[foo]]\nAgrajag = bowl of petunias') # # Check that Validate now fails: with pytest.raises(IllegalItemError, match='Agrajag'): validate(workflow_id, against_source=True) async def test_validate_against_source_gets_old_tvars( workflow_source, capsys, validate, scheduler, run, install, run_dir ): """Validation will retrieve template variables from a previously played workflow. """ src_dir = workflow_source({ '#!jinja2': None, 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'initial cycle point': '1854', 'graph': { 'P1Y': 'foo' }, }, 'runtime': { 'foo': { 'script': 'cylc pause ${CYLC_WORKFLOW_ID}' } } }) wf_id = await install(src_dir) installed_dir = run_dir / wf_id # Check that the original installation validates OK: validate(installed_dir) # # Start a scheduler with tvars option: schd = scheduler( wf_id, templatevars=['FOO="foo"'] ) async with run(schd): pass # Replace foo in graph with {{FOO}} and check that this still # Validates (using db value for FOO): flow_file = (installed_dir / 'flow.cylc') flow_file.write_text( flow_file.read_text().replace('P1Y = foo', 'P1Y = {{FOO}}')) validate(wf_id, against_source=True) # Check that the source will not validate alone: flow_file = (src_dir / 'flow.cylc') flow_file.write_text( flow_file.read_text().replace('P1Y = foo', 'P1Y = {{FOO}}')) with pytest.raises(Jinja2Error): validate(src_dir) def test_validate_simple_graph(flow, validate, caplog): """Test deprecation notice for Cylc 7 simple graph (no recurrence section) """ id_ = flow({ 'scheduler': {'allow implicit tasks': True}, 'scheduling': {'dependencies': {'graph': 'foo'}} }) validate(id_) expect = ( 'graph items were automatically upgraded' ' in "workflow definition":' '\n * (8.0.0) [scheduling][dependencies]graph -> [scheduling][graph]R1' ) assert expect in caplog.messages def test_pre_cylc8(flow, validate, caplog): """Test all current non-silent workflow obsoletions and deprecations. """ id_ = flow({ 'cylc': { 'events': { 'reset timer': 10, 'reset inactivity timer': 15, } }, "scheduling": { "initial cycle point": "20150808T00", "final cycle point": "20150808T00", "graph": { "P1D": "foo => cat & dog" }, "special tasks": { "external-trigger": 'cat("meow available")' } }, 'runtime': { 'foo, cat, dog': { 'suite state polling': {'template': ''}, 'events': {'reset timer': 20} } } }, defaults=False) validate(id_) for warning in ( ( ' * (7.8.0) [runtime][foo, cat, dog][suite state polling]template' ' - DELETED (OBSOLETE)'), ' * (7.8.1) [cylc][events]reset timer - DELETED (OBSOLETE)', ' * (7.8.1) [cylc][events]reset inactivity timer - DELETED (OBSOLETE)', ( ' * (7.8.1) [runtime][foo, cat, dog][events]reset timer' ' - DELETED (OBSOLETE)'), ( ' * (8.0.0) [runtime][foo, cat, dog][suite state polling]' ' -> [runtime][foo, cat, dog][workflow state polling]' ' - value unchanged'), ' * (8.0.0) [cylc] -> [scheduler] - value unchanged' ): assert warning in caplog.messages def test_graph_upgrade_msg_default(flow, validate, caplog, log_filter): """It lists Cycling definitions which need upgrading.""" id_ = flow({ 'scheduler': {'allow implicit tasks': True}, 'scheduling': { 'initial cycle point': 1042, 'dependencies': { 'R1': {'graph': 'foo'}, 'P1Y': {'graph': 'bar & baz'} } }, }) validate(id_) assert log_filter(contains='[scheduling][dependencies][X]graph') assert log_filter(contains='for X in:\n P1Y, R1') def test_graph_upgrade_msg_graph_equals(flow, validate, caplog, log_filter): """It gives a more useful message in special case where graph is key rather than section: [scheduling] [[dependencies]] graph = foo => bar """ id_ = flow({ 'scheduler': {'allow implicit tasks': True}, 'scheduling': {'dependencies': {'graph': 'foo => bar'}}, }) validate(id_) assert log_filter( contains='[scheduling][dependencies]graph -> [scheduling][graph]R1' ) def test_graph_upgrade_msg_graph_equals2(flow, validate, caplog, log_filter): """Both an implicit R1 and explict reccurance exist: It appends a note. """ id_ = flow({ 'scheduler': {'allow implicit tasks': True}, 'scheduling': { 'initial cycle point': '1000', 'dependencies': { 'graph': 'foo => bar', 'P1Y': {'graph': 'a => b'}}}, }) validate(id_) expect = ( 'graph items were automatically upgraded in' ' "workflow definition":' '\n * (8.0.0) [scheduling][dependencies][X]graph' ' -> [scheduling][graph]X - for X in:' '\n P1Y, graph' '\n ([scheduling][dependencies]graph moves to [scheduling][graph]R1)' ) assert log_filter(contains=expect) def test_undefined_parent(flow, validate): """It should catch tasks which inherit from implicit families.""" id_ = flow({ 'scheduling': {'graph': {'R1': 'foo'}}, 'runtime': {'foo': {'inherit': 'FOO'}} }) with pytest.raises(WorkflowConfigError, match='undefined parent for foo'): validate(id_) def test_log_parent_demoted(flow, validate, monkeypatch, caplog, log_filter): """It should log family "demotion" in verbose mode.""" monkeypatch.setattr( 'cylc.flow.flags.verbosity', 10, ) caplog.set_level(logging.DEBUG) id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo' } }, 'runtime': { 'foo': {'inherit': 'None, FOO'}, 'FOO': {}, } }) validate(id_) assert log_filter(contains='First parent(s) demoted to secondary') assert log_filter(contains="FOO as parent of 'foo'") cylc-flow-8.6.4/tests/integration/scripts/test_broadcast.py0000664000175000017500000002744315202510242024346 0ustar alastairalastair# 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 . import logging from ansimarkup import strip as cstrip import pytest from cylc.flow.network.client import WorkflowRuntimeClient from cylc.flow.option_parsers import Options from cylc.flow.rundb import CylcWorkflowDAO from cylc.flow.scheduler import Scheduler from cylc.flow.scripts.broadcast import ( _main, get_option_parser, ) from cylc.flow.util import sstrip BroadcastOptions = Options(get_option_parser()) async def test_broadcast_multi_workflow( one_conf, flow, scheduler, start, run_dir, test_dir, capsys, ): """Test a multi-workflow broadcast command.""" # create three workflows one = scheduler(flow(one_conf)) two = scheduler(flow(one_conf)) thr = scheduler(flow(one_conf)) # the ID under which all three are installed id_base = test_dir.relative_to(run_dir) async with start(one): async with start(two): async with start(thr): capsys.readouterr() # test a successful broadcast command rets = await _main( BroadcastOptions(settings=['script=true']), f'{id_base}*' ) # all three broadcasts should have succeeded assert list(rets.values()) == [True, True, True] out, err = capsys.readouterr() assert '[*/root] script=true' in out assert err == '' # test an unsuccessful broadcast command rets = await _main( BroadcastOptions( namespaces=['*'], settings=['script=true'], ), f'{id_base}*', ) # all three broadcasts should have failed assert list(rets.values()) == [False, False, False] out, err = capsys.readouterr() assert '[*/root] script=true' not in out assert ( # NOTE: for historical reasons this message goes to stdout # not stderr 'Rejected broadcast:' ' settings are not compatible with the workflow' ) in out assert err == '' async def test_broadcast_multi_namespace( flow, scheduler, start, db_select, ): """Test a multi-namespace broadcast command. See https://github.com/cylc/cylc-flow/issues/6334 """ id_ = flow( { 'scheduling': { 'graph': {'R1': 'a & b & c & fin'}, }, 'runtime': { 'root': {'execution time limit': 'PT1S'}, 'VOWELS': {'execution time limit': 'PT2S'}, 'CONSONANTS': {'execution time limit': 'PT3S'}, 'a': {'inherit': 'VOWELS'}, 'b': {'inherit': 'CONSONANTS'}, 'c': {'inherit': 'CONSONANTS'}, }, } ) schd = scheduler(id_) async with start(schd): # issue a broadcast to multiple namespaces rets = await _main( BroadcastOptions( settings=['execution time limit = PT5S'], namespaces=['root', 'VOWELS', 'CONSONANTS'], ), schd.workflow, ) # the broadcast should succeed assert list(rets.values()) == [True] # the broadcast manager should store the "coerced" setting for task in ['a', 'b', 'c', 'fin']: assert schd.broadcast_mgr.get_broadcast( schd.tokens.duplicate(cycle='1', task=task) ) == {'execution time limit': 5.0} # the database should store the "raw" setting assert sorted( db_select(schd, True, CylcWorkflowDAO.TABLE_BROADCAST_STATES) ) == [ ('*', 'CONSONANTS', 'execution time limit', 'PT5S'), ('*', 'VOWELS', 'execution time limit', 'PT5S'), ('*', 'root', 'execution time limit', 'PT5S'), ] async def test_broadcast_truncated_datetime(flow, scheduler, start, capsys): """It should reject truncated datetime cycle points. See https://github.com/cylc/cylc-flow/issues/6407 """ id_ = flow({ 'scheduling': { 'initial cycle point': '2000', 'graph': { 'R1': 'foo', }, } }) schd = scheduler(id_) async with start(schd): # attempt an invalid broadcast rets = await _main( BroadcastOptions( settings=['[environment]FOO=bar'], point_strings=['050101T0000Z'], # <== truncated ), schd.workflow, ) # the broadcast should fail assert list(rets.values()) == [False] # an error should be recorded _out, err = capsys.readouterr() assert ( 'Rejected broadcast:' ' settings are not compatible with the workflow' ) in err async def test_invalid(one, start, capsys, monkeypatch): """It should reject invalid broadcasts. * Broadcast operations may contain a mix of valid and invalid cycles/namespaces/settings. * Valid settings are applied and reported. * Invalid settings are rejected and reported. """ # disable client side config validation so that we can see invalid # broadcast settings in the CLI output monkeypatch.setattr( 'cylc.flow.scripts.broadcast.cylc_config_validate', lambda *args, **kwargs: True, ) # 1) disable CLI colour output monkeypatch.setattr('cylc.flow.scripts.broadcast.cparse', cstrip) async with start(one): # test the GraphQL API client = WorkflowRuntimeClient(one.workflow) response = await client.async_request( 'graphql', { 'request_string': ''' mutation( $workflows: [WorkflowID]!, $cycles: [BroadcastCyclePoint], $namespaces: [NamespaceName], $settings: [BroadcastSetting] ) { broadcast( mode: Set workflows: $workflows cyclePoints: $cycles namespaces: $namespaces settings: $settings ) { result } } ''', 'variables': { 'workflows': [one.workflow], # valid, invalid 'cycles': ['*', 'invalid'], 'namespaces': ['root', 'invalid'], 'settings': [{'script': 'true'}, {'invalid': 'false'}], } } ) modified, rejected = response['broadcast']['result'][0]['response'] assert modified == [ [ # cycle '*', # namespace 'root', # settings {'script': 'true'}, ] ] assert rejected == { 'point_strings': ['invalid'], 'namespaces': ['invalid'], 'settings': [{'invalid': 'false'}], } # 2) test the CLI capsys.readouterr() # flush out any stdout/err await _main( BroadcastOptions( point_strings=['*', 'invalid'], namespaces=['root', 'invalid'], settings=['script=true', 'invalid=false'], ), one.workflow, ) out, err = capsys.readouterr() assert out == 'Broadcast set:\n+ [*/root] script=true\n' assert err == sstrip(''' ERROR: Rejected broadcast: settings are not compatible with the workflow --namespace=invalid --point=invalid --set=invalid=false ''') + '\n' async def test_internal_error(one, start, capsys, monkeypatch, log_filter): """It should handle internal code errors elegantly.""" # simulate an internal error # Note: The broadcast "lock" mechanism is an example of how such an error # could arise def put_broadcast(*args, **kwargs): raise Exception('FooBar') async with start(one): # issue a broadcast one.broadcast_mgr.put_broadcast = put_broadcast client = WorkflowRuntimeClient(one.workflow) response = await client.async_request( 'graphql', { 'request_string': ''' mutation( $workflows: [WorkflowID]!, $cycles: [BroadcastCyclePoint], $namespaces: [NamespaceName], $settings: [BroadcastSetting] ) { broadcast( mode: Set workflows: $workflows cyclePoints: $cycles namespaces: $namespaces settings: $settings ) { result } } ''', 'variables': { 'workflows': [one.workflow], 'cycles': ['*'], 'namespaces': ['root'], 'settings': [{'script': 'true'}], } } ) # check it comes back with an error code assert ( response['broadcast']['result'][0] )['response']['error']['message'] == 'FooBar' # the error should be logged server-side too assert log_filter(contains='FooBar') async def test_broadcast_auto_upgraded_settings( one: Scheduler, start, log_filter, caplog: pytest.LogCaptureFixture, capsys: pytest.CaptureFixture, ): """Test broadcast of deprecated/obsolete settings.""" opts = { 'point_strings': ['1'], 'namespaces': ['root'], } async with start(one): # Test deprecated setting that is moved to a different section by # the auto-upgrader should still work. rets = await _main( BroadcastOptions( settings=['[job]execution time limit=PT1H'], **opts ), one.workflow, ) assert set(rets.values()) == {True} assert log_filter( logging.WARNING, regex=r"Deprecated config.* upgraded.* broadcast" ) stdout, stderr = capsys.readouterr() assert "Traceback" not in stdout + stderr caplog.clear() # Test obsolete setting that is removed by the auto-upgrader should # be rejected. rets = await _main( BroadcastOptions( settings=['extra log files=whatever'], **opts ), one.workflow, ) assert set(rets.values()) == {False} assert log_filter( logging.WARNING, regex=r"Obsolete config.* rejected by the broadcast", ) stdout, stderr = capsys.readouterr() assert "InputError: No valid settings to broadcast" in stderr assert "Traceback" not in stdout + stderr cylc-flow-8.6.4/tests/integration/scripts/test_graph.py0000664000175000017500000000321315202510242023472 0ustar alastairalastair# 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 . import pytest from cylc.flow.option_parsers import Options from cylc.flow.scripts.graph import _main, get_option_parser Opts = Options(get_option_parser()) @pytest.fixture def disable_graph_open(monkeypatch): """Prevent "cylc graph" from trying to pop open the image.""" monkeypatch.setattr( 'cylc.flow.scripts.graph.open_image', lambda *_a, **_k: None, ) async def test_blank_graph(one, disable_graph_open, capsys): """It should inform the user if there are no tasks to display.""" # graph with one task await _main(Opts(color='never'), one.tokens.id) out, err = capsys.readouterr() assert 'Graph rendered to' in out # graph with no tasks (the only task is in cycle "1") await _main(Opts(color='never'), one.tokens.id, '5') out, err = capsys.readouterr() assert 'No tasks to display' in err assert 'Try changing the start and stop values' in err cylc-flow-8.6.4/tests/integration/scripts/test_kill.py0000664000175000017500000002116715202510242023334 0ustar alastairalastair# 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 . import asyncio import logging from secrets import token_hex from unittest.mock import Mock import pytest from cylc.flow.commands import ( kill_tasks, remove_tasks, run_cmd, set_prereqs_and_outputs, ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.scheduler import Scheduler from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_remote_mgr import ( REMOTE_FILE_INSTALL_DONE, REMOTE_FILE_INSTALL_IN_PROGRESS, REMOTE_INIT_DONE, REMOTE_INIT_IN_PROGRESS, ) from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_WAITING, ) LOCALHOST = 'localhost' async def task_state(itask: TaskProxy, state: str, timeout=4, **kwargs): """Await task state.""" async with asyncio.timeout(timeout): while not itask.state(state, **kwargs): await asyncio.sleep(0.1) def patch_remote_init(schd: Scheduler, value: str): """Set remote init state.""" schd.task_job_mgr.task_remote_mgr.remote_init_map[LOCALHOST] = value async def test_simulation(flow, scheduler, run): """Test killing a running task in simulation mode.""" conf = { 'scheduling': { 'graph': { 'R1': 'foo', }, }, 'runtime': { 'root': { 'simulation': { 'default run length': 'PT30S', }, }, }, } schd: Scheduler = scheduler(flow(conf), paused_start=False) async with run(schd): itask = schd.pool.get_tasks()[0] await task_state(itask, TASK_STATUS_RUNNING) await run_cmd(kill_tasks(schd, [itask.identity])) await task_state(itask, TASK_STATUS_FAILED, is_held=True) assert schd.check_workflow_stalled() async def test_kill_preparing( flow, scheduler, run, monkeypatch: pytest.MonkeyPatch, log_filter ): """Test killing a preparing task.""" schd: Scheduler = scheduler( flow('foo'), run_mode='live', paused_start=False ) async with run(schd): # Make the task indefinitely preparing: monkeypatch.setattr( schd.task_job_mgr, '_prep_submit_task_job', Mock(return_value=None) ) itask = schd.pool.get_tasks()[0] await task_state(itask, TASK_STATUS_PREPARING, is_held=False) await run_cmd(kill_tasks(schd, [itask.identity])) await task_state(itask, TASK_STATUS_SUBMIT_FAILED, is_held=True) assert log_filter(logging.ERROR, 'killed in job prep') async def test_kill_preparing_pipeline( flow, scheduler, start, monkeypatch: pytest.MonkeyPatch ): """Test killing a preparing task through various stages of the preparing pipeline that involve submitting subprocesses and waiting for them to complete.""" # Make localhost look like a remote target so we can test # remote init/file install stages: monkeypatch.setattr( 'cylc.flow.task_job_mgr.get_localhost_install_target', Mock(return_value=token_hex()), ) schd: Scheduler = scheduler( flow('one'), run_mode='live', paused_start=False ) async with start(schd): remote_mgr = schd.task_job_mgr.task_remote_mgr mock_eval_platform = Mock(return_value=None) monkeypatch.setattr(remote_mgr, 'eval_platform', mock_eval_platform) mock_remote_init = Mock() monkeypatch.setattr(remote_mgr, 'remote_init', mock_remote_init) mock_file_install = Mock() monkeypatch.setattr(remote_mgr, 'file_install', mock_file_install) itask = schd.pool.get_tasks()[0] # Platform eval: schd.submit_task_jobs([itask]) assert itask.state(TASK_STATUS_PREPARING) assert schd.release_tasks_to_run() is False await run_cmd(kill_tasks(schd, [itask.identity])) assert itask.state(TASK_STATUS_SUBMIT_FAILED) assert schd.release_tasks_to_run() is False # Set to finished: mock_eval_platform.return_value = LOCALHOST # Should not submit after finish because it was killed: assert schd.release_tasks_to_run() is False # Remote init: patch_remote_init(schd, REMOTE_INIT_IN_PROGRESS) schd.submit_task_jobs([itask]) assert itask.state(TASK_STATUS_PREPARING) assert schd.release_tasks_to_run() is False await run_cmd(kill_tasks(schd, [itask.identity])) assert itask.state(TASK_STATUS_SUBMIT_FAILED) assert schd.release_tasks_to_run() is False # Set to finished: patch_remote_init(schd, REMOTE_INIT_DONE) # Should not submit after finish because it was killed: assert schd.release_tasks_to_run() is False assert not mock_remote_init.called # Remote file install: patch_remote_init(schd, REMOTE_FILE_INSTALL_IN_PROGRESS) schd.submit_task_jobs([itask]) assert itask.state(TASK_STATUS_PREPARING) assert schd.release_tasks_to_run() is False await run_cmd(kill_tasks(schd, [itask.identity])) assert itask.state(TASK_STATUS_SUBMIT_FAILED) assert schd.release_tasks_to_run() is False # Set to finished: patch_remote_init(schd, REMOTE_FILE_INSTALL_DONE) # Should not submit after finish because it was killed: assert schd.release_tasks_to_run() is False assert not mock_file_install.called @pytest.mark.parametrize('method', (remove_tasks, set_prereqs_and_outputs)) async def test_kill_hold_retry(method, flow, scheduler, start): """Test the interaction between kill, hold and automated retry. When killed, tasks should be put into the held state to suppress automatic retries. Once the task has been completed or been removed, the held and retrying states no longer serve a purpose and should be cleared (see https://github.com/cylc/cylc-flow/pull/7100). """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'execution & submission' }, }, 'runtime': { 'execution, submission': { 'execution retry delays': 'PT0S', 'submission retry delays': 'PT0S', } } }) schd: Scheduler = scheduler(id_) async with start(schd): # a task we will make execution-fail execution = schd.pool.get_task(IntegerPoint('1'), 'execution') # a task we will make submission-fail submission = schd.pool.get_task(IntegerPoint('1'), 'submission') assert execution and submission # the tasks should not be held (doesn't hurt to check!) assert execution.state(is_held=False) assert submission.state(is_held=False) # fake job submissions execution.state_reset(TASK_STATUS_RUNNING) submission.state_reset(TASK_STATUS_PREPARING) schd.task_job_mgr._set_retry_timers(execution, execution.tdef.rtconfig) schd.task_job_mgr._set_retry_timers( submission, submission.tdef.rtconfig ) # kill the tasks await run_cmd( kill_tasks(schd, [execution.identity, submission.identity]) ) # the tasks should be in the held state with a retry lined up assert execution.state(TASK_STATUS_WAITING, is_held=True) # held assert submission.state(TASK_STATUS_WAITING, is_held=True) assert list(execution.state.xtriggers.values()) == [False] # retry assert list(submission.state.xtriggers.values()) == [False] # either remove or complete the tasks await run_cmd( method(schd, [execution.identity, submission.identity], []) ) # the held state should be cleared assert execution.state(is_held=False) assert submission.state(is_held=False) # the retry state should be cleared assert list(execution.state.xtriggers.values()) == [True] assert list(submission.state.xtriggers.values()) == [True] cylc-flow-8.6.4/tests/integration/scripts/test_validate_reinstall.py0000664000175000017500000001006315202510242026240 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.option_parsers import Options from cylc.flow.scripts.validate_reinstall import ( get_option_parser as vr_gop, vr_cli, ) ValidateReinstallOptions = Options(vr_gop()) def answer_prompts(monkeypatch, *responses): """Hardcode responses to "cylc vr" interactive prompts.""" # make it look like we are running this command in a terminal monkeypatch.setattr( 'cylc.flow.scripts.validate_reinstall.is_terminal', lambda: True ) monkeypatch.setattr( 'cylc.flow.scripts.reinstall.is_terminal', lambda: True ) # patch user input count = -1 def _input(prompt): nonlocal count, responses responses = responses count += 1 print(prompt) # send the prompt to stdout for testing return responses[count] monkeypatch.setattr( 'cylc.flow.scripts.validate_reinstall._input', _input, ) monkeypatch.setattr( 'cylc.flow.scripts.reinstall._input', _input, ) async def test_prompt_for_running_workflow_with_no_changes( monkeypatch, capsys, one_run, capcall, ): """It should reinstall and restart the workflow with no changes. See: https://github.com/cylc/cylc-flow/issues/6261 We hope to get users into the habit of "cylc vip" to create a new run, and "cylc vr" to contine an old one (picking up any new changes in the process). If there are no changes to reinstall (or if the user chooses not to resintall) the "cylc vr" prompts whether to continue or do nothing. The "nothing to reinstall" situation can be interpreted two ways: 1. Unexpected error, the user expected there to be something to reinstall, but there wasn't. E.g, they forgot to press save. 2. Unexpected annoyance, I wanted to restart the workflow, just do it. To handle this we explain that there are no changes to reinstall and prompt the user to see if they want to press save or restart the workflow. """ # disable the clean_sysargv logic (this interferes with other tests) cleanup_sysargv_calls = capcall( 'cylc.flow.scripts.validate_reinstall.cleanup_sysargv' ) # answer "y" to prompt answer_prompts(monkeypatch, 'n', 'y') # attempt to restart it with "cylc vr" ret = await vr_cli( vr_gop(), ValidateReinstallOptions(), one_run ) # the workflow should reinstall assert ret # the user should have been warned that there were no changes to reinstall outerr = capsys.readouterr()[0] assert 'Reinstall would make the above changes' in outerr # they should have been presented with a prompt # (to which we have hardcoded the response "y") assert 'Restart anyway?' in outerr # the workflow should have restarted assert len(cleanup_sysargv_calls) == 1 async def test_reinstall_abort( monkeypatch, capsys, one_run, ): """It should abort reinstallation according to user prompt.""" # answer 'n' to prompt answer_prompts(monkeypatch, 'n', 'n') # attempt to restart it with "cylc vr" ret = await vr_cli( vr_gop(), ValidateReinstallOptions(), one_run ) assert ret is False # they should have been presented with a prompt # (to which we have hardcoded the response "n") assert 'Continue' in capsys.readouterr()[0] cylc-flow-8.6.4/tests/integration/scripts/test_completion_server.py0000664000175000017500000001350715202510242026137 0ustar alastairalastair# 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 . """Integration tests for the "cylc completion-server command. See also the more extensive unit tests for this module. """ from cylc.flow.scripts.completion_server import complete_cylc def setify(coro): """Cast returned lists to sets for coroutines. Convenience function to use when you want to test output not order. """ async def _coro(*args, **kwargs): ret = await coro(*args, **kwargs) if isinstance(ret, list): return set(ret) return ret return _coro async def test_list_prereqs_and_outputs(flow, scheduler, start): """Test the success cases for listing task prereqs/outputs. The error cases are tested in a unit test (doesn't require a running scheduler). """ _complete_cylc = setify(complete_cylc) # Note: results are un-ordered id_ = flow({ 'scheduler': { 'allow implicit tasks': 'True', }, 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'graph': { 'P1': ''' a => b c => d b[-P1] => b ''' }, }, 'runtime': { 'a': {}, 'b': { 'outputs': { 'foo': 'abc def ghi', } } } }) schd = scheduler(id_) async with start(schd): await schd.update_data_structure() b1 = schd.tokens.duplicate(cycle='1', task='b') d1 = schd.tokens.duplicate(cycle='1', task='d') e1 = schd.tokens.duplicate(cycle='1', task='e') # does not exist # list prereqs (b1) assert await _complete_cylc('cylc', 'set', b1.id, '--pre', '') == { # keywords 'all', # intra-cycle dependency '1/a:succeeded', # inter-cycle dependency '0/b:succeeded', } # list outputs (b1) assert await _complete_cylc('cylc', 'set', b1.id, '--out', '') == { # regular task outputs 'expired', 'failed', 'started', 'submit-failed', 'submitted', 'succeeded', # custom task outputs 'foo', } # list prereqs (d1) assert await _complete_cylc('cylc', 'set', d1.id, '--pre', '') == { # keywords 'all', # d1 prereqs '1/c:succeeded', } # list prereqs for multiple (b1, d1) assert await _complete_cylc( 'cylc', 'set', b1.id, d1.id, '--pre', '', ) == { # keywords 'all', # b1 prereqs '1/a:succeeded', '0/b:succeeded', # d1 prereqs '1/c:succeeded', } # list prereqs for multiple (b1, d1) - alternative format assert await _complete_cylc( 'cylc', 'set', f'{schd.id}//', f'//{b1.relative_id}', f'//{d1.relative_id}', '--pre', '', ) == { # keywords 'all', # b1 prereqs '1/a:succeeded', '0/b:succeeded', # d1 prereqs '1/c:succeeded', } # list outputs for a non-existant task assert await _complete_cylc('cylc', 'set', e1.id, '--out', '') == set() # list outputs for a non-existant workflow assert await _complete_cylc( 'cylc', 'set', # this invalid workflow shouldn't prevent it from returning values # for the valid one 'no-such-workflow//', f'{schd.id}//', f'//{b1.relative_id}', f'//{d1.relative_id}', '--pre', '', ) == { # keywords 'all', # b1 prereqs '1/a:succeeded', '0/b:succeeded', # d1 prereqs '1/c:succeeded', } # start a second workflow to test multi-workflow functionality id2 = flow({ 'scheduling': { 'graph': { 'R1': ''' x => z ''' } }, 'runtime': {'x': {}, 'z': {}}, }) schd2 = scheduler(id2) async with start(schd2): await schd2.update_data_structure() z1 = schd2.tokens.duplicate(cycle='1', task='z') # list prereqs for multiple tasks in multiple workflows # (it should combine the results from both workflows) assert await _complete_cylc( 'cylc', 'set', b1.id, z1.id, '--pre', '', ) == { # keywords 'all', # workflow1//1/b prereqs '0/b:succeeded', '1/a:succeeded', # workflow2//1/z prereqs '1/x:succeeded' } cylc-flow-8.6.4/tests/integration/scripts/conftest.py0000664000175000017500000000324115202510242023160 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from uuid import uuid1 import pytest from cylc.flow.workflow_files import WorkflowFiles from ..utils.flow_writer import flow_config_str @pytest.fixture def one_src(tmp_path, one_conf): """Basic source workflow containing the config of the "one" test flow.""" src_dir = tmp_path (src_dir / 'flow.cylc').write_text(flow_config_str(one_conf)) return src_dir @pytest.fixture def one_run(one_src, test_dir, run_dir): """Basic source workflow with a _cylc-install/source symlink.""" w_run_dir = test_dir / str(uuid1()) w_run_dir.mkdir() (w_run_dir / 'flow.cylc').write_text( (one_src / 'flow.cylc').read_text() ) install_dir = (w_run_dir / WorkflowFiles.Install.DIRNAME) install_dir.mkdir(parents=True) (install_dir / WorkflowFiles.Install.SOURCE).symlink_to( one_src, target_is_directory=True, ) return str(w_run_dir.relative_to(run_dir)) cylc-flow-8.6.4/tests/integration/scripts/test_reinstall.py0000664000175000017500000003177415202510242024403 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import asyncio from contextlib import asynccontextmanager from pathlib import Path import re from secrets import token_hex from types import SimpleNamespace from ansimarkup import strip as cstrip import pytest from cylc.flow.exceptions import WorkflowFilesError from cylc.flow.install import reinstall_workflow from cylc.flow.option_parsers import Options from cylc.flow.scripts.reinstall import ( get_option_parser as reinstall_gop, reinstall_cli, ) from cylc.flow.workflow_files import WorkflowFiles from ..utils.entry_points import EntryPointWrapper ReInstallOptions = Options(reinstall_gop()) # cli opts # interactive: yes no # rose: yes no # workflow_running: yes no @pytest.fixture(autouse=True) def color_strip(monkeypatch: pytest.MonkeyPatch): """Strip colour as the normal colour stripping doesn't apply to tests.""" monkeypatch.setattr('cylc.flow.scripts.reinstall.cparse', cstrip) @pytest.fixture def interactive(monkeypatch): monkeypatch.setattr( 'cylc.flow.scripts.reinstall.is_terminal', lambda: True, ) @pytest.fixture def non_interactive(monkeypatch): monkeypatch.setattr( 'cylc.flow.scripts.reinstall.is_terminal', lambda: False, ) @pytest.fixture def answer_prompt(monkeypatch: pytest.MonkeyPatch): """Answer reinstall prompt.""" def inner(answer: str): monkeypatch.setattr( 'cylc.flow.scripts.reinstall._input', lambda x: answer ) return inner @pytest.fixture def one_src(tmp_path): src_dir = tmp_path / 'src' src_dir.mkdir() (src_dir / 'flow.cylc').touch() (src_dir / 'rose-suite.conf').touch() return SimpleNamespace(path=src_dir) @pytest.fixture def one_run(one_src, test_dir, run_dir): w_run_dir = test_dir / token_hex(4) w_run_dir.mkdir() (w_run_dir / 'flow.cylc').touch() (w_run_dir / 'rose-suite.conf').touch() install_dir = (w_run_dir / WorkflowFiles.Install.DIRNAME) install_dir.mkdir(parents=True) (install_dir / WorkflowFiles.Install.SOURCE).symlink_to( one_src.path, target_is_directory=True, ) return SimpleNamespace( path=w_run_dir, id=str(w_run_dir.relative_to(run_dir)), ) async def test_rejects_random_workflows(one, one_run): """It should only work with workflows installed by cylc install.""" with pytest.raises(WorkflowFilesError) as exc_ctx: await reinstall_cli(opts=ReInstallOptions(), workflow_id=one.workflow) assert 'was not installed with cylc install' in str(exc_ctx.value) async def test_invalid_source_dir(one_src, one_run): """It should detect & fail for an invalid source symlink""" source_link = Path( one_run.path, WorkflowFiles.Install.DIRNAME, WorkflowFiles.Install.SOURCE, ) source_link.unlink() source_link.symlink_to(one_src.path / 'flow.cylc') with pytest.raises(WorkflowFilesError) as exc_ctx: await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert 'Workflow source dir is not accessible' in str(exc_ctx.value) async def test_no_changes_needed(one_src, one_run, capsys, interactive): """It should not reinstall if no changes are needed. This is not a hard requirement, in practice rsync output may differ from expectation so this is a nice-to-have, not expected to work 100% of the time. """ assert not await reinstall_cli( opts=ReInstallOptions(), workflow_id=one_run.id ) assert 'up to date with' in capsys.readouterr().out async def test_non_interactive( one_src, one_run, capsys, capcall, non_interactive ): """It should not perform a dry-run or prompt in non-interactive mode.""" # capture reinstall calls reinstall_calls = capcall( 'cylc.flow.scripts.reinstall.reinstall_workflow', reinstall_workflow, ) # give it something to reinstall (one_src.path / 'a').touch() # reinstall assert await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) # only one rsync call should have been made (i.e. no --dry-run) assert len(reinstall_calls) == 1 assert 'Successfully reinstalled' in capsys.readouterr().out async def test_interactive( one_src, one_run, capsys, capcall, interactive, answer_prompt ): """It should perform a dry-run and prompt in interactive mode.""" # capture reinstall calls reinstall_calls = capcall( 'cylc.flow.scripts.reinstall.reinstall_workflow', reinstall_workflow, ) # give it something to reinstall (one_src.path / 'a').touch() answer_prompt('n') assert ( await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) is False ) # only one rsync call should have been made (i.e. the --dry-run) assert [call[1].get('dry_run') for call in reinstall_calls] == [True] assert 'reinstall cancelled' in capsys.readouterr().out reinstall_calls.clear() answer_prompt('y') assert await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) # two rsync calls should have been made (i.e. the --dry-run and the real) assert [call[1].get('dry_run') for call in reinstall_calls] == [ True, False ] assert 'Successfully reinstalled' in capsys.readouterr().out async def test_workflow_running( one_src, one_run, monkeypatch, capsys, non_interactive, ): """It should advise running "cylc reload" where applicable.""" # the message we are expecting reload_message = f'Run "cylc reload {one_run.id}"' # reinstall with a stopped workflow (reload message shouldn't show) assert await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert reload_message not in capsys.readouterr().out # reinstall with a running workflow (reload message should show) monkeypatch.setattr( # make it look like the workflow is running 'cylc.flow.scripts.reinstall.load_contact_file', lambda x: None, ) assert await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert reload_message in capsys.readouterr().out async def test_rsync_stuff(one_src, one_run, capsys, non_interactive): """Make sure rsync is working correctly.""" # src contains files: a, b (one_src.path / 'a').touch() with open(one_src.path / 'b', 'w+') as b_file: b_file.write('x') (one_src.path / 'b').touch() # run contains files: b, c (where b is different to the source copy) (one_run.path / 'b').touch() (one_run.path / 'c').touch() await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) # a should have been left assert (one_run.path / 'a').exists() # b should have been updated assert (one_run.path / 'b').exists() with open(one_run.path / 'b', 'r') as b_file: assert b_file.read() == 'x' # c should have been removed assert not (one_run.path / 'c').exists() async def test_rose_warning( one_src, one_run, capsys, interactive, answer_prompt ): """It should warn that Rose installed files will be deleted. See https://github.com/cylc/cylc-rose/issues/149 """ # fragment of the message we expect rose_message = ( 'Files created by Rose file installation will show as deleted' ) answer_prompt('n') (one_src.path / 'a').touch() # give it something to install # reinstall (with rose-suite.conf file) await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert rose_message in capsys.readouterr().err # reinstall (no rose-suite.conf file) (one_src.path / 'rose-suite.conf').unlink() await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert rose_message not in capsys.readouterr().err async def test_keyboard_interrupt( one_src, one_run, interactive, monkeypatch, capsys ): """It should handle a KeyboardInterrupt during dry-run elegantly. E.G. A user may ctrl+c rather than answering "n" (for no). To make it clear a canceled message should show. """ def raise_keyboard_interrupt(): raise KeyboardInterrupt() # currently the first call in the dry-run branch monkeypatch.setattr( 'cylc.flow.scripts.reinstall.is_terminal', raise_keyboard_interrupt, ) await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert 'reinstall cancelled' in capsys.readouterr().out async def test_rsync_fail(one_src, one_run, mock_glbl_cfg, non_interactive): """It should raise an error on rsync failure.""" mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', ''' [platforms] [[localhost]] rsync command = false ''', ) (one_src.path / 'a').touch() # give it something to install with pytest.raises(WorkflowFilesError) as exc_ctx: await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) assert 'An error occurred reinstalling' in str(exc_ctx.value) async def test_permissions_change( one_src, one_run, interactive, answer_prompt, capsys: pytest.CaptureFixture, ): """It detects permissions changes.""" # Add script file: script_path: Path = one_src.path / 'myscript' script_path.touch() await reinstall_cli( opts=ReInstallOptions(skip_interactive=True), workflow_id=one_run.id ) assert (one_run.path / 'myscript').is_file() capsys.readouterr() # clears capsys # Change permissions (e.g. user forgot to make it executable before) script_path.chmod(0o777) # Answer "no" to reinstall prompt (we just want to test dry run) answer_prompt('n') await reinstall_cli( opts=ReInstallOptions(), workflow_id=one_run.id ) out, _ = capsys.readouterr() # On some systems may get "recv" instead of "send" assert re.search(r'(send|recv) myscript\n', out) @pytest.fixture def my_install_plugin(monkeypatch): """This configures a single post_install plugin. The plugin starts an async task, then returns. """ progress = [] @EntryPointWrapper def post_install_basic(*_, **__): """Simple plugin that returns one env var and one template var.""" async def my_async(): # the async task await asyncio.sleep(2) progress.append('end') # start the async task progress.append('start') asyncio.get_event_loop().create_task(my_async()) progress.append('return') # return a blank result return { 'env': {}, 'template_variables': {}, } monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [post_install_basic] if namespace == 'cylc.post_install' else [] ) ) return progress async def test_async_block( one_src, one_run, my_install_plugin, monkeypatch, ): """Ensure async tasks created by post_install plugins are awaited. The cylc-rose plugin may create asyncio tasks when run but cannot await them (because it isn't async itself). To get around this we have "cylc reinstall" use "async_block" which detects tasks created in the background and awaits them. This test ensures that the async_block mechanism is correctly plugged in to "cylc reinstall". See https://github.com/cylc/cylc-rose/issues/274 """ # this is what it should do (one_src.path / 'a').touch() # give it something to install assert my_install_plugin == [] await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) # the presence of "end" means that the task was awaited assert my_install_plugin == ['start', 'return', 'end'] # substitute the "async_block" (which waits for asyncio tasks started in # the background) for a fake implementation (which doesn't) @asynccontextmanager async def async_block(): yield monkeypatch.setattr( 'cylc.flow.plugins._async_block', async_block, ) # this is what it would have done without async block (one_src.path / 'b').touch() # give it something else to install my_install_plugin.clear() assert my_install_plugin == [] await reinstall_cli(opts=ReInstallOptions(), workflow_id=one_run.id) # the absence of "end" means that the task was not awaited assert my_install_plugin == ['start', 'return'] cylc-flow-8.6.4/tests/integration/scripts/test_set.py0000664000175000017500000004051615202510242023173 0ustar alastairalastair# 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 . """Test "cylc set" functionality. Note: see also functional tests """ from secrets import token_hex from typing import Callable import pytest from cylc.flow.commands import ( run_cmd, set_prereqs_and_outputs, ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.data_messages_pb2 import ( PbJob, PbTaskProxy, ) from cylc.flow.data_store_mgr import ( JOBS, TASK_PROXIES, task_mean_elapsed_time, ) from cylc.flow.id import TaskTokens from cylc.flow.scheduler import Scheduler from cylc.flow.task_outputs import ( TASK_OUTPUT_FAILED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_PREPARING, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, ) def outputs_section(*names: str) -> dict: """Create outputs section with random messages for the given output names. """ return { 'outputs': { name: token_hex() for name in names } } async def test_set_parentless_spawning( flow, scheduler, run, complete, ): """Ensure that setting outputs does not interfere with parentless spawning. Setting outputs manually causes the logic to follow a different code pathway to "natural" output satisfaction. If we're not careful this could lead to "premature shutdown" (i.e. the scheduler thinks it's finished when it isn't), this test makes sure that's not the case. """ id_ = flow({ 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'runahead limit': 'P0', 'graph': {'P1': 'a => z'}, }, }) schd: Scheduler = scheduler(id_, paused_start=False) async with run(schd): # mark cycle 1 as succeeded schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a'), TaskTokens('1', 'z')}, ['succeeded'], [], ['1'], ) # the parentless task "a" should be spawned out to the runahead limit assert schd.pool.get_task_ids() == {'2/a', '3/a'} # the workflow should run on to the next cycle await complete(schd, '2/a', timeout=5) async def test_rerun_incomplete( flow, scheduler, run, complete, reflog, ): """Incomplete tasks should be re-run.""" id_ = flow({ 'scheduling': { 'graph': {'R1': 'a => z'}, }, 'runtime': { # register a custom output 'a': {'outputs': {'x': 'xyz'}}, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): # generate 1/a:x but do not complete 1/a schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, ['x'], [], ['1'] ) triggers = reflog(schd) await complete(schd) assert triggers == { # the task 1/a should have been run despite the earlier # setting of the "x" output ('1/a', None), ('1/z', ('1/a',)), } async def test_data_store( flow, scheduler, start, ): """Test that manually set prereqs/outputs are applied to the data store.""" id_ = flow({ 'scheduling': { 'graph': {'R1': 'a => z'}, }, 'runtime': { # register a custom output 'a': {'outputs': {'x': 'xyz'}}, }, }) schd: Scheduler = scheduler(id_) async with start(schd): await schd.update_data_structure() data = schd.data_store_mgr.data[schd.tokens.id] # 1/a not the same data-store object post set and not in pool # post succeeded. task_a_id = schd.pool.get_task(IntegerPoint('1'), 'a').tokens.id # set the 1/a:succeeded prereq of 1/z schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'z')}, [], ['1/a:succeeded'], ['1']) task_z = data[TASK_PROXIES][ schd.pool.get_task(IntegerPoint('1'), 'z').tokens.id ] await schd.update_data_structure() assert task_z.prerequisites[0].satisfied is True # set 1/a:x the task should be waiting with output x satisfied schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, ['x'], [], ['1'] ) await schd.update_data_structure() task_a: PbTaskProxy = data[TASK_PROXIES][task_a_id] assert task_a.state == TASK_STATUS_WAITING assert task_a.outputs['x'].satisfied is True assert task_a.outputs['succeeded'].satisfied is False # set 1/a:succeeded the task should be succeeded with output x sat schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, ['succeeded'], [], ['1'] ) await schd.update_data_structure() task_a: PbTaskProxy = data[TASK_PROXIES][task_a_id] assert task_a.state == TASK_STATUS_SUCCEEDED assert task_a.outputs['x'].satisfied is True assert task_a.outputs['succeeded'].satisfied is True async def test_incomplete_detection( one_conf, flow, scheduler, start, log_filter, ): """It should detect and log finished tasks left with incomplete outputs.""" schd = scheduler(flow(one_conf)) async with start(schd): schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'one')}, ['failed'], [], ['1'] ) assert log_filter(contains='1/one did not complete') async def test_pre_all(flow, scheduler, run): """Ensure that --pre=all is interpreted as a special case and _not_ tokenized. """ id_ = flow({'scheduling': {'graph': {'R1': 'a => z'}}}) schd = scheduler(id_, paused_start=False) async with run(schd) as log: schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'z')}, [], ['all'], [] ) warn_or_higher = [i for i in log.records if i.levelno > 30] assert warn_or_higher == [] async def test_bad_prereq( flow: 'Callable', scheduler: 'Callable', run: 'Callable', complete: 'Callable', caplog: 'pytest.LogCaptureFixture' ): """Attempting to set an invalid prerequisite should not leave a trace in the DB that prevents the target task from spawning. later on. """ id_ = flow({ 'scheduling': { 'graph': {'R1': 'a => b => c'}, }, }) schd = scheduler(id_, paused_start=False) async with run(schd): schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'c')}, [], ['1/a'], [] ) assert schd.pool.get_task_ids() == {'1/a'} assert '1/c does not depend on "1/a:succeeded"' in caplog.text schd.workflow_db_mgr.process_queued_ops() # This will fail if the previous set left 1/c in the DB: schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'c')}, [], ['1/b'], [] ) assert schd.pool.get_task_ids() == {'1/a', '1/c'} await complete(schd, '1/c', timeout=5) async def test_no_outputs_given(flow, scheduler, start): """Test `cylc set` without providing any outputs. It should set the "success pathway" outputs. """ schd: Scheduler = scheduler( flow({ 'scheduling': { 'graph': { 'R1': r""" foo? => alpha foo:submitted? => bravo foo:started? => charlie foo:x => xray # Optional custom outputs not emitted: foo:y? => yankee # Non-success-pathway outputs not emitted: foo:submit-failed? => delta """, }, }, 'runtime': { 'foo': outputs_section('x', 'y'), }, }) ) async with start(schd): foo = schd.pool.get_tasks()[0] await run_cmd( set_prereqs_and_outputs(schd, {foo.tokens}, []) ) assert set(foo.state.outputs.get_completed_outputs()) == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, 'x' } assert schd.pool.get_task_ids() == { '1/alpha', '1/bravo', '1/charlie', '1/xray', } async def test_completion_expr(flow, scheduler, start): """Test `cylc set` without providing any outputs on a task that has a custom completion expression.""" conf = { 'scheduling': { 'graph': { 'R1': 'foo? | foo:x? => bar' }, }, 'runtime': { 'foo': { **outputs_section('x'), 'completion': '(succeeded or x) or failed' }, }, } schd: Scheduler = scheduler(flow(conf)) async with start(schd): foo = schd.pool.get_tasks()[0] await run_cmd( set_prereqs_and_outputs(schd, {foo.tokens}, []) ) assert set(foo.state.outputs.get_completed_outputs()) == { TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, } async def test_set_submitted(flow, scheduler, start): """`cylc set --out submitted` should spawn children that depend on the output, but not affect the task's state.""" schd: Scheduler = scheduler(flow('foo:submitted? => bar')) async with start(schd): foo = schd.pool.get_tasks()[0] await run_cmd( set_prereqs_and_outputs( schd, [foo.identity], [], ['submitted'] ) ) assert set(foo.state.outputs.get_completed_outputs()) == { TASK_OUTPUT_SUBMITTED, } assert schd.pool.get_task_ids() == {'1/foo', '1/bar'} assert foo.state(TASK_STATUS_WAITING) async def test_job_state(flow, scheduler, start, db_select): """Test that setting outputs does not change the job state.""" schd: Scheduler = scheduler(flow('foo => bar')) fail_time = '1950-01-01T00:00:00Z' def db_task_states(itask: TaskProxy): return db_select( schd, True, 'task_states', 'status', name=itask.tdef.name ) def assert_job_failed(itask: TaskProxy): """...in the DB and data store.""" assert db_select( schd, True, 'task_jobs', 'run_status', 'time_run_exit', name=itask.tdef.name, ) == [(1, fail_time)] pb_job: PbJob = schd.data_store_mgr.data[schd.tokens.id][JOBS][ itask.job_tokens.id ] assert pb_job.state == TASK_STATUS_FAILED assert pb_job.finished_time == fail_time async with start(schd): foo = schd.pool.get_tasks()[0] schd.submit_task_jobs([foo]) schd.task_events_mgr.process_message( foo, 'INFO', TASK_OUTPUT_FAILED, event_time=fail_time ) await schd.update_data_structure() assert foo.state(TASK_STATUS_FAILED) assert db_task_states(foo) == [(TASK_STATUS_FAILED,)] assert_job_failed(foo) await run_cmd( set_prereqs_and_outputs(schd, [foo.identity], []) ) await schd.update_data_structure() # Task is now succeeded: assert foo.state(TASK_STATUS_SUCCEEDED) assert db_task_states(foo) == [(TASK_STATUS_SUCCEEDED,)] # But job is still failed: assert_job_failed(foo) async def test_set_already_succeeded( flow, scheduler, run, complete, db_select ): """Doing `cylc set` on a task that has already succeeded should not change anything.""" schd: Scheduler = scheduler( flow('foo => bar'), paused_start=False, ) def db_task_states(itask: TaskProxy): return db_select( schd, True, 'task_states', 'submit_num', 'status', 'time_updated', name=itask.tdef.name, ) def data_store_task_state(itask: TaskProxy): return schd.data_store_mgr.data[schd.tokens.id][TASK_PROXIES][ itask.tokens.id ].state async with run(schd): foo = schd.pool.get_tasks()[0] await complete(schd, foo.identity) time_updated = db_task_states(foo)[0][2] expected = [(1, TASK_STATUS_SUCCEEDED, time_updated)] assert db_task_states(foo) == expected assert data_store_task_state(foo) == TASK_STATUS_SUCCEEDED await run_cmd( set_prereqs_and_outputs(schd, [foo.identity], []) ) assert foo.state(TASK_STATUS_SUCCEEDED) await schd.update_data_structure() assert db_task_states(foo) == expected assert data_store_task_state(foo) == TASK_STATUS_SUCCEEDED async def test_timings(one_conf, flow, scheduler, start, caplog): """Test that setting outputs does not change the task timings.""" wid = flow({ **one_conf, 'runtime': { 'one': { 'execution time limit': 'PT100S', }, }, }) schd: Scheduler = scheduler(wid) async with start(schd): itask = schd.pool.get_tasks()[0] def check_times(): assert not itask.tdef.elapsed_times assert task_mean_elapsed_time(itask.tdef) == 100 itask.state_reset(TASK_STATUS_PREPARING) schd.task_events_mgr.process_message( itask, 'INFO', TASK_OUTPUT_STARTED ) check_times() await run_cmd(set_prereqs_and_outputs(schd, [itask.identity], [])) check_times() async def test_set_clears_modifiers(flow, scheduler, start): """Manually setting a final output on a task should clear some states. The is_held and is_retry task states should be cleared if a task is manually set to a final state as these states become moot and confusing. See https://github.com/cylc/cylc-flow/issues/7065 """ id_ = flow( { 'scheduling': { 'graph': {'R1': 'a => b'}, }, 'runtime': { 'a': { 'outputs': { 'x': 'xxx', }, }, }, }, ) schd = scheduler(id_) async with start(schd): task_a = schd.pool.get_task(IntegerPoint('1'), 'a') assert task_a # line up both submission and execution retries schd.task_events_mgr._retry_task(task_a, 0, True) schd.task_events_mgr._retry_task(task_a, 0, False) # mark the task as held schd.pool.hold_tasks({task_a.tokens}) # update the data store await schd.update_data_structure() task_a_data_store = schd.data_store_mgr.data[schd.id]['task_proxies'][ task_a.tokens.id ] # the task should have the states is_held and is_retry assert task_a.state.is_held is True assert task_a_data_store.is_retry is True assert task_a_data_store.is_retry is True # set a custom output await run_cmd( set_prereqs_and_outputs(schd, [task_a.identity], [], ['x']) ) # the is_held and is_retry statuses should be unchanged assert task_a.state.is_held is True assert task_a_data_store.is_retry is True assert task_a_data_store.is_retry is True # manually complete the task await run_cmd( set_prereqs_and_outputs( schd, [task_a.identity], [], [TASK_OUTPUT_SUCCEEDED] ) ) await schd.update_data_structure() # the is_held and is_retry states should have been cleared assert task_a.state.is_held is False assert task_a_data_store.is_retry is False assert task_a_data_store.is_retry is False cylc-flow-8.6.4/tests/integration/scripts/test_show.py0000664000175000017500000002130715202510242023355 0ustar alastairalastair# 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 . import json import pytest import re from types import SimpleNamespace from colorama import init as colour_init from cylc.flow.id import Tokens from cylc.flow.scripts.show import ( ShowOptions, show, ) RE_STATE = re.compile('state:.*') @pytest.fixture(scope='module') def mod_my_conf(): """A workflow configuration with some workflow metadata.""" return { 'meta': { 'title': 'Workflow Title', 'description': """ My multiline description. """, 'URL': 'http://ismycomputerturnedon.com/', 'answer': '42', }, 'scheduling': { 'graph': { 'R1': 'foo' } }, 'runtime': { 'foo': { 'meta': { 'title': 'Task Title', 'description': ''' Task multiline description ''', 'URL': ( 'http://hasthelargehadroncollider' 'destroyedtheworldyet.com/' ), 'question': 'mutually exclusive', }, }, }, } @pytest.fixture(scope='module') async def mod_my_schd(mod_flow, mod_scheduler, mod_start, mod_my_conf): """A "started" workflow.""" id_ = mod_flow(mod_my_conf) schd = mod_scheduler(id_) async with mod_start(schd): yield schd async def test_workflow_meta_query(mod_my_schd, capsys): """It should fetch workflow metadata.""" colour_init(strip=True, autoreset=True) opts = SimpleNamespace( comms_timeout=5, json=False, list_prereqs=False, task_defs=None, ) # plain output ret = await show(mod_my_schd.workflow, [], opts) assert ret == 0 out, err = capsys.readouterr() assert out.splitlines() == [ 'title: Workflow Title', 'description: My', 'multiline', 'description.', 'answer: 42', 'URL: http://ismycomputerturnedon.com/', ] # json output opts.json = True ret = await show(mod_my_schd.workflow, [], opts) assert ret == 0 out, err = capsys.readouterr() assert json.loads(out) == { 'title': 'Workflow Title', 'description': 'My\nmultiline\ndescription.', 'answer': '42', 'URL': 'http://ismycomputerturnedon.com/', } async def test_task_meta_query(mod_my_schd, capsys): """It should fetch task metadata.""" colour_init(strip=True, autoreset=True) opts = SimpleNamespace( comms_timeout=5, json=False, list_prereqs=False, task_defs=['foo'], ) # plain output ret = await show( mod_my_schd.workflow, None, opts, ) assert ret == 0 out, err = capsys.readouterr() assert out.splitlines() == [ 'title: Task Title', 'question: mutually exclusive', 'description: Task', 'multiline', 'description', 'URL: http://hasthelargehadroncolliderdestroyedtheworldyet.com/', ] # json output opts.json = True ret = await show(mod_my_schd.workflow, [], opts) assert ret == 0 out, err = capsys.readouterr() assert json.loads(out) == { 'foo': { 'title': 'Task Title', 'question': 'mutually exclusive', 'description': 'Task\nmultiline\ndescription', 'URL': 'http://hasthelargehadroncolliderdestroyedtheworldyet.com/', } } async def test_task_instance_query( flow, scheduler, start, capsys ): """It should fetch task instance data, sorted by task name.""" colour_init(strip=True, autoreset=True) opts = SimpleNamespace( comms_timeout=5, json=False, task_defs=None, list_prereqs=False, ) schd = scheduler( flow( { 'scheduling': { 'graph': {'R1': 'zed & dog & cat & ant'}, }, }, ), paused_start=False, ) async with start(schd): await schd.update_data_structure() ret = await show( schd.workflow, [Tokens('//1/*')], opts, ) assert ret == 0 out, _ = capsys.readouterr() assert [ line for line in out.splitlines() if line.startswith("Task ID") ] == [ # results should be sorted 'Task ID: 1/ant', 'Task ID: 1/cat', 'Task ID: 1/dog', 'Task ID: 1/zed', ] @pytest.mark.parametrize( 'workflow_run_mode, run_mode_info', ( ('live', 'Skip'), ('dummy', 'Dummy'), ('simulation', 'Simulation'), ) ) @pytest.mark.parametrize( 'attributes_bool, flow_nums, expected_state, expected_flows', [ pytest.param( False, [1], 'state: waiting (run mode={})', None, ), pytest.param( True, [1, 2], 'state: waiting (held,queued,runahead,run mode={})', 'flows: [1,2]', ) ] ) async def test_task_instance_state_flows( flow, scheduler, start, capsys, workflow_run_mode, run_mode_info, attributes_bool, flow_nums, expected_state, expected_flows ): """It should print task instance state, attributes, and flows.""" colour_init(strip=True, autoreset=True) opts = SimpleNamespace( comms_timeout=5, json=False, task_defs=None, list_prereqs=False, ) schd = scheduler( flow( { 'scheduling': { 'graph': {'R1': 'a'}, }, 'runtime': { 'a': {'run mode': 'skip'} } }, ), paused_start=True, run_mode=workflow_run_mode, ) async with start(schd): [itask] = schd.pool.get_tasks() itask.state_reset( is_held=attributes_bool, is_queued=attributes_bool, is_runahead=attributes_bool ) itask.flow_nums = set(flow_nums) schd.pool.data_store_mgr.delta_task_held( itask.tdef.name, itask.point, itask.state.is_held) schd.pool.data_store_mgr.delta_task_state(itask) schd.pool.data_store_mgr.delta_task_flow_nums(itask) await schd.update_data_structure() ret = await show( schd.workflow, [Tokens('//1/*')], opts, ) assert ret == 0 out, _ = capsys.readouterr() assert [ line for line in out.splitlines() if line.startswith("state:") ] == [ expected_state.format(run_mode_info), ] if expected_flows is not None: assert [ line for line in out.splitlines() if line.startswith("flows:") ] == [ expected_flows, ] async def test_task_run_mode_changes(flow, scheduler, start, capsys): """Broadcasting a change of run mode changes run mode shown by cylc show. """ opts = ShowOptions() schd = scheduler( flow({'scheduling': {'graph': {'R1': 'a'}}}), run_mode='live' ) async with start(schd): # Control: No mode set, the Run Mode setting is not shown: await schd.update_data_structure() ret = await show( schd.workflow, [Tokens('//1/a')], opts, ) assert ret == 0 out, _ = capsys.readouterr() state, = RE_STATE.findall(out) assert 'waiting' in state # Broadcast change task to skip mode: schd.broadcast_mgr.put_broadcast(['1'], ['a'], [{'run mode': 'skip'}]) await schd.update_data_structure() # show now shows skip mode: ret = await show( schd.workflow, [Tokens('//1/a')], opts, ) assert ret == 0 out, _ = capsys.readouterr() state, = RE_STATE.findall(out) assert 'run mode=Skip' in state cylc-flow-8.6.4/tests/integration/scripts/test_back_compat_flow_all.py0000664000175000017500000000671415202510242026524 0ustar alastairalastair# 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 . """Test set, trigger, and remove command back-compat with --flow=all. This option is now just the default (flows = ['all'] -> []). BACK COMPAT: handle --flow=all from pre-8.5 clients """ import pytest from cylc.flow.commands import ( force_trigger_tasks, remove_tasks, run_cmd, set_prereqs_and_outputs, ) from cylc.flow.exceptions import InputError from cylc.flow.scheduler import Scheduler from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED async def test_back_compat_flow_all(flow, scheduler, start): """Handle --flow=all from old clients. The trigger, set, and remove commmands no longer take --flow=all, but for a while we need to handle that option coming in from older clients. (Prior to 8.5 it was the schema default for remove, and was documented as an option for set and trigger). """ conf = { 'scheduling': { 'graph': { 'R1': 'a & b' }, }, } schd: Scheduler = scheduler(flow(conf)) async with start(schd): foo, bar = schd.pool.get_tasks() # For comparison, set should fail with an illegal flow value (allx) with pytest.raises( InputError, match="Flow values must be integers, or 'new', or 'none'" ): await run_cmd( set_prereqs_and_outputs(schd, [foo.identity], ['allx']) ) # but the old --flow=all is OK - it converts to the default. await run_cmd( set_prereqs_and_outputs(schd, [foo.identity], ['all']) ) assert ( TASK_OUTPUT_SUCCEEDED in foo.state.outputs.get_completed_outputs() ) # For comparison, trigger should fail with an illegal flow value (allx) with pytest.raises( InputError, match="Flow values must be integers, or 'new', or 'none'" ): await run_cmd( force_trigger_tasks(schd, [bar.identity], ['allx']) ) # but the old --flow=all is OK - it converts to the default. await run_cmd( force_trigger_tasks(schd, [bar.identity], ['all']) ) assert bar in schd.pool.tasks_to_trigger_now # For comparison, remove should fail with an illegal flow value (allx) with pytest.raises( InputError, match="Flow values must be integers" ): await run_cmd( remove_tasks(schd, [bar.identity], ['allx']) ) # but the old --flow=all is OK - it converts to the default. await run_cmd( remove_tasks(schd, [bar.identity], ['all']) ) assert bar not in schd.pool.get_tasks() assert bar not in schd.pool.tasks_to_trigger_now cylc-flow-8.6.4/tests/integration/scripts/test_dump.py0000664000175000017500000000624715202510242023350 0ustar alastairalastair# 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 . """Test the "cylc dump" command.""" import pytest from cylc.flow.option_parsers import ( Options, ) from cylc.flow.scripts.dump import ( dump, get_option_parser, ) DumpOptions = Options(get_option_parser()) async def test_dump_tasks(flow, scheduler, start): """It should show n=0 tasks. See: https://github.com/cylc/cylc-flow/pull/5600 """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'true', }, 'scheduling': { 'graph': { 'R1': 'a => b => c', }, }, }) schd = scheduler(id_) async with start(schd): # schd.release_tasks_to_run() await schd.update_data_structure() ret = [] await dump( id_, DumpOptions(disp_form='tasks', legacy_format=True), write=ret.append ) assert ret == ['a, 1, waiting, not-held, queued, not-runahead'] @pytest.mark.parametrize( 'attributes_bool, flow_nums, dump_str', [ pytest.param( True, [1, 2], '1/a:waiting (held,queued,runahead) flows=[1,2]', id='1' ), pytest.param( False, [1, 2], '1/a:waiting', id='2' ) ] ) async def test_dump_format( flow, scheduler, start, attributes_bool, flow_nums, dump_str ): """Check the new "cylc dump" output format, i.e. task IDs. See: https://github.com/cylc/cylc-flow/pull/6440 """ id_ = flow({ 'scheduler': { 'allow implicit tasks': 'true', }, 'scheduling': { 'graph': { 'R1': 'a', }, }, }) schd = scheduler(id_) async with start(schd): [itask] = schd.pool.get_tasks() itask.state_reset( is_held=attributes_bool, is_runahead=attributes_bool, is_queued=attributes_bool ) itask.flow_nums = set(flow_nums) schd.pool.data_store_mgr.delta_task_held( itask.tdef.name, itask.point, itask.state.is_held) schd.pool.data_store_mgr.delta_task_state(itask) schd.pool.data_store_mgr.delta_task_flow_nums(itask) await schd.update_data_structure() ret = [] await dump( id_, DumpOptions(disp_form='tasks', show_flows=attributes_bool), write=ret.append ) assert ret == [dump_str] cylc-flow-8.6.4/tests/integration/scripts/test_cat_log.py0000664000175000017500000000655715202510242024017 0ustar alastairalastair# 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 . """Integration test for the cat log script. """ import pytest import re import shutil from cylc.flow.exceptions import InputError from cylc.flow.option_parsers import Options from cylc.flow.scripts.cat_log import ( _main as cat_log, get_option_parser as cat_log_gop ) BAD_NAME = "NONEXISTENTWORKFLOWNAME" @pytest.fixture def brokendir(run_dir): brokendir = (run_dir / BAD_NAME) brokendir.mkdir(exist_ok=True) yield brokendir shutil.rmtree(brokendir) def test_fail_no_file(flow): """It produces a helpful error if there is no workflow log file. """ parser = cat_log_gop() id_ = flow({}) with pytest.raises(InputError, match='Log file not found.'): cat_log(parser, Options(parser)(), id_) def test_fail_rotation_out_of_range(flow): """It produces a helpful error if rotation number > number of log files. """ parser = cat_log_gop() id_ = flow({}) path = flow.args[1] name = id_.split('/')[-1] logpath = (path / name / 'log/scheduler') logpath.mkdir(parents=True) (logpath / '01-start-01.log').touch() with pytest.raises(SystemExit): cat_log(parser, Options(parser)(rotation_num=0), id_) msg = r'--rotation 1 invalid \(max value is 0\)' with pytest.raises(InputError, match=msg): cat_log(parser, Options(parser)(rotation_num=1), id_) def test_bad_workflow(run_dir): """Test "cylc cat-log" with bad workflow name.""" parser = cat_log_gop() msg = re.compile( fr'^Workflow ID not found: {BAD_NAME}' fr'\n\(Directory not found: {run_dir}/{BAD_NAME}\)$', re.MULTILINE ) with pytest.raises(InputError, match=msg): cat_log(parser, Options(parser)(filename='l'), BAD_NAME) def test_bad_workflow2(run_dir, brokendir, capsys): """Check a non existent file in a valid workflow results in error. """ parser = cat_log_gop() with pytest.raises(SystemExit, match='1'): cat_log( parser, Options(parser)(filename='j'), BAD_NAME ) msg = ( f'File not found: {run_dir}' '/NONEXISTENTWORKFLOWNAME/log/j\n') assert capsys.readouterr().err == msg def test_bad_task_dir(run_dir, brokendir, capsys): """Check a non existent job log dir in a valid workflow results in error. """ parser = cat_log_gop() with pytest.raises(SystemExit, match='1'): cat_log( parser, Options(parser)(mode='list-dir'), BAD_NAME + "//1/foo" ) msg = ( f'Directory not found: {run_dir}' '/NONEXISTENTWORKFLOWNAME/log/job/1/foo/NN\n') assert capsys.readouterr().err == msg cylc-flow-8.6.4/tests/integration/reftests/0000775000175000017500000000000015202510242021131 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/reftests/test_cyclers.py0000664000175000017500000001036615202510242024214 0ustar alastairalastair# 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 . async def test_360_calendar(flow, scheduler, reftest): """Test 360 day calendar.""" wid = flow({ 'scheduling': { 'initial cycle point': '2013-02-28', 'final cycle point': '2013-03-01', 'cycling mode': '360day', 'graph': { 'P1D': 'foo[-P1D] => foo' }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20130228T0000Z/foo', ('20130227T0000Z/foo',)), ('20130229T0000Z/foo', ('20130228T0000Z/foo',)), ('20130230T0000Z/foo', ('20130229T0000Z/foo',)), ('20130301T0000Z/foo', ('20130230T0000Z/foo',)), } async def test_365_calendar(flow, scheduler, reftest): """Test 365 day calendar.""" wid = flow({ 'scheduling': { 'initial cycle point': '2012-02-28', 'final cycle point': '2012-03-01', 'cycling mode': '365day', 'graph': { 'P1D': 'foo[-P1D] => foo' }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20120228T0000Z/foo', ('20120227T0000Z/foo',)), ('20120301T0000Z/foo', ('20120228T0000Z/foo',)), } async def test_366_calendar(flow, scheduler, reftest): """Test 366 day calendar.""" wid = flow({ 'scheduling': { 'initial cycle point': '2013-02-28', 'final cycle point': '2013-03-01', 'cycling mode': '366day', 'graph': { 'P1D': 'foo[-P1D] => foo' }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20130228T0000Z/foo', ('20130227T0000Z/foo',)), ('20130229T0000Z/foo', ('20130228T0000Z/foo',)), ('20130301T0000Z/foo', ('20130229T0000Z/foo',)), } async def test_icp_fcp_notation(flow, scheduler, reftest): """Test initial and final cycle point special notation (^, $)""" wid = flow({ 'scheduling': { 'initial cycle point': '2016-01-01', 'final cycle point': '2016-01-02', 'graph': { 'R1': 'foo', 'R1/^': 'bar', 'R1/^+PT1H': 'baz', 'R1/$-PT1H': 'boo', 'R1/$': 'foo[^] & bar[^] & baz[^+PT1H] & boo[^+PT23H] => bot' }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20160101T0000Z/foo', None), ('20160101T0000Z/bar', None), ('20160101T0100Z/baz', None), ('20160101T2300Z/boo', None), ( '20160102T0000Z/bot', ( '20160101T0000Z/bar', '20160101T0000Z/foo', '20160101T0100Z/baz', '20160101T2300Z/boo', ), ), } async def test_recurrence_format_1(flow, scheduler, reftest): """Test ISO 8601 recurrence format no. 1 with unbounded repetitions.""" wid = flow({ 'scheduler': { 'cycle point format': 'CCYY-MM-DD', }, 'scheduling': { 'initial cycle point': '2010-01-01', 'final cycle point': '2010-01-10', 'graph': { 'R/2010-01-01/2010-01-04': 'worf', # 3-day interval }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('2010-01-01/worf', None), ('2010-01-04/worf', None), ('2010-01-07/worf', None), ('2010-01-10/worf', None), } cylc-flow-8.6.4/tests/integration/reftests/test_pre_initial.py0000664000175000017500000001230115202510242025036 0ustar alastairalastair# 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 . async def test_basic(flow, scheduler, reftest): """Test simplification of basic conditionals in pre-initial cycling""" wid = flow({ 'scheduling': { 'initial cycle point': '2010-01-01', 'final cycle point': '2010-01-02', 'graph': { 'T00': 'a[-P1D] & b => a', }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20100101T0000Z/b', None), ('20100102T0000Z/b', None), ('20100101T0000Z/a', ('20091231T0000Z/a', '20100101T0000Z/b')), ('20100102T0000Z/a', ('20100101T0000Z/a', '20100102T0000Z/b')), } async def test_advanced(flow, scheduler, reftest): """Test nested conditional simplification for pre-initial cycling.""" wid = flow({ 'scheduling': { 'initial cycle point': '2010-01-01', 'final cycle point': '2010-01-02', 'graph': { 'PT6H': '(a[-PT6H] & b) & c[-PT6H] => a & c', }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20100101T0000Z/b', None), ('20100101T0600Z/b', None), ( '20100101T0000Z/a', ('20091231T1800Z/a', '20091231T1800Z/c', '20100101T0000Z/b'), ), ( '20100101T0000Z/c', ('20091231T1800Z/a', '20091231T1800Z/c', '20100101T0000Z/b'), ), ('20100101T1200Z/b', None), ( '20100101T0600Z/a', ('20100101T0000Z/a', '20100101T0000Z/c', '20100101T0600Z/b'), ), ( '20100101T0600Z/c', ('20100101T0000Z/a', '20100101T0000Z/c', '20100101T0600Z/b'), ), ('20100101T1800Z/b', None), ( '20100101T1200Z/a', ('20100101T0600Z/a', '20100101T0600Z/c', '20100101T1200Z/b'), ), ( '20100101T1200Z/c', ('20100101T0600Z/a', '20100101T0600Z/c', '20100101T1200Z/b'), ), ('20100102T0000Z/b', None), ( '20100101T1800Z/c', ('20100101T1200Z/a', '20100101T1200Z/c', '20100101T1800Z/b'), ), ( '20100101T1800Z/a', ('20100101T1200Z/a', '20100101T1200Z/c', '20100101T1800Z/b'), ), ( '20100102T0000Z/a', ('20100101T1800Z/a', '20100101T1800Z/c', '20100102T0000Z/b'), ), ( '20100102T0000Z/c', ('20100101T1800Z/a', '20100101T1800Z/c', '20100102T0000Z/b'), ), } async def test_drop(flow, scheduler, reftest): """Test the case of dropping a conditional based on pre-initial cycling""" wid = flow({ 'scheduling': { 'initial cycle point': '2010-01-01', 'final cycle point': '2010-01-02', 'graph': { 'PT6H': 'a[-PT6H] & b[-PT6H] => a => b', }, }, }) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20100101T0000Z/a', ('20091231T1800Z/a', '20091231T1800Z/b')), ('20100101T0000Z/b', ('20100101T0000Z/a',)), ('20100101T0600Z/a', ('20100101T0000Z/a', '20100101T0000Z/b')), ('20100101T0600Z/b', ('20100101T0600Z/a',)), ('20100101T1200Z/a', ('20100101T0600Z/a', '20100101T0600Z/b')), ('20100101T1200Z/b', ('20100101T1200Z/a',)), ('20100101T1800Z/a', ('20100101T1200Z/a', '20100101T1200Z/b')), ('20100101T1800Z/b', ('20100101T1800Z/a',)), ('20100102T0000Z/a', ('20100101T1800Z/a', '20100101T1800Z/b')), ('20100102T0000Z/b', ('20100102T0000Z/a',)), } async def test_over_bracketed(flow, scheduler, reftest, validate): """Test nested conditional simplification for pre-initial cycling.""" wid = flow({ 'scheduling': { 'initial cycle point': '2013-12-25T12:00Z', 'final cycle point': '2013-12-25T12:00Z', 'graph': { 'T12': ''' (a[-P1D]:fail? | b[-P1D]:fail? | c[-P1D]:fail?) => d a? & b? & c? # Implied by implicit cycling now... ''', }, }, }) validate(wid) schd = scheduler(wid, paused_start=False) assert await reftest(schd) == { ('20131225T1200Z/c', None), ( '20131225T1200Z/d', ('20131224T1200Z/a', '20131224T1200Z/b', '20131224T1200Z/c'), ), ('20131225T1200Z/a', None), ('20131225T1200Z/b', None), } cylc-flow-8.6.4/tests/integration/reftests/test_triggering.py0000664000175000017500000000233215202510242024703 0ustar alastairalastair# 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 . async def test_fail(flow, scheduler, reftest): """Test triggering on :fail""" id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo:failed => bar' } }, 'runtime': { 'foo': { 'simulation': {'fail cycle points': 'all'} } } }) schd = scheduler(id_, paused_start=False) assert await reftest(schd) == { ('1/foo', None), ('1/bar', ('1/foo',)), } cylc-flow-8.6.4/tests/integration/conftest.py0000664000175000017500000005360515202510242021502 0ustar alastairalastair# 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 . """Default fixtures for functional tests.""" import asyncio from functools import partial from pathlib import Path import re from shutil import rmtree from time import time from typing import ( TYPE_CHECKING, List, Set, Tuple, Union, ) import pytest from cylc.flow.config import WorkflowConfig from cylc.flow.id import Tokens from cylc.flow.network.client import WorkflowRuntimeClient from cylc.flow.option_parsers import Options from cylc.flow.pathutil import get_cylc_run_dir from cylc.flow.run_modes import RunMode from cylc.flow.rundb import CylcWorkflowDAO from cylc.flow.scripts.install import ( get_option_parser as install_gop, install as cylc_install, ) from cylc.flow.scripts.show import ( ShowOptions, prereqs_and_outputs_query, ) from cylc.flow.scripts.validate import ValidateOptions from cylc.flow.task_state import ( TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, ) from cylc.flow.util import serialise_set from cylc.flow.wallclock import get_current_time_string from cylc.flow.workflow_files import infer_latest_run_from_id from cylc.flow.workflow_status import StopMode from .utils import _rm_if_empty from .utils.flow_tools import ( _make_flow, _make_scheduler, _make_src_flow, _run_flow, _start_flow, ) if TYPE_CHECKING: from cylc.flow.scheduler import Scheduler from cylc.flow.task_proxy import TaskProxy InstallOpts = Options(install_gop()) @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """Expose the result of tests to their fixtures. This will add a variable to the "node" object which differs depending on the scope of the test. scope=function `_function_outcome` will be set to the result of the test function. scope=module `_module_outcome will be set to a list of all test results in the module. https://github.com/pytest-dev/pytest/issues/230#issuecomment-402580536 """ outcome = yield rep = outcome.get_result() # scope==function item._function_outcome = rep # scope==module _module_outcomes = getattr(item.module, '_module_outcomes', {}) _module_outcomes[(item.nodeid, rep.when)] = rep item.module._module_outcomes = _module_outcomes def _pytest_passed(request: pytest.FixtureRequest) -> bool: """Returns True if the test(s) a fixture was used in passed.""" if hasattr(request.node, '_function_outcome'): return request.node._function_outcome.outcome in {'passed', 'skipped'} return all(( report.outcome in {'passed', 'skipped'} for report in request.node.obj._module_outcomes.values() )) @pytest.fixture(scope='session') def run_dir(): """The cylc run directory for this host.""" path = Path(get_cylc_run_dir()) path.mkdir(exist_ok=True) yield path @pytest.fixture(scope='session') def ses_test_dir(request, run_dir): """The root run dir for test flows in this test session.""" timestamp = get_current_time_string(use_basic_format=True) uuid = f'cit-{timestamp}' path = Path(run_dir, uuid) path.mkdir(exist_ok=True) yield path _rm_if_empty(path) @pytest.fixture(scope='module') def mod_test_dir(request, ses_test_dir): """The root run dir for test flows in this test module.""" path = Path( ses_test_dir, # Shorten path by dropping `integration.` prefix: re.sub(r'^integration\.', '', request.module.__name__) ) path.mkdir(exist_ok=True) yield path if _pytest_passed(request): # test passed -> remove all files rmtree(path, ignore_errors=False) else: # test failed -> remove the test dir if empty _rm_if_empty(path) @pytest.fixture def test_dir(request, mod_test_dir): """The root run dir for test flows in this test function.""" path = Path(mod_test_dir, request.function.__name__) path.mkdir(parents=True, exist_ok=True) yield path if _pytest_passed(request): # test passed -> remove all files rmtree(path, ignore_errors=False) else: # test failed -> remove the test dir if empty _rm_if_empty(path) @pytest.fixture(scope='module') def mod_flow(run_dir, mod_test_dir): """A function for creating module-level flows.""" yield partial(_make_flow, run_dir, mod_test_dir) @pytest.fixture def flow(run_dir, test_dir): """A function for creating function-level flows.""" yield partial(_make_flow, run_dir, test_dir) @pytest.fixture(scope='module') def mod_scheduler(): """Return a Scheduler object for a flow. Usage: see scheduler() below """ with _make_scheduler() as _scheduler: yield _scheduler @pytest.fixture def scheduler(): """Return a Scheduler object for a flow. Args: id_ (str): Workflow name. **opts (Any): Options to be passed to the Scheduler. """ with _make_scheduler() as _scheduler: yield _scheduler @pytest.fixture(scope='module') def mod_start(): """Start a scheduler but don't set it running (module scope).""" return partial(_start_flow, None) @pytest.fixture def start(caplog: pytest.LogCaptureFixture): """Start a scheduler but don't set it running.""" return partial(_start_flow, caplog) @pytest.fixture(scope='module') def mod_run(): """Start a scheduler and set it running (module scope).""" return partial(_run_flow, None) @pytest.fixture def run(caplog: pytest.LogCaptureFixture): """Start a scheduler and set it running.""" return partial(_run_flow, caplog) @pytest.fixture def one_conf(): return { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'one' } } } @pytest.fixture(scope='module') def mod_one_conf(): return { 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'one' } } } @pytest.fixture def one(one_conf, flow, scheduler): """Return a Scheduler for the simple "R1 = one" graph.""" id_ = flow(one_conf) schd = scheduler(id_) return schd @pytest.fixture(scope='module') def mod_one(mod_one_conf, mod_flow, mod_scheduler): id_ = mod_flow(mod_one_conf) schd = mod_scheduler(id_) return schd @pytest.fixture(scope='module') def event_loop(): """This fixture defines the event loop used for each test. The default scoping for this fixture is "function" which means that all async fixtures must have "function" scoping. Defining `event_loop` as a module scoped fixture opens the door to module scoped fixtures but means all tests in a module will run in the same event loop. This is fine, it's actually an efficiency win but also something to be aware of. See: https://github.com/pytest-dev/pytest-asyncio/issues/171 """ loop = asyncio.get_event_loop_policy().new_event_loop() yield loop # gracefully exit async generators loop.run_until_complete(loop.shutdown_asyncgens()) # cancel any tasks still running in this event loop for task in asyncio.all_tasks(loop): task.cancel() loop.close() @pytest.fixture def db_select(): """Select columns from workflow database. Args: schd: The Scheduler object for the workflow. process_db_queue: Whether to process the scheduler's db queue before querying. table: The name of the database table to query. *columns (optional): The columns to select from the table. To select all columns, omit or use '*'. **where (optional): Kwargs specifying ='' for use in WHERE clauses. If more than one specified, they will be chained together using an AND operator. """ def _check_columns(table: str, *columns: str) -> None: all_columns = [x[0] for x in CylcWorkflowDAO.TABLES_ATTRS[table]] for col in columns: if col not in all_columns: raise ValueError(f"Column '{col}' not in table '{table}'") def _inner( schd: 'Scheduler', process_db_queue: bool, table: str, *columns: str, **where: str ) -> List[Tuple[str, ...]]: if process_db_queue: schd.process_workflow_db_queue() if table not in CylcWorkflowDAO.TABLES_ATTRS: raise ValueError(f"Table '{table}' not in database") if not columns: columns = ('*',) elif columns != ('*',): _check_columns(table, *columns) stmt = f'SELECT {",".join(columns)} FROM {table}' stmt_args = [] if where: _check_columns(table, *where.keys()) where_stmt = ' AND '.join([ f'{col}=?' for col in where.keys() ]) stmt += f' WHERE {where_stmt}' stmt_args = list(where.values()) with schd.workflow_db_mgr.get_pri_dao() as pri_dao: return list(pri_dao.connect().execute(stmt, stmt_args)) return _inner @pytest.fixture def gql_query(): """Execute a GraphQL query given a workflow runtime client.""" async def _gql_query( client: 'WorkflowRuntimeClient', query_str: str ) -> object: ret = await client.async_request( 'graphql', { 'request_string': 'query { ' + query_str + ' }' } ) return ret return _gql_query @pytest.fixture def validate(run_dir): """Provides a function for validating workflow configurations. Attempts to load the configuration, will raise exceptions if there are errors. Args: id_ - The flow to validate kwargs - Arguments to pass to ValidateOptions """ def _validate(id_: Union[str, Path], **kwargs) -> WorkflowConfig: id_ = str(id_) return WorkflowConfig( id_, str(Path(run_dir, id_, 'flow.cylc')), ValidateOptions(**kwargs) ) return _validate @pytest.fixture(scope='module') def mod_validate(run_dir): """Provides a function for validating workflow configurations. Attempts to load the configuration, will raise exceptions if there are errors. Args: id_ - The flow to validate kwargs - Arguments to pass to ValidateOptions """ def _validate(id_: Union[str, Path], **kwargs) -> WorkflowConfig: id_ = str(id_) return WorkflowConfig( id_, str(Path(run_dir, id_, 'flow.cylc')), ValidateOptions(**kwargs) ) return _validate @pytest.fixture def capture_submission(): """Suppress job submission and capture submitted tasks. Provides a function to run on a Scheduler *whilst started*, use like so: async with start(schd): submitted_tasks = capture_submission(schd) or: async with run(schd): submitted_tasks = capture_submission(schd) """ def _disable_submission(schd: 'Scheduler') -> 'Set[TaskProxy]': submitted_tasks: 'Set[TaskProxy]' = set() def _submit_task_jobs(itasks): for itask in itasks: itask.state_reset(TASK_STATUS_SUBMITTED) submitted_tasks.update(itasks) return itasks schd.submit_task_jobs = _submit_task_jobs # type: ignore return submitted_tasks return _disable_submission @pytest.fixture def capture_polling(): """Suppress job polling and capture polled tasks. Provides a function to run on a started Scheduler. async with start(schd): polled_tasks = capture_polling(schd) or: async with run(schd): polled_tasks = capture_polling(schd) """ def _disable_polling(schd: 'Scheduler') -> 'Set[TaskProxy]': polled_tasks: 'Set[TaskProxy]' = set() def run_job_cmd( _1, itasks, _3, _4=None ): polled_tasks.update(itasks) return itasks schd.task_job_mgr._run_job_cmd = run_job_cmd # type: ignore return polled_tasks return _disable_polling @pytest.fixture(scope='module') def mod_workflow_source(mod_flow, tmp_path_factory): """Create a workflow source directory. Args: cfg: Can be passed a config dictionary. Yields: Path to source directory. """ def _inner(cfg): src_dir = _make_src_flow(tmp_path_factory.getbasetemp(), cfg) return src_dir yield _inner @pytest.fixture def workflow_source(mod_flow, tmp_path): """Create a workflow source directory. Args: cfg: Can be passed a config dictionary. Yields: Path to source directory. """ def _inner(cfg): src_dir = _make_src_flow(tmp_path, cfg) return src_dir yield _inner @pytest.fixture def install(test_dir, run_dir): """Install a workflow from source Args: (Actually args for _inner, but what the fixture appears to take to the user) source: Directory containing the source. **kwargs: Options for cylc install. Returns: Workflow id, including run directory. """ async def _inner(source, **kwargs): opts = InstallOpts(**kwargs) # Note we append the source.name to the string rather than creating # a subfolder because the extra layer of directories would exceed # Cylc install's default limit. opts.workflow_name = ( f'{str(test_dir.relative_to(run_dir))}.{source.name}') workflow_id, _ = await cylc_install(opts, str(source)) workflow_id = infer_latest_run_from_id(workflow_id) return workflow_id yield _inner @pytest.fixture def reflog(): """Integration test version of the --reflog CLI option. This returns a set which captures task triggers. Note, you'll need to call this on the scheduler *after* you have started it. N.B. Trigger order is not stable; using a set ensures that tests check trigger logic rather than binding to specific trigger order which could change in the future, breaking the test. Args: schd: The scheduler to capture triggering information for. flow_nums: If True, the flow numbers of the task being triggered will be added to the end of each entry. Returns: tuple (task, triggers): If flow_nums == False (task, flow_nums, triggers): If flow_nums == True task: The [relative] task ID e.g. "1/a". flow_nums: The serialised flow nums e.g. ["1"]. triggers: Sorted tuple of the trigger IDs, e.g. ("1/a", "2/b"). """ def _reflog(schd: 'Scheduler', flow_nums: bool = False) -> Set[tuple]: submit_task_jobs = schd.submit_task_jobs triggers = set() def _submit_task_jobs(*args, **kwargs): itasks = submit_task_jobs(*args, **kwargs) for itask in itasks: deps = tuple(sorted(itask.state.get_resolved_dependencies())) if flow_nums: triggers.add( ( itask.identity, serialise_set(itask.flow_nums), deps or None, ) ) else: triggers.add((itask.identity, deps or None)) return itasks schd.submit_task_jobs = _submit_task_jobs return triggers return _reflog async def _complete( schd: 'Scheduler', *wait_tokens: Union[Tokens, str], stop_mode=StopMode.AUTO, timeout: int = 60, allow_paused: bool = False, ) -> None: """Wait for the workflow, or tasks within it to complete. Args: schd: The scheduler to await. wait_tokens: If specified, this will wait for the tasks represented by these tokens to be marked as completed by the task pool. Can use relative task ids as strings (e.g. '1/a') rather than tokens for convenience. stop_mode: If tokens_list is not provided, this will wait for the scheduler to be shutdown with the specified mode (default = AUTO, i.e. workflow completed normally). timeout: Max time to wait for the condition to be met. Note, if you need to increase this, you might want to rethink your test. Note, use this timeout rather than wrapping the complete call with async.timeout (handles shutdown logic more cleanly). allow_paused: This function will raise an Exception if the scheduler is paused (because this usually means the sepecified tasks cannot complete) unless allow_paused==True. Raises: AssertionError: In the event the scheduler shut down or the operation timed out. """ if schd.is_paused and not allow_paused: raise Exception( "You are waiting for a paused scheduler - if this is intended " "then use `complete(..., allow_paused=True)`" ) start_time = time() tokens_list: List[Tokens] = [] for tokens in wait_tokens: if isinstance(tokens, str): tokens = Tokens(tokens, relative=True) tokens_list.append(tokens.task) # capture task completion remove_if_complete = schd.pool.remove_if_complete def _remove_if_complete(itask, output=None): ret = remove_if_complete(itask) if ret and itask.tokens.task in tokens_list: tokens_list.remove(itask.tokens.task) return ret # capture workflow shutdown request set_stop = schd._set_stop stop_requested = False def _set_stop(mode=None): nonlocal stop_requested if mode == stop_mode: stop_requested = True return set_stop(mode) else: set_stop(mode) raise Exception(f'Workflow bailed with stop mode = {mode}') # determine the completion condition def done(): if wait_tokens: if not tokens_list: return True if not schd.contact_data: raise AssertionError( "Scheduler shut down before tasks completed: " + ", ".join(map(str, tokens_list)) ) return False # otherwise wait for the scheduler to shut down return stop_requested or not schd.contact_data with pytest.MonkeyPatch.context() as mp: mp.setattr(schd.pool, 'remove_if_complete', _remove_if_complete) mp.setattr(schd, '_set_stop', _set_stop) # wait for the condition to be met while not done(): # allow the main loop to advance await asyncio.sleep(0) if (time() - start_time) > timeout: msg = "Timeout waiting for " if wait_tokens: msg += ", ".join(map(str, tokens_list)) else: msg += "workflow to shut down" raise AssertionError(msg) @pytest.fixture def complete(): return _complete @pytest.fixture(scope='module') def mod_complete(): return _complete @pytest.fixture def reftest(run, reflog, complete): """Fixture that runs a simple reftest. Combines the `reflog` and `complete` fixtures. """ async def _reftest( schd: 'Scheduler', flow_nums: bool = False, ) -> Set[tuple]: async with run(schd): triggers = reflog(schd, flow_nums) await complete(schd) return triggers return _reftest @pytest.fixture def cylc_show(): """Fixture that runs `cylc show` on a scheduler, returning JSON object.""" async def _cylc_show(schd: 'Scheduler', *task_ids: str) -> dict: pclient = WorkflowRuntimeClient(schd.workflow) await schd.update_data_structure() json_filter: dict = {} await prereqs_and_outputs_query( schd.id, [Tokens(id_, relative=True) for id_ in task_ids], pclient, ShowOptions(json=True), json_filter, ) return json_filter return _cylc_show @pytest.fixture def capture_live_submissions(capcall, monkeypatch): """Capture live submission attempts. This prevents real jobs from being submitted to the system. If you call this fixture from a test, it will return a set of tasks that would have been submitted had this fixture not been used. """ def fake_submit(self, itasks, *_): self.submit_nonlive_task_jobs(itasks, RunMode.SIMULATION) for itask in itasks: for status in (TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED): self.task_events_mgr.process_message( itask, 'INFO', status, '2000-01-01T00:00:00Z', '(received)', ) return itasks # suppress and capture live submissions submit_live_calls = capcall( 'cylc.flow.task_job_mgr.TaskJobManager.submit_livelike_task_jobs', fake_submit) def get_submissions(): return { itask.identity for ((_self, itasks, *_), _kwargs) in submit_live_calls for itask in itasks } return get_submissions cylc-flow-8.6.4/tests/integration/test_workflow_db_mgr.py0000664000175000017500000001643715202510242024102 0ustar alastairalastair# 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 . from datetime import datetime, timedelta import pytest import sqlite3 from typing import TYPE_CHECKING from cylc.flow import commands if TYPE_CHECKING: from cylc.flow.scheduler import Scheduler async def test_restart_number( flow, one_conf, start, scheduler, log_filter, db_select ): """The restart number should increment correctly.""" id_ = flow(one_conf) async def test(expected_restart_num: int, do_reload: bool = False): """(Re)start the workflow and check the restart number is as expected. """ schd: 'Scheduler' = scheduler(id_, paused_start=True) async with start(schd): if do_reload: await commands.run_cmd(commands.reload_workflow(schd)) assert schd.workflow_db_mgr.n_restart == expected_restart_num assert log_filter( contains=f"(re)start number={expected_restart_num + 1}" # (In the log, it's 1 higher than backend value) ) assert ('n_restart', f'{expected_restart_num}') in db_select( schd, False, 'workflow_params' ) # First start await test(expected_restart_num=0) # Restart await test(expected_restart_num=1) # Restart + reload - https://github.com/cylc/cylc-flow/issues/4918 await test(expected_restart_num=2, do_reload=True) # Final restart await test(expected_restart_num=3) def db_remove_column(schd: 'Scheduler', table: str, column: str) -> None: """Remove a column from a scheduler DB table. ALTER TABLE DROP COLUMN is not supported by sqlite yet, so we have to copy the table (without the column) and rename it back to the original. """ with schd.workflow_db_mgr.get_pri_dao() as pri_dao: conn = pri_dao.connect() # Get current column names, minus column cursor = conn.execute(f'PRAGMA table_info({table})') desc = cursor.fetchall() c_names = ','.join( [fields[1] for fields in desc if fields[1] != column] ) # Copy table data to a temporary table, and rename it back. conn.execute(rf"CREATE TABLE 'tmp'({c_names})") conn.execute( rf"INSERT INTO 'tmp'({c_names}) SELECT {c_names} FROM {table}") conn.execute(rf"DROP TABLE '{table}'") conn.execute(rf"ALTER TABLE 'tmp' RENAME TO '{table}'") conn.commit() async def test_db_upgrade_pre_803( flow, one_conf, start, scheduler, log_filter, db_select ): """Test scheduler restart with upgrade of pre-8.0.3 DB.""" id_ = flow(one_conf) # Run a scheduler to create a DB. schd: 'Scheduler' = scheduler(id_, paused_start=True) async with start(schd): assert ('n_restart', '0') in db_select(schd, False, 'workflow_params') # Remove task_states:is_manual_submit to fake a pre-8.0.3 DB. db_remove_column(schd, "task_states", "is_manual_submit") db_remove_column(schd, "task_jobs", "flow_nums") schd: 'Scheduler' = scheduler(id_, paused_start=True) # Restart should fail due to the missing column. with pytest.raises(sqlite3.OperationalError): async with start(schd): pass assert ('n_restart', '1') in db_select(schd, False, 'workflow_params') schd: 'Scheduler' = scheduler(id_, paused_start=True) # Run the DB upgrader for version 8.0.2 # (8.0.2 requires upgrade) with schd.workflow_db_mgr.get_pri_dao() as pri_dao: schd.workflow_db_mgr.upgrade_pre_803(pri_dao) # Restart should now succeed. async with start(schd): assert ('n_restart', '2') in db_select(schd, False, 'workflow_params') async def test_workflow_param_rapid_toggle( one_conf, flow, scheduler, run ): """Check that queuing a workflow param toggle operation twice before processing does not cause any problems. https://github.com/cylc/cylc-flow/issues/5593 """ schd: 'Scheduler' = scheduler(flow(one_conf), paused_start=False) async with run(schd): assert schd.is_paused is False schd.pause_workflow() schd.resume_workflow() schd.process_workflow_db_queue() assert schd.is_paused is False w_params = dict(schd.workflow_db_mgr.pri_dao.select_workflow_params()) assert w_params['is_paused'] == '0' async def test_record_only_non_clock_triggers( flow, run, scheduler, complete, db_select ): """Database does not record wall_clock xtriggers. https://github.com/cylc/cylc-flow/issues/5911 Includes: - Not in DB: A normal wall clock xtrigger (wall_clock). - In DB: An xrandom mis-labelled as wall_clock trigger DB). - Not in DB: An execution retry xtrigger. @TODO: Refactor to use simulation mode to speedup after Simulation mode upgrade bugfixes: This should speed this test up considerably. """ rawpoint = '1348' id_ = flow({ "scheduler": { 'cycle point format': '%Y', 'allow implicit tasks': True }, "scheduling": { "initial cycle point": rawpoint, "xtriggers": { "another": "xrandom(100)", "wall_clock": "xrandom(100, _=Not a real wall clock trigger)", "real_wall_clock": "wall_clock()" }, "graph": { "R1": """ @another & @wall_clock & @real_wall_clock => foo @real_wall_clock => bar """ } }, }) schd = scheduler(id_, paused_start=False, run_mode='simulation') async with run(schd): await complete(schd, timeout=20) # Assert that (only) the real clock trigger is not in the db: assert db_select(schd, False, 'xtriggers', 'signature') == [ ('xrandom(100)',), ('xrandom(100, _=Not a real wall clock trigger)',)] async def test_time_zone_writing( one_conf, flow, scheduler, start, db_select, set_timezone ): """Don't store scheduler startup timezone forever. https://github.com/cylc/cylc-flow/issues/6701 """ set_timezone('XXX-19:00') schd = scheduler(flow(one_conf), paused_start=False, run_mode='live') async with start(schd): itask = schd.pool.get_tasks()[0] now = datetime.now().astimezone() set_timezone('XXX-19:17') schd.submit_task_jobs([itask]) # Check the db time_submit: (time_submit,) = db_select(schd, False, 'task_jobs', 'time_submit')[0] time_submit = datetime.strptime(time_submit, '%Y-%m-%dT%H:%M:%S%z') # The submit time should be approx correct: assert ( abs(time_submit - now) < timedelta(seconds=10) ), f"{time_submit} ~= {now}" cylc-flow-8.6.4/tests/integration/test_platforms.py0000664000175000017500000000413015202510242022710 0ustar alastairalastair# 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 . """Integration testing for platforms functionality.""" from cylc.flow.scheduler import Scheduler async def test_prep_submit_task_tries_multiple_platforms( flow, scheduler, start, mock_glbl_cfg ): """Preparation tries multiple platforms within a group if the task platform setting matches a group, and that after all platforms have been tried that the hosts matching that platform group are cleared. See https://github.com/cylc/cylc-flow/pull/6109 """ global_conf = ''' [platforms] [[myplatform]] hosts = broken [[anotherbad]] hosts = broken2 [platform groups] [[mygroup]] platforms = myplatform, anotherbad''' mock_glbl_cfg('cylc.flow.platforms.glbl_cfg', global_conf) wid = flow({ "scheduling": {"graph": {"R1": "foo"}}, "runtime": {"foo": {"platform": "mygroup"}} }) schd: Scheduler = scheduler(wid, run_mode='live') async with start(schd): itask = schd.pool.get_tasks()[0] itask.submit_num = 1 # simulate failed attempts to contact the job hosts schd.bad_hosts.update({'broken', 'broken2'}) res = schd.task_job_mgr._prep_submit_task_job(itask) assert res is False # ensure the bad hosts have been cleared assert not schd.task_job_mgr.bad_hosts cylc-flow-8.6.4/tests/integration/test_stop_after_cycle_point.py0000664000175000017500000001127115202510242025443 0ustar alastairalastair# 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 . """Test logic pertaining to the stop after cycle points. This may be defined in different ways: * In the workflow configuration. * On the command line. * Or loaded from the database. When the workflow hits the "stop after" point, it should be wiped (i.e. set to None). """ from typing import Optional from cylc.flow import commands from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.id import Tokens from cylc.flow.workflow_status import StopMode async def test_stop_after_cycle_point( flow, scheduler, run, reflog, complete, ): """Test the stop after cycle point. This ensures: * The stop after point gets loaded from the config. * The workflow stops when it hits this point. * The point gets wiped when the workflow hits this point. * The point is stored/retrieved from the DB as appropriate. """ async def stops_after_cycle(schd) -> Optional[str]: """Run the workflow until it stops and return the cycle point.""" triggers = reflog(schd) await complete(schd, timeout=2) assert len(triggers) == 1 # only one task (i.e. cycle) should be run return Tokens(list(triggers)[0][0], relative=True)['cycle'] def get_db_value(schd) -> Optional[str]: """Return the cycle point value stored in the DB.""" with schd.workflow_db_mgr.get_pri_dao() as pri_dao: return dict(pri_dao.select_workflow_params())['stopcp'] config = { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': '1', 'stop after cycle point': '1', 'graph': { 'P1': 'a[-P1] => a', }, }, } id_ = flow(config) schd = scheduler(id_, paused_start=False) async with run(schd): # the cycle point should be loaded from the workflow configuration assert schd.config.stop_point == IntegerPoint('1') # this value should *not* be written to the database assert get_db_value(schd) is None # the workflow should stop after cycle 1 assert await stops_after_cycle(schd) == '1' # change the configured cycle point to "2" config['scheduling']['stop after cycle point'] = '2' id_ = flow(config, workflow_id=id_) schd = scheduler(id_, paused_start=False) async with run(schd): # the cycle point should be reloaded from the workflow configuration assert schd.config.stop_point == IntegerPoint('2') # this value should not be written to the database assert get_db_value(schd) is None # the workflow should stop after cycle 2 assert await stops_after_cycle(schd) == '2' # override the configured value via the CLI option schd = scheduler(id_, paused_start=False, **{'stopcp': '3'}) async with run(schd): # the CLI should take precedence over the config assert schd.config.stop_point == IntegerPoint('3') # this value *should* be written to the database assert get_db_value(schd) == '3' # the workflow should stop after cycle 3 assert await stops_after_cycle(schd) == '3' # once the workflow hits this point, it should get cleared assert get_db_value(schd) is None schd = scheduler(id_, paused_start=False) async with run(schd): # the workflow should fall back to the configured value assert schd.config.stop_point == IntegerPoint('2') # override this value whilst the workflow is running await commands.run_cmd( commands.stop( schd, cycle_point=IntegerPoint('4'), mode=StopMode.REQUEST_CLEAN, ) ) assert schd.config.stop_point == IntegerPoint('4') # the new *should* be written to the database assert get_db_value(schd) == '4' schd = scheduler(id_, paused_start=False) async with run(schd): # the workflow should stop after cycle 4 assert await stops_after_cycle(schd) == '4' cylc-flow-8.6.4/tests/integration/test_job_runner_mgr.py0000664000175000017500000001104015202510242023707 0ustar alastairalastair# 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 . import errno import logging from pathlib import Path import re from textwrap import dedent from cylc.flow.job_runner_mgr import JobRunnerManager from cylc.flow.pathutil import get_workflow_run_job_dir from cylc.flow.task_state import TASK_STATUS_RUNNING from cylc.flow.subprocctx import SubProcContext from cylc.flow.task_job_logs import JOB_LOG_OUT, JOB_LOG_ERR async def test_kill_error(one, start, test_dir, capsys, log_filter): """It should report the failure to kill a job.""" async with start(one): # make it look like the task is running itask = one.pool.get_tasks()[0] itask.submit_num += 1 itask.state_reset(TASK_STATUS_RUNNING) # fake job details workflow_job_log_dir = Path(get_workflow_run_job_dir(one.workflow)) job_id = itask.job_tokens.relative_id job_log_dir = Path(workflow_job_log_dir, job_id) # create job status file (give it a fake pid) job_log_dir.mkdir(parents=True) (job_log_dir / 'job.status').write_text(dedent(''' CYLC_JOB_RUNNER_NAME=background CYLC_JOB_ID=99999999 CYLC_JOB_PID=99999999 ''')) # attempt to kill the job using the jobs-kill script # (note this is normally run via a subprocess) capsys.readouterr() JobRunnerManager().jobs_kill(str(workflow_job_log_dir), [job_id]) # the kill should fail, the failure should be written to stdout # (the jobs-kill callback will read this in and handle it) out, err = capsys.readouterr() assert re.search( # # NOTE: ESRCH = no such process rf'TASK JOB ERROR.*{job_id}.*Errno {errno.ESRCH}', out, ) # feed this jobs-kill output into the scheduler # (as if we had run the jobs-kill script as a subprocess) one.task_job_mgr._kill_task_jobs_callback( # mock the subprocess SubProcContext( one.task_job_mgr.JOBS_KILL, ['mock-cmd'], # provide it with the out/err the script produced out=out, err=err, ), [itask], ) # a warning should be logged assert log_filter( regex=r'1/one/01:running.*job kill failed', level=logging.WARNING, ) assert itask.state(TASK_STATUS_RUNNING) async def test_create_nn_new(one, start): """Test _create_nn. It should create the NN symlink. """ async with start(one): itask = one.pool.get_tasks()[0] workflow_job_log_dir = Path(get_workflow_run_job_dir(one.workflow)) job_id = itask.tokens.duplicate(job='01').relative_id job_log_dir = Path(workflow_job_log_dir, job_id) job_log_dir.mkdir(parents=True) # call _create_nn JobRunnerManager()._create_nn(job_log_dir / 'job') # check the symlink exists assert (job_log_dir.parent / "NN").is_symlink() async def test_create_nn_old(one, start): """Test _create_nn. It should remove existing job logs, if the dir already exists. """ async with start(one): itask = one.pool.get_tasks()[0] # fake some old job logs workflow_job_log_dir = Path(get_workflow_run_job_dir(one.workflow)) job_id = itask.tokens.duplicate(job='01').relative_id job_log_dir = Path(workflow_job_log_dir, job_id) job_log_dir.mkdir(parents=True) job_logs = [] for name in JOB_LOG_OUT, JOB_LOG_ERR: job_logs.append(job_log_dir / name) # create the logs for job_log in job_logs: job_log.touch() # call _create_nn JobRunnerManager()._create_nn(job_log_dir / 'job') # check they were removed for job_log in job_logs: assert not job_log.is_file() cylc-flow-8.6.4/tests/integration/README.md0000664000175000017500000000536215202510242020557 0ustar alastairalastair# Integration Tests This directory contains Cylc integration tests. ## How To Run These Tests ```console $ pytest tests/i $ pytest tests/i -n 5 # run up to 5 tests in parallel $ pytest tests/i --dist=no -n0 # turn off xdist (allows --pdb etc) ``` ## What Are Integration Tests Integration tests aren't end-to-end tests. They focus on targeted interactions of multiple modules and may do a bit of monkeypatching to achieve that result. With Cylc this typically involves running workflows. The general approach is: 1) Start a workflow. 2) Put it in a funny state. 3) Test how components interract to handle this state. I.e., the integration test framework runs the scheduler. The only thing it's really cutting out is the CLI. You can do everything, up to and including reference tests with it if so inclined, although that would really be a functional test implemented in Python: async with run(schd) as log: # run the workflow with a timeout of 60 seconds await asyncio.sleep(60) assert reftest(log) == ''' 1/b triggered off [1/a] 1/c triggered off [1/b] ''' For a more integration'y approach to reftests we can do something like this which is essentially just another way of getting the "triggered off" information without having to run the main loop and bring race conditions into play: async with start(schd): assert set(schd.pool.get_tasks()) == {'1/a'} # setting a:succeeded should spawn b schd.command_reset('1/a', 'succeeded') assert set(schd.pool.get_tasks()) == {'1/b'} # setting b:x should spawn c schd.command_reset('1/b', 'x') assert set(schd.pool.get_tasks()) == {'1/b', '1/c'} ## Guidelines Don't write functional tests here: * No sleep statements! * Avoid interaction with the command line. * Avoid testing interaction with other systems. * Don't get workflows to call out to executables. * Put workflows into funny states via artificial means rather than by getting the workflow to actually run to the desired state. * Avoid testing specific log messages or output formats where more general testing is possible. Don't write unit tests here: * No testing of odd methods and functions. * If it runs *really* quickly, it's likely a unit test. ## How To Write Integation Tests Common test patterns are documented in `test_examples.py`. Workflows can be run in two ways: ``` with start(schd): # starts the Scheduler but does not start the main loop # (always the better option if its possible) ... with run(schd): # starts the Scheduler and sets the main loop running await asyncio.sleep(0) # yield control to the main loop ... ``` These methods both shut down the workflow / clean up after themselves. It is necessary to shut down workflows correctly to clean up resorces and running tasks. cylc-flow-8.6.4/tests/integration/test_install.py0000664000175000017500000001333315202510242022354 0ustar alastairalastair# 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 . """Test cylc install.""" import pytest from pathlib import Path from typing import Callable, Tuple from cylc.flow.async_util import pipe from cylc.flow.scripts import scan from cylc.flow.workflow_files import WorkflowFiles from cylc.flow.scripts.install import ( InstallOptions, install_cli ) from .network.test_scan import init_flows from .utils.entry_points import EntryPointWrapper SRV_DIR = Path(WorkflowFiles.Service.DIRNAME) CONTACT = Path(WorkflowFiles.Service.CONTACT) RUN_N = Path(WorkflowFiles.RUN_N) INSTALL = Path(WorkflowFiles.Install.DIRNAME) INSTALLED_MSG = "INSTALLED {wfrun} from" WF_ACTIVE_MSG = '1 run of "{wf}" is already active:' BAD_CONTACT_MSG = "Bad contact file:" @pytest.fixture() def patch_graphql_query( monkeypatch: pytest.MonkeyPatch ): # Define a mocked graphql_query pipe function. @pipe async def _graphql_query(flow, fields, filters=None): flow.update({"status": "running"}) return flow # Swap out the function that cylc.flow.scripts.scan. monkeypatch.setattr( 'cylc.flow.scripts.scan.graphql_query', _graphql_query, ) @pytest.fixture() def src_run_dirs( mock_glbl_cfg: Callable, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> Tuple[Path, Path]: """Create some workflow source and run dirs for testing. Source dirs: /w1 /w2 Run dir: /w1/run1 """ tmp_src_path = tmp_path / 'cylc-src' tmp_run_path = tmp_path / 'cylc-run' tmp_src_path.mkdir() tmp_run_path.mkdir() init_flows( tmp_run_path=tmp_run_path, running=('w1/run1',), tmp_src_path=tmp_src_path, src=('w1', 'w2') ) mock_glbl_cfg( 'cylc.flow.install.glbl_cfg', f''' [install] source dirs = {tmp_src_path} ''' ) monkeypatch.setattr('cylc.flow.pathutil._CYLC_RUN_DIR', tmp_run_path) return tmp_src_path, tmp_run_path async def test_install_scan_no_ping( src_run_dirs: Tuple[Path, Path], capsys: pytest.CaptureFixture, caplog: pytest.LogCaptureFixture ) -> None: """At install, running intances should be reported. Ping = False case: don't query schedulers. """ opts = InstallOptions() opts.no_ping = True await install_cli(opts, id_='w1') out = capsys.readouterr().out assert INSTALLED_MSG.format(wfrun='w1/run2') in out assert WF_ACTIVE_MSG.format(wf='w1') in out # Empty contact file faked with "touch": assert f"{BAD_CONTACT_MSG} w1/run1" in caplog.text await install_cli(opts, id_='w2') out = capsys.readouterr().out assert WF_ACTIVE_MSG.format(wf='w2') not in out assert INSTALLED_MSG.format(wfrun='w2/run1') in out async def test_install_scan_ping( src_run_dirs: Tuple[Path, Path], capsys: pytest.CaptureFixture, caplog: pytest.LogCaptureFixture, patch_graphql_query: Callable ) -> None: """At install, running intances should be reported. Ping = True case: but mock scan's scheduler query method. """ opts = InstallOptions() opts.no_ping = False await install_cli(opts, id_='w1') out = capsys.readouterr().out assert INSTALLED_MSG.format(wfrun='w1/run2') in out assert WF_ACTIVE_MSG.format(wf='w1') in out assert scan.FLOW_STATE_SYMBOLS["running"] in out # Empty contact file faked with "touch": assert f"{BAD_CONTACT_MSG} w1/run1" in caplog.text await install_cli(opts, id_='w2') out = capsys.readouterr().out assert INSTALLED_MSG.format(wfrun='w2/run1') in out assert WF_ACTIVE_MSG.format(wf='w2') not in out async def test_install_gets_back_compat_mode_for_plugins( src_run_dirs: Tuple[Path, Path], monkeypatch: pytest.MonkeyPatch, capcall, capsys: pytest.CaptureFixture, ): """Assert that cylc install will detect whether a workflow should use back compat mode _before_ running pre_configure plugins so that those plugins can use that information. """ # track calls of the check_deprecation method # (this is the thing that sets cylc.flow.flags.back_compat) check_deprecation_calls = capcall( 'cylc.flow.scripts.install.check_deprecation' ) @EntryPointWrapper def failIfDeprecated(*args, **kwargs): """A fake Cylc Plugin entry point""" # print the number of times the check_deprecation method has been # called print(f'CALLS={len(check_deprecation_calls)}') # return a blank result return { 'env': {}, 'template_variables': {}, } # Monkeypatch our fake entry point into iter_entry_points: monkeypatch.setattr( 'cylc.flow.plugins.iter_entry_points', lambda namespace: ( [failIfDeprecated] if namespace == 'cylc.pre_configure' else [] ) ) # install the workflow opts = InstallOptions() await install_cli(opts, id_='w1') # ensure the check_deprecation method was called before the plugin was run assert 'CALLS=1' in capsys.readouterr()[0] cylc-flow-8.6.4/tests/integration/test_compat_mode.py0000664000175000017500000001030415202510242023170 0ustar alastairalastair# 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 . """Tests for Cylc 7 compatibility mode.""" from typing import TYPE_CHECKING from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.data_store_mgr import TASK_PROXIES if TYPE_CHECKING: from cylc.flow.scheduler import Scheduler async def test_blocked_tasks_in_n0(flow, scheduler, run, complete): """Ensure that tasks with no satisfied dependencies remain in the pool. In this example, the "recover" task is not satisfiable because its upstream dependency "foo:failed" will never be satisfied. The unsatisfiable "recover" task should remain in n=0 until removed/completed. See https://github.com/cylc/cylc-flow/issues/4983 """ id_ = flow( { 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'runahead limit': 'P2', 'dependencies': { 'P1': { 'graph': ''' foo:fail => recover foo | recover => bar ''', }, }, }, }, filename='suite.rc', ) schd: 'Scheduler' = scheduler(id_, paused_start=False, debug=True) async with run(schd): # the workflow should run for three cycles, then runahead stall await complete(schd, *(f'{cycle}/bar' for cycle in range(1, 4))) assert schd.is_stalled # the "blocked" recover tasks should remain in the pool assert schd.pool.get_task_ids() == { '1/recover', '2/recover', '3/recover', '4/foo', } # the "blocked" tasks should remain visible in the data store assert { (x.cycle_point, x.graph_depth, x.name) for x in schd.data_store_mgr.data[schd.tokens.id][ TASK_PROXIES ].values() } == { ('1', 1, 'foo'), ('1', 0, 'recover'), ('1', 1, 'bar'), ('2', 1, 'foo'), ('2', 0, 'recover'), ('2', 1, 'bar'), ('3', 1, 'foo'), ('3', 0, 'recover'), ('3', 1, 'bar'), ('4', 0, 'foo'), ('4', 1, 'recover'), ('4', 1, 'bar'), } # remove the unsatisfiable tasks # (i.e. manually implement a suicide trigger) for cycle in range(1, 4): itask = schd.pool.get_task(IntegerPoint(str(cycle)), 'recover') schd.pool.remove(itask, 'suicide-trigger') assert schd.pool.get_task_ids() == { '4/foo', '5/foo', '6/foo', '7/foo', } # the workflow continue into the next three cycles, then stall again # (i.e. the runahead limit should move forward after the removes) await complete(schd, *(f'{cycle}/bar' for cycle in range(4, 7))) assert schd.is_stalled assert { (x.cycle_point, x.graph_depth, x.name) for x in schd.data_store_mgr.data[schd.tokens.id][ TASK_PROXIES ].values() } == { ('4', 1, 'foo'), ('4', 0, 'recover'), ('4', 1, 'bar'), ('5', 1, 'foo'), ('5', 0, 'recover'), ('5', 1, 'bar'), ('6', 1, 'foo'), ('6', 0, 'recover'), ('6', 1, 'bar'), ('7', 0, 'foo'), ('7', 1, 'recover'), ('7', 1, 'bar'), } cylc-flow-8.6.4/tests/integration/run_modes/0000775000175000017500000000000015202510242021265 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/run_modes/test_mode_overrides.py0000664000175000017500000001250315202510242025705 0ustar alastairalastair# 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 . """Test that using [runtime][TASK]run mode works in each mode. Point 3 of the Skip Mode proposal https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md | The run mode should be controlled by a new task configuration | [runtime][]run mode with the default being live. | As a runtime configuration, this can be defined in the workflow | for development / testing purposes or set by cylc broadcast. n.b: This is pretty much a functional test and probably ought to be labelled as such, but uses the integration test framework. """ import pytest from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.id import TaskTokens from cylc.flow.run_modes import WORKFLOW_RUN_MODES, RunMode from cylc.flow.scheduler import Scheduler, SchedulerStop from cylc.flow.task_state import TASK_STATUS_WAITING from cylc.flow.commands import ( run_cmd, force_trigger_tasks ) @pytest.mark.parametrize('workflow_run_mode', sorted(WORKFLOW_RUN_MODES)) async def test_run_mode_override_from_config( capture_live_submissions, flow, scheduler, run, complete, workflow_run_mode, validate ): """Test that `[runtime][]run mode` overrides workflow modes.""" id_ = flow({ 'scheduling': { 'graph': { 'R1': 'live & skip', }, }, 'runtime': { 'live': {'run mode': 'live'}, 'skip': {'run mode': 'skip'}, } }) run_mode = RunMode(workflow_run_mode) validate(id_) schd = scheduler(id_, run_mode=run_mode, paused_start=False) async with run(schd): await complete(schd) if workflow_run_mode == 'live': assert capture_live_submissions() == {'1/live'} elif workflow_run_mode == 'dummy': # Skip mode doesn't override dummy mode: assert capture_live_submissions() == {'1/live', '1/skip'} else: assert capture_live_submissions() == set() async def test_force_trigger_does_not_override_run_mode( flow, scheduler, start, ): """Force-triggering a task will not override the run mode. Taken from spec at https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md#proposal """ wid = flow({ 'scheduling': {'graph': {'R1': 'foo'}}, 'runtime': {'foo': {'run mode': 'skip'}} }) schd = scheduler(wid, run_mode="live") async with start(schd): foo = schd.pool.get_tasks()[0] # Force trigger task: await run_cmd(force_trigger_tasks(schd, ['1/foo'], ['1'])) # ... but job submission will always change this to the correct mode: schd.submit_task_jobs([foo]) assert foo.run_mode.value == 'skip' async def test_run_mode_skip_abides_by_held(flow, scheduler, run): """Tasks with run mode = skip will continue to abide by the is_held flag as normal. Taken from spec at https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md#proposal """ wid = flow({ 'scheduling': {'graph': {'R1': 'foo'}}, 'runtime': {'foo': {'run mode': 'skip'}} }) schd: Scheduler = scheduler(wid, run_mode="live", paused_start=False) async with run(schd): foo = schd.pool.get_tasks()[0] assert not foo.state.is_held # Hold task, check that it's held: schd.pool.hold_tasks({TaskTokens('1', 'foo')}) assert foo.state.is_held await schd._main_loop() assert foo.state(TASK_STATUS_WAITING) schd.pool.release_held_tasks({TaskTokens('1', 'foo')}) assert not foo.state.is_held with pytest.raises(SchedulerStop): # Will shut down as foo has run await schd._main_loop() async def test_run_mode_override_from_broadcast( flow, scheduler, start, complete, log_filter, capture_live_submissions ): """Test that run_mode modifications only apply to one task. """ cfg = { "scheduler": {"cycle point format": "%Y"}, "scheduling": { "initial cycle point": "1000", "final cycle point": "1001", "graph": {"P1Y": "foo"}}, "runtime": { } } id_ = flow(cfg) schd = scheduler(id_, run_mode='live', paused_start=False) async with start(schd): schd.broadcast_mgr.put_broadcast( ['1000'], ['foo'], [{'run mode': 'skip'}]) foo_1000 = schd.pool.get_task(ISO8601Point('1000'), 'foo') foo_1001 = schd.pool.get_task(ISO8601Point('1001'), 'foo') schd.submit_task_jobs([foo_1000, foo_1001]) assert foo_1000.run_mode.value == 'skip' assert capture_live_submissions() == {'1001/foo'} cylc-flow-8.6.4/tests/integration/run_modes/test_nonlive.py0000664000175000017500000001317715202510242024361 0ustar alastairalastair# 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 . import pytest from typing import Any, Dict from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.id import TaskTokens from cylc.flow.scheduler import Scheduler # Define here to ensure test doesn't just mirror code: KGO = { 'live': { 'flow_nums': '[1]', 'is_manual_submit': 0, 'try_num': 1, 'submit_status': 0, 'run_signal': None, 'run_status': 0, # capture_live_submissions fixture submits jobs in sim mode 'platform_name': 'simulation', 'job_runner_name': 'simulation', 'job_id': None, }, 'skip': { 'flow_nums': '[1]', 'is_manual_submit': 0, 'try_num': 1, 'submit_status': 0, 'run_signal': None, 'run_status': 0, 'platform_name': 'skip', 'job_runner_name': 'skip', 'job_id': None, }, } def not_time(data: Dict[str, Any]): """Filter out fields containing times to reduce risk of flakiness""" return {k: v for k, v in data.items() if 'time' not in k} @pytest.fixture def submit_and_check_db(): """Wraps up testing that we want to do repeatedly in test_db_task_jobs. """ def _inner(schd): # Submit task jobs: schd.submit_task_jobs(schd.pool.get_tasks()) # Make sure that db changes are enacted: schd.workflow_db_mgr.process_queued_ops() for mode, kgo in KGO.items(): task_jobs = schd.workflow_db_mgr.pub_dao.select_task_job(1, mode) # Check all non-datetime items against KGO: assert not_time(task_jobs) == kgo, ( f'Mode {mode}: incorrect db entries.') # Check that timestamps have been created: for timestamp in [ 'time_submit', 'time_submit_exit', 'time_run', 'time_run_exit' ]: assert task_jobs[timestamp] is not None return _inner async def test_db_task_jobs( flow, scheduler, start, capture_live_submissions, submit_and_check_db ): """Ensure that task job data is added to the database correctly for each run mode. """ schd: Scheduler = scheduler( flow({ 'scheduling': { 'graph': { 'R1': ' & '.join(KGO) } }, 'runtime': { mode: {'run mode': mode} for mode in KGO }, }), run_mode='live' ) async with start(schd): # Reference all task proxies so we can examine them # at the end of the test: itask_skip = schd.pool.get_task(IntegerPoint('1'), 'skip') itask_live = schd.pool.get_task(IntegerPoint('1'), 'live') submit_and_check_db(schd) # Set outputs to failed: schd.pool.set_prereqs_and_outputs( {TaskTokens('*', 'root')}, ['failed'], [], [] ) submit_and_check_db(schd) # capture_live_submissions fixture submits jobs in sim mode assert itask_live.run_mode.value == 'simulation' assert itask_skip.run_mode.value == 'skip' async def test_db_task_states( one_conf, flow, scheduler, start ): """Test that tasks will have the same information entered into the task state database whichever mode is used. """ conf = one_conf conf['runtime'] = {'one': {'run mode': 'skip'}} schd = scheduler(flow(conf)) async with start(schd): schd.submit_task_jobs(schd.pool.get_tasks()) schd.workflow_db_mgr.process_queued_ops() result = schd.workflow_db_mgr.pri_dao.connect().execute( 'SELECT * FROM task_states').fetchone() # Submit number has been added to the table: assert result[5] == 1 # time_created added to the table assert result[3] async def test_mean_task_time( flow, scheduler, start, complete, capture_live_submissions ): """Non-live tasks are not added to the list of task times, so skipping tasks will not affect how long Cylc expects tasks to run. """ schd = scheduler(flow({ 'scheduling': { 'initial cycle point': '1000', 'final cycle point': '1002', 'graph': {'P1Y': 'foo'}} }), run_mode='live') async with start(schd): itask = schd.pool.get_task(ISO8601Point('10000101T0000Z'), 'foo') assert list(itask.tdef.elapsed_times) == [] # Make the task run in skip mode at one cycle: schd.broadcast_mgr.put_broadcast( ['1000'], ['foo'], [{'run mode': 'skip'}]) # Fake adding some other examples of the task: itask.tdef.elapsed_times.extend([133.0, 132.4]) # Submit two tasks: schd.submit_task_jobs([itask]) # Ensure that the skipped task has succeeded, and that the # number of items in the elapsed_times has not changed. assert itask.state.status == 'succeeded' assert len(itask.tdef.elapsed_times) == 2 cylc-flow-8.6.4/tests/integration/run_modes/test_simulation.py0000664000175000017500000004077015202510242025072 0ustar alastairalastair# 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 . """Test the workings of simulation mode""" import logging from pathlib import Path import pytest from pytest import param from cylc.flow import commands from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.run_modes import RunMode from cylc.flow.run_modes.simulation import sim_time_check async def test_started_trigger(flow, reftest, scheduler): """Does the started task output trigger downstream tasks in sim mode? Long standing Bug discovered in Skip Mode work. https://github.com/cylc/cylc-flow/pull/6039#issuecomment-2321147445 """ schd = scheduler( flow( { 'scheduler': { 'events': { 'stall timeout': 'PT0S', 'abort on stall timeout': True, } }, 'scheduling': {'graph': {'R1': 'a:started => b'}}, } ), paused_start=False, ) assert await reftest(schd) == { ('1/a', None), ('1/b', ('1/a',)) } @pytest.fixture def monkeytime(monkeypatch): """Convenience function monkeypatching time.""" def _inner(time_: int): monkeypatch.setattr('cylc.flow.task_job_mgr.time', lambda: time_) monkeypatch.setattr( 'cylc.flow.run_modes.simulation.time', lambda: time_) return _inner @pytest.fixture def run_simjob(monkeytime): """Run a simulated job to completion. Returns the output status. """ def _run_simjob(schd, point, task): itask = schd.pool.get_task(point, task) itask.state.is_queued = False monkeytime(0) schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) monkeytime(itask.mode_settings.timeout + 1) # Run Time Check assert sim_time_check( schd.task_events_mgr, [itask], schd.workflow_db_mgr ) is True # Capture result process queue. return itask return _run_simjob @pytest.fixture(scope='module') async def sim_time_check_setup( mod_flow, mod_scheduler, mod_start, mod_one_conf, ): schd = mod_scheduler(mod_flow({ 'scheduler': {'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': '1066', 'graph': { 'R1': 'one & fail_all & fast_forward', 'P1Y': 'fail_once & fail_all_submits' } }, 'runtime': { 'one': {}, 'fail_all': { 'simulation': { 'fail cycle points': 'all', 'fail try 1 only': False }, 'outputs': {'foo': 'bar'} }, # This task ought not be finished quickly, but for the speed up 'fast_forward': { 'execution time limit': 'PT1M', 'simulation': {'speedup factor': 2} }, 'fail_once': { 'simulation': { 'fail cycle points': '1066, 1068', } }, 'fail_all_submits': { 'simulation': { 'fail cycle points': '1066', 'fail try 1 only': False, } } } })) async with mod_start(schd): itasks = schd.pool.get_tasks() [schd.task_job_mgr._set_retry_timers(i) for i in itasks] yield schd, itasks def test_false_if_not_running( sim_time_check_setup, monkeypatch ): schd, itasks = sim_time_check_setup itasks = [i for i in itasks if i.state.status != 'running'] # False if task status not running: assert sim_time_check(schd.task_events_mgr, itasks, '') is False @pytest.mark.parametrize( 'itask, point, results', ( # Task fails this CP, first submit. param( 'fail_once', '1066', (True, False, False), id='only-fail-on-submit-1'), # Task succeeds this CP, all submits. param( 'fail_once', '1067', (False, False, False), id='do-not-fail-this-cp'), # Task fails this CP, first submit. param( 'fail_once', '1068', (True, False, False), id='and-another-cp'), # Task fails this CP, all submits. param( 'fail_all_submits', '1066', (True, True, True), id='fail-all-submits'), # Task succeeds this CP, all submits. param( 'fail_all_submits', '1067', (False, False, False), id='fail-no-submits'), ) ) def test_fail_once(sim_time_check_setup, itask, point, results, monkeypatch): """A task with a fail cycle point only fails at that cycle point, and then only on the first submission. """ schd, _ = sim_time_check_setup itask = schd.pool.get_task( ISO8601Point(point), itask) for i, result in enumerate(results): itask.try_timers['execution-retry'].num = i schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert itask.mode_settings.sim_task_fails is result def test_task_finishes(sim_time_check_setup, monkeytime, caplog): """...and an appropriate message sent. Checks that failed and bar are output if a task is set to fail. Does NOT check every possible cause of an outcome - this is done in unit tests. """ schd, _ = sim_time_check_setup monkeytime(0) # Setup a task to fail, submit it. fail_all_1066 = schd.pool.get_task(ISO8601Point('1066'), 'fail_all') fail_all_1066.state.status = 'running' fail_all_1066.state.is_queued = False schd.task_job_mgr.submit_nonlive_task_jobs( [fail_all_1066], RunMode.SIMULATION ) # For the purpose of the test delete the started time set by # submit_nonlive_task_jobs. fail_all_1066.summary['started_time'] = 0 # Before simulation time is up: assert sim_time_check(schd.task_events_mgr, [fail_all_1066], '') is False # Time's up... monkeytime(12) # After simulation time is up it Fails and records custom outputs: assert sim_time_check(schd.task_events_mgr, [fail_all_1066], '') is True outputs = fail_all_1066.state.outputs assert outputs.is_message_complete('succeeded') is False assert outputs.is_message_complete('bar') is True assert outputs.is_message_complete('failed') is True def test_task_sped_up(sim_time_check_setup, monkeytime): """Task will speed up by a factor set in config.""" schd, _ = sim_time_check_setup fast_forward_1066 = schd.pool.get_task( ISO8601Point('1066'), 'fast_forward') # Run the job submission method: monkeytime(0) schd.task_job_mgr.submit_nonlive_task_jobs( [fast_forward_1066], RunMode.SIMULATION ) fast_forward_1066.state.is_queued = False result = sim_time_check(schd.task_events_mgr, [fast_forward_1066], '') assert result is False monkeytime(29) result = sim_time_check(schd.task_events_mgr, [fast_forward_1066], '') assert result is False monkeytime(31) result = sim_time_check(schd.task_events_mgr, [fast_forward_1066], '') assert result is True async def test_settings_restart(monkeytime, flow, scheduler, start): """Check that simulation mode settings are correctly restored upon restart. In the case of start time this is collected from the database from task_jobs.start_time. tasks: one: Runs straighforwardly. two: Test case where database is missing started_time because it was upgraded from an earlier version of Cylc. """ id_ = flow({ 'scheduler': {'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': '1066', 'graph': { 'R1': 'one & two' } }, 'runtime': { 'root': { 'execution time limit': 'PT1M', 'execution retry delays': 'P0Y', 'simulation': { 'speedup factor': 1, 'fail cycle points': 'all', 'fail try 1 only': True, } }, } }) schd = scheduler(id_) # Start the workflow: async with start(schd): og_timeouts = {} for itask in schd.pool.get_tasks(): schd.task_job_mgr.submit_nonlive_task_jobs( [itask], RunMode.SIMULATION ) og_timeouts[itask.identity] = itask.mode_settings.timeout # Mock wallclock < sim end timeout monkeytime(itask.mode_settings.timeout - 1) assert sim_time_check( schd.task_events_mgr, [itask], schd.workflow_db_mgr ) is False # Stop and restart the scheduler: schd = scheduler(id_) async with start(schd): for itask in schd.pool.get_tasks(): # Check that we haven't got mode settings back: assert itask.mode_settings is None if itask.identity == '1066/two': # Delete the database entry for `two`: Ensure that # we don't break sim mode on upgrade to this version of Cylc. schd.workflow_db_mgr.pri_dao.connect().execute( 'UPDATE task_jobs' '\n SET time_submit = NULL' '\n WHERE (name == \'two\')' ) schd.workflow_db_mgr.process_queued_ops() monkeytime(42) else: monkeytime(og_timeouts[itask.identity] - 1) assert sim_time_check( schd.task_events_mgr, [itask], schd.workflow_db_mgr ) is False # Check that the itask.mode_settings is now re-created assert itask.mode_settings.simulated_run_length == 60.0 assert itask.mode_settings.sim_task_fails is True async def test_settings_reload( flow, scheduler, start, run_simjob ): """Check that simulation mode settings are changed for future pseudo jobs on reload. """ id_ = flow({ 'scheduler': {'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': '1066', 'graph': {'R1': 'one'} }, 'runtime': { 'one': { 'execution time limit': 'PT1M', 'execution retry delays': 'P0Y', 'simulation': { 'speedup factor': 1, 'fail cycle points': 'all', 'fail try 1 only': False, } }, } }) schd = scheduler(id_) async with start(schd): # Submit first psuedo-job and "run" to failure: one_1066 = schd.pool.get_task(ISO8601Point('1066'), 'one') itask = run_simjob(schd, one_1066.point, 'one') assert itask.state.outputs.is_message_complete('failed') is False # Modify config as if reinstall had taken place: conf_file = Path(schd.workflow_run_dir) / 'flow.cylc' conf_file.write_text( conf_file.read_text().replace('False', 'True')) # Reload Workflow: await commands.run_cmd(commands.reload_workflow(schd)) # Submit second psuedo-job and "run" to success: itask = run_simjob(schd, one_1066.point, 'one') assert itask.state.outputs.is_message_complete('succeeded') is True async def test_settings_broadcast( flow, scheduler, start, monkeytime, log_filter ): """Assert that broadcasting a change in the settings for a task affects subsequent psuedo-submissions. """ id_ = flow({ 'scheduler': {'cycle point format': '%Y'}, 'scheduling': { 'initial cycle point': '1066', 'graph': {'R1': 'one'} }, 'runtime': { 'one': { 'execution time limit': 'PT1S', 'execution retry delays': '2*PT5S', 'simulation': { 'speedup factor': 1, 'fail cycle points': '1066', 'fail try 1 only': False } }, } }, defaults=False) schd = scheduler(id_, paused_start=False, run_mode='simulation') async with start(schd) as log: itask = schd.pool.get_task(ISO8601Point('1066'), 'one') itask.state.is_queued = False # Submit the first - the sim task will fail: schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert itask.mode_settings.sim_task_fails is True # Let task finish. monkeytime(itask.mode_settings.timeout + 1) assert sim_time_check( schd.task_events_mgr, [itask], schd.workflow_db_mgr ) is True # The mode_settings object has been cleared: assert itask.mode_settings is None # Change a setting using broadcast: schd.broadcast_mgr.put_broadcast( ['1066'], ['one'], [{ 'simulation': {'fail cycle points': ''}, }]) # Submit again - result is different: schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert itask.mode_settings.sim_task_fails is False # Assert that setting run mode on a simulation mode task fails with # warning: good, bad = schd.broadcast_mgr.put_broadcast( ['1066'], ['one'], [{ 'run mode': 'live', }]) assert good == [] assert bad == {'settings': [{'run mode': 'live'}]} record = log_filter(contains='will not be actioned')[0] assert record[0] == logging.WARNING assert 'run mode' not in schd.broadcast_mgr.broadcasts # Assert Clearing the broadcast works schd.broadcast_mgr.clear_broadcast() schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert itask.mode_settings.sim_task_fails is True # Assert that list of broadcasts doesn't change if we submit # Invalid fail cycle points to broadcast. itask.mode_settings = None schd.broadcast_mgr.put_broadcast( ['1066'], ['one'], [{ 'simulation': {'fail cycle points': 'higadfuhasgiurguj'} }]) schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert ( 'Invalid ISO 8601 date representation: higadfuhasgiurguj' in log.messages[-1]) # Check that the invalid broadcast hasn't # changed the itask sim mode settings: assert itask.mode_settings.sim_task_fails is True schd.broadcast_mgr.put_broadcast( ['1066'], ['one'], [{ 'simulation': {'fail cycle points': '1'} }]) schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert ( 'Invalid ISO 8601 date representation: 1' in log.messages[-1]) # Broadcast tasks will reparse correctly: schd.broadcast_mgr.put_broadcast( ['1066'], ['one'], [{ 'simulation': {'fail cycle points': '1945, 1977, 1066'}, 'execution retry delays': '3*PT2S' }]) schd.task_job_mgr.submit_nonlive_task_jobs([itask], RunMode.SIMULATION) assert itask.mode_settings.sim_task_fails is True assert itask.try_timers['execution-retry'].delays == [2.0, 2.0, 2.0] # n.b. rtconfig should remain unchanged, lest we cancel broadcasts: assert itask.tdef.rtconfig['execution retry delays'] == [5.0, 5.0] async def test_db_submit_num( flow, one_conf, scheduler, run, complete, db_select ): """Test simulation mode correctly increments the submit_num in the DB.""" one_conf['runtime'] = { 'one': {'simulation': {'default run length': 'PT0S'}} } schd = scheduler(flow(one_conf), paused_start=False) async with run(schd): await complete(schd, '1/one', timeout=10) assert db_select(schd, False, 'task_states', 'submit_num', 'status') == [ (1, 'succeeded'), ] cylc-flow-8.6.4/tests/integration/run_modes/test_skip.py0000664000175000017500000002225615202510242023653 0ustar alastairalastair# 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 . """Test for skip mode integration. """ from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.id import TaskTokens from cylc.flow.scheduler import Scheduler from cylc.flow.task_outputs import TASK_OUTPUT_FAILED from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_SUCCEEDED, ) async def test_settings_override_from_broadcast( flow, scheduler, start, complete, log_filter ): """Test that skip mode runs differently if settings are modified. """ cfg = { "scheduling": {"graph": {"R1": "foo:failed => bar"}}, "runtime": { "foo": { "events": { 'handler events': 'failed', "handlers": 'echo "HELLO"' } } } } id_ = flow(cfg) schd = scheduler(id_, run_mode='live') async with start(schd): schd.broadcast_mgr.put_broadcast( ['1'], ['foo'], [ {'run mode': 'skip'}, {'skip': {'outputs': 'failed'}}, {'skip': {'disable task event handlers': "False"}} ] ) foo, = schd.pool.get_tasks() schd.submit_task_jobs(schd.pool.get_tasks()) # Run mode has changed: assert foo.platform['name'] == 'skip' # Output failed emitted: assert foo.state.status == 'failed' # After processing events there is a handler in the subprocpool: schd.task_events_mgr.process_events(schd) assert 'echo "HELLO"' in schd.proc_pool.is_not_done()[0][0].cmd async def test_broadcast_changes_set_skip_outputs( flow, scheduler, start ): """When cylc set --out skip is used, task outputs are updated with broadcasts. Skip mode proposal point 4 https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md | The cylc set --out option should accept the skip value which should | set the outputs defined in [runtime][][skip]outputs. | The skip keyword should not be allowed in custom outputs. """ wid = flow({ 'scheduling': {'graph': {'R1': 'foo:x?\nfoo:y?'}}, 'runtime': {'foo': {'outputs': { 'x': 'some message', 'y': 'another message'}}} }) schd = scheduler(wid, run_mode='live') async with start(schd): schd.broadcast_mgr.put_broadcast( ['1'], ['foo'], [{'skip': {'outputs': 'x'}}], ) foo, = schd.pool.get_tasks() schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, ['skip'], [], []) foo_outputs = foo.state.outputs.get_completed_outputs() assert foo_outputs == { 'submitted': '(manually completed)', 'started': '(manually completed)', 'succeeded': '(manually completed)', 'x': '(manually completed)'} async def test_skip_mode_outputs( flow, scheduler, reftest, ): """Skip mode can be configured by the `[runtime][][skip]` section. Skip mode proposal point 2 https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md """ graph = r""" # By default, all required outputs will be generated # plus succeeded if success is optional: foo? & foo:required_out => success_if_optional & required_outs # The outputs submitted and started are always produced # and do not need to be defined in [runtime][X][skip]outputs: foo:submitted => submitted_always foo:started => started_always # If outputs is specified and does not include either # succeeded or failed then succeeded will be produced. opt:optional_out? => optional_outs_produced should_fail:fail => did_fail """ wid = flow({ 'scheduling': {'graph': {'R1': graph}}, 'runtime': { 'root': { 'run mode': 'skip', 'outputs': { 'required_out': 'the plans have been on display...', 'optional_out': 'its only four light years away...' } }, 'opt': { 'skip': { 'outputs': 'optional_out' } }, 'should_fail': { 'skip': { 'outputs': 'failed' } } } }) schd = scheduler(wid, run_mode='live', paused_start=False) assert await reftest(schd) == { ('1/did_fail', ('1/should_fail',),), ('1/foo', None,), ('1/opt', None,), ('1/optional_outs_produced', ('1/opt',),), ('1/required_outs', ('1/foo', '1/foo',),), ('1/should_fail', None,), ('1/started_always', ('1/foo',),), ('1/submitted_always', ('1/foo',),), ('1/success_if_optional', ('1/foo', '1/foo',),), } async def test_doesnt_release_held_tasks( one_conf, flow, scheduler, start, log_filter, capture_live_submissions ): """Point 5 of the proposal https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md | Tasks with run mode = skip will continue to abide by the is_held | flag as normal. """ one_conf['runtime'] = {'one': {'run mode': 'skip'}} schd = scheduler(flow(one_conf), run_mode='live', paused_start=False) async with start(schd): msg = 'held tasks shoudn\'t {}' # Set task to held and check submission in skip mode doesn't happen: schd.pool.hold_tasks({TaskTokens('1', 'one')}) schd.release_tasks_to_run() assert not log_filter(contains='=> running'), msg.format('run') assert not log_filter(contains='=> succeeded'), msg.format('succeed') # Release held task and assert that it now skips successfully: schd.pool.release_held_tasks({TaskTokens('1', 'one')}) schd.release_tasks_to_run() assert log_filter(contains='=> running'), msg.format('run') assert log_filter(contains='=> succeeded'), msg.format('succeed') async def test_prereqs_marked_satisfied_by_skip_mode( flow, scheduler, start, log_filter, complete ): """Point 8 from the skip mode proposal https://github.com/cylc/cylc-admin/blob/master/docs/proposal-skip-mode.md | When tasks are run in skip mode, the prerequisites which correspond | to the outputs they generate should be marked as "satisfied by skip mode" | rather than "satisfied naturally" for provenance reasons. """ schd = scheduler(flow({ 'scheduling': {'graph': {'R1': 'foo => bar'}}, 'runtime': {'foo': {'run mode': 'skip'}} }), run_mode='live') async with start(schd): foo = schd.pool.get_task(IntegerPoint(1), 'foo') schd.submit_task_jobs([foo]) bar = schd.pool.get_task(IntegerPoint(1), 'bar') satisfied_message, = bar.state.prerequisites[0]._satisfied.values() assert satisfied_message == 'satisfied by skip mode' async def test_outputs_can_be_changed( one_conf, flow, start, scheduler, validate ): schd = scheduler(flow(one_conf), run_mode='live') async with start(schd): # Broadcast the task into skip mode, output failed and submit it: schd.broadcast_mgr.put_broadcast( ["1"], ["one"], [ {"run mode": "skip"}, {"skip": {"outputs": "failed"}}, ], ) schd.submit_task_jobs(schd.pool.get_tasks()) # Broadcast the task into skip mode, output succeeded and submit it: schd.broadcast_mgr.put_broadcast( ['1'], ['one'], [{'skip': {'outputs': 'succeeded'}}] ) schd.submit_task_jobs(schd.pool.get_tasks()) async def test_rerun_after_skip_mode_broadcast( flow, one_conf, scheduler, start ): """Test re-running a task after it has been set to skip. See https://github.com/cylc/cylc-flow/pull/6940 """ id_ = flow({ **one_conf, "runtime": { "one": { "execution time limit": "PT1M", }, }, }) schd: Scheduler = scheduler(id_, run_mode='live') async with start(schd): itask = schd.pool.get_tasks()[0] schd.submit_task_jobs([itask]) schd.task_events_mgr.process_message( itask, 'CRITICAL', TASK_OUTPUT_FAILED ) assert itask.state(TASK_STATUS_FAILED) schd.broadcast_mgr.put_broadcast( ['1'], ['root'], [{'run mode': 'skip'}] ) schd.submit_task_jobs([itask]) assert itask.state(TASK_STATUS_SUCCEEDED) cylc-flow-8.6.4/tests/integration/utils/0000775000175000017500000000000015202510242020432 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/utils/entry_points.py0000664000175000017500000000202315202510242023536 0ustar alastairalastair#!/usr/bin/env python3 # 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 . """Utilities for working with entry points.""" class EntryPointWrapper: """Wraps a method to make it look like an entry point.""" def __init__(self, fcn): self.name = fcn.__name__ self.fcn = fcn def load(self): return self.fcn cylc-flow-8.6.4/tests/integration/utils/__init__.py0000664000175000017500000000216515202510242022547 0ustar alastairalastair# 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 . """General utilities for the integration test infrastructure. These utilities are not intended for direct use by tests (hence the underscore function names). Use the fixtures provided in the conftest instead. """ def _rm_if_empty(path): """Convenience wrapper for removing empty directories.""" try: path.rmdir() except OSError: return False return True cylc-flow-8.6.4/tests/integration/utils/test_flow_writer.py0000664000175000017500000000572115202510242024413 0ustar alastairalastair# 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 . """Tests to ensure the tests are working - very meta. https://github.com/cylc/cylc-flow/pull/2740#discussion_r206086008 And yes, these are unit-tests inside a functional test framework thinggy. """ from textwrap import dedent from .flow_writer import ( _write_header, _write_setting, _write_section, flow_config_str ) def test_write_header(): """It should write out cylc configuration headings.""" assert _write_header('foo', 1) == '[foo]' assert _write_header('foo', 2) == ' [[foo]]' def test_write_setting_singleline(): """It should write out cylc configuration settings.""" assert _write_setting('key', 'value', 1) == [ 'key = value' ] assert _write_setting('key', 'value', 2) == [ ' key = value' ] def test_write_setting_script(): """It should preserve indentation for script items.""" assert _write_setting('script', 'a\nb\nc', 2) == [ ' script = """', 'a', 'b', 'c', ' """' ] def test_write_setting_multiline(): """It should write out cylc configuration settings over multiple lines.""" assert _write_setting('key', 'foo\nbar', 1) == [ 'key = """', ' foo', ' bar', '"""' ] assert _write_setting('key', 'foo\nbar', 2) == [ ' key = """', ' foo', ' bar', ' """' ] def test_write_section(): """It should write out entire cylc configuraitons.""" assert _write_section( 'foo', { 'bar': { 'pub': 'beer' }, 'baz': 42 }, 1 ) == [ '[foo]', ' baz = 42', ' [[bar]]', ' pub = beer' ] def test_flow_config_str(): """It should write out entire cylc configuration files.""" assert flow_config_str( { '#!JiNjA2': None, 'foo': { 'bar': { 'pub': 'beer' }, 'baz': 42 }, 'qux': 'asdf' } ) == dedent(''' #!jinja2 qux = asdf [foo] baz = 42 [[bar]] pub = beer ''').strip() + '\n' cylc-flow-8.6.4/tests/integration/utils/test_utils.py0000664000175000017500000000254215202510242023206 0ustar alastairalastair# 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 . """Tests to ensure the tests are working - very meta. https://github.com/cylc/cylc-flow/pull/2740#discussion_r206086008 And yes, these are unit-tests inside a functional test framework thinggy. """ from pathlib import Path from . import ( _rm_if_empty, ) def test_rm_if_empty(tmp_path): """It should remove dirs if empty and suppress exceptions otherwise.""" path1 = Path(tmp_path, 'foo') path2 = Path(path1, 'bar') path2.mkdir(parents=True) _rm_if_empty(path1) assert path2.exists() _rm_if_empty(path2) assert not path2.exists() _rm_if_empty(path1) assert not path1.exists() cylc-flow-8.6.4/tests/integration/utils/test_flow_tools.py0000664000175000017500000000237715202510242024243 0ustar alastairalastair# 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 . """Tests to ensure the tests are working - very meta. https://github.com/cylc/cylc-flow/pull/2740#discussion_r206086008 """ from pathlib import Path # test _make_flow via the conftest fixture def test_flow(run_dir, flow, one_conf): """It should create a flow in the run directory.""" id_ = flow(one_conf) assert Path(run_dir / id_).exists() assert Path(run_dir / id_ / 'flow.cylc').exists() with open(Path(run_dir / id_ / 'flow.cylc'), 'r') as flow_file: assert 'scheduling' in flow_file.read() cylc-flow-8.6.4/tests/integration/utils/flow_tools.py0000664000175000017500000001476715202510242023212 0ustar alastairalastair# 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 . """Wrappers for creating and launching flows. These utilities are not intended for direct use by tests (hence the underscore function names). Use the fixtures provided in the conftest instead. """ import asyncio from contextlib import ( asynccontextmanager, contextmanager, ) import logging from pathlib import Path from secrets import token_hex from typing import ( Any, Optional, Union, ) import pytest from cylc.flow import CYLC_LOG from cylc.flow.scheduler import ( Scheduler, SchedulerStop, ) from cylc.flow.scheduler_cli import RunOptions from cylc.flow.workflow_files import WorkflowFiles from cylc.flow.workflow_status import StopMode from .flow_writer import flow_config_str def _make_src_flow(src_path, conf, filename=WorkflowFiles.FLOW_FILE): """Construct a workflow on the filesystem""" flow_src_dir = (src_path / token_hex(4)) flow_src_dir.mkdir(parents=True, exist_ok=True) if isinstance(conf, dict): conf = flow_config_str(conf) with open((flow_src_dir / filename), 'w+') as flow_file: flow_file.write(conf) return flow_src_dir def _make_flow( cylc_run_dir: Union[Path, str], test_dir: Path, conf: Union[dict, str], name: Optional[str] = None, workflow_id: Optional[str] = None, defaults: Optional[bool] = True, filename: str = WorkflowFiles.FLOW_FILE, ) -> str: """Construct a workflow on the filesystem. Args: conf: Either a workflow config dictionary, or a graph string to be used as the R1 graph in the workflow config. defaults: Set up a common defaults. * [scheduling]allow implicit tasks = true Set false for Cylc 7 upgrader tests. """ if workflow_id: flow_run_dir = (cylc_run_dir / workflow_id) else: if name is None: name = token_hex(4) flow_run_dir = (test_dir / name) flow_run_dir.mkdir(parents=True, exist_ok=True) workflow_id = str(flow_run_dir.relative_to(cylc_run_dir)) if isinstance(conf, str): conf = { 'scheduling': { 'graph': { 'R1': conf } } } if defaults: # set the default simulation runtime to zero (can be overridden) ( conf.setdefault('runtime', {}) .setdefault('root', {}) .setdefault('simulation', {}) .setdefault('default run length', 'PT0S') ) # allow implicit tasks by default: conf.setdefault('scheduler', {}).setdefault( 'allow implicit tasks', 'True') with open((flow_run_dir / filename), 'w+') as flow_file: flow_file.write(flow_config_str(conf)) return workflow_id @contextmanager def _make_scheduler(): """Return a scheduler object for a flow registration.""" schd: Scheduler = None # type: ignore[assignment] def __make_scheduler(id_: str, **opts: Any) -> Scheduler: opts = { # safe n sane defaults for integration tests 'paused_start': True, 'run_mode': 'simulation', **opts, } options = RunOptions(**opts) # create workflow nonlocal schd schd = Scheduler(id_, options) return schd yield __make_scheduler # Teardown if hasattr(schd, 'workflow_db_mgr'): schd.workflow_db_mgr.on_workflow_shutdown() @asynccontextmanager async def _start_flow( caplog: Optional[pytest.LogCaptureFixture], schd: Scheduler, level: int = logging.INFO ): """Start a scheduler but don't set the main loop running.""" if caplog: caplog.set_level(level, CYLC_LOG) await schd.install() try: # Nested `try...finally` to ensure caplog always yielded even if # exception occurs in Scheduler try: await schd.start() finally: # After this `yield`, the `with` block of the context manager # is executed: yield caplog finally: # Cleanup - this always runs after the `with` block of the # context manager. # Need to shut down Scheduler, but time out in case something # goes wrong: async with asyncio.timeout(5): await schd.shutdown(SchedulerStop("integration test teardown")) @asynccontextmanager async def _run_flow( caplog: Optional[pytest.LogCaptureFixture], schd: Scheduler, level: int = logging.INFO ): """Start a scheduler and set the main loop running.""" if caplog: caplog.set_level(level, CYLC_LOG) await schd.install() task: Optional[asyncio.Task] = None try: # Nested `try...finally` to ensure caplog always yielded even if # exception occurs in Scheduler try: await schd.start() # Do not await as we need to yield control to the main loop: task = asyncio.create_task(schd.run_scheduler()) finally: # After this `yield`, the `with` block of the context manager # is executed: yield caplog finally: # Cleanup - this always runs after the `with` block of the # context manager. # Need to shut down Scheduler, but time out in case something # goes wrong: async with asyncio.timeout(5): if task: # ask the scheduler to shut down nicely, # let main loop handle it: schd._set_stop(StopMode.REQUEST_NOW_NOW) await task if schd.contact_data: async with asyncio.timeout(5): # Scheduler still running... try more forceful tear down: await schd.shutdown(SchedulerStop("integration test teardown")) if task: # Brute force cleanup if something went wrong: task.cancel() cylc-flow-8.6.4/tests/integration/utils/flow_writer.py0000664000175000017500000000555315202510242023357 0ustar alastairalastair# 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 . """Utility for writing Cylc Flow configuration files. These utilities are not intended for direct use by tests (hence the underscore function names). Use the fixtures provided in the conftest instead. """ from textwrap import dedent from typing import List def _write_header(name: str, level: int) -> str: """Write a cylc section definition.""" indent = ' ' * (level - 1) return f'{indent}{"[" * level}{name}{"]" * level}' def _write_setting(key: str, value: str, level: int) -> List[str]: """Write a cylc setting definition.""" indent = ' ' * (level - 1) value = str(value) if '\n' in value: value = dedent(value).strip() ret = [f'{indent}{key} = """'] if 'script' in key: ret.extend(value.splitlines()) else: ret.extend([ f'{indent} {line}' for line in value.splitlines() ]) ret += [f'{indent}"""'] else: ret = [f'{" " * (level - 1)}{key} = {value}'] return ret def _write_section(name: str, section: dict, level: int) -> List[str]: """Write an entire cylc section including headings and settings.""" ret = [] ret.append(_write_header(name, level)) ret.extend(_write_conf(section, level)) return ret def _write_conf(conf: dict, level: int) -> List[str]: ret = [] for key, value in conf.items(): # write out settings first if key.lower() == '#!jinja2': ret.append('#!jinja2') elif not isinstance(value, dict): ret.extend( _write_setting(key, value, level + 1) ) for key, value in conf.items(): # then sections after if isinstance(value, dict): ret.extend( _write_section(key, value, level + 1) ) return ret def flow_config_str(conf: dict) -> str: """Convert a configuration dictionary into cylc/parsec format. Args: conf (dict): A [nested] dictionary of configurations. Returns: str - Multiline string in cylc/parsec format. """ return '\n'.join(_write_conf(conf, 0)) + '\n' cylc-flow-8.6.4/tests/integration/test_reload.py0000664000175000017500000002676715202510242022173 0ustar alastairalastair# 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 . """Tests for reload behaviour in the scheduler.""" from cylc.flow import ( commands, flags, ) from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.data_store_mgr import TASK_PROXIES from cylc.flow.platforms import get_platform from cylc.flow.scheduler import Scheduler from cylc.flow.task_state import ( TASK_STATUS_PREPARING, TASK_STATUS_SUBMITTED, TASK_STATUS_WAITING, ) async def test_reload_waits_for_pending_tasks( flow, scheduler, start, monkeypatch, log_scan, ): """Reload should flush out preparing tasks and pause the workflow. Reloading a workflow with preparing tasks may be unsafe and is at least confusing. For safety we should pause the workflow and flush out any preparing tasks before attempting reload. See https://github.com/cylc/cylc-flow/issues/5107 """ # speed up the test: monkeypatch.setattr('cylc.flow.scheduler.sleep', lambda *_: None) # a simple workflow with a single task id_ = flow('foo') schd: Scheduler = scheduler(id_, paused_start=False) # we will artificially push the task through these states state_seq = [ # repeat the preparing state a few times to simulate a task # taking multiple main-loop cycles to submit TASK_STATUS_PREPARING, TASK_STATUS_PREPARING, TASK_STATUS_PREPARING, TASK_STATUS_SUBMITTED, ] # start the scheduler async with start(schd) as log: foo = schd.pool.get_tasks()[0] # set the task to go through some state changes def submit_task_jobs(*a, **k): try: foo.state_reset(state_seq.pop(0)) except IndexError: foo.waiting_on_job_prep = False return [foo] monkeypatch.setattr( schd.task_job_mgr, 'submit_task_jobs', submit_task_jobs ) # the task should start as waiting assert foo.state(TASK_STATUS_WAITING) # put the task into the preparing state schd.release_tasks_to_run() assert foo.state(TASK_STATUS_PREPARING) # reload the workflow await commands.run_cmd(commands.reload_workflow(schd)) # the task should end in the submitted state assert foo.state(TASK_STATUS_SUBMITTED) # ensure the order of events was correct log_scan( log, [ # the task should have entered the preparing state before the # reload was requested '[1/foo:waiting] => preparing', # the reload should have put the workflow into the paused state 'Pausing the workflow: Reloading workflow', # reload should have waited for the task to submit '[1/foo/00:preparing] => submitted', # before then reloading the workflow config 'Reloading the workflow definition.', # post-reload the workflow should have been resumed 'RESUMING the workflow now', ], ) async def test_reload_failure( flow, one_conf, scheduler, start, log_filter, ): """Reload should not crash the workflow on config errors. A warning should be logged along with the error. """ id_ = flow(one_conf) schd = scheduler(id_) async with start(schd): # corrupt the config by removing the scheduling section two_conf = {**one_conf, 'scheduling': {}} flow(two_conf, workflow_id=id_) # reload the workflow await commands.run_cmd(commands.reload_workflow(schd)) # the reload should have failed but the workflow should still be # running assert log_filter( contains=( 'Reload failed - WorkflowConfigError:' ' missing [scheduling][[graph]] section' ) ) # the config should be unchanged assert schd.config.cfg['scheduling']['graph']['R1'] == 'one' async def test_reload_global_platform( flow, one_conf, scheduler, start, log_filter, tmp_path, monkeypatch, ): global_config_path = tmp_path / 'global.cylc' monkeypatch.setenv("CYLC_CONF_PATH", str(global_config_path.parent)) # Original global config file global_config_path.write_text(""" [platforms] [[localhost]] [[[meta]]] x = 1 """) glbl_cfg(reload=True) assert glbl_cfg().get(['platforms', 'localhost', 'meta', 'x']) == '1' id_ = flow(one_conf) schd = scheduler(id_) async with start(schd): # Task platforms reflect the original config rtconf = schd.broadcast_mgr.get_updated_rtconfig( schd.pool.get_tasks()[0] ) platform = get_platform(rtconf) assert platform['meta']['x'] == '1' # Modify the global config file global_config_path.write_text(""" [platforms] [[localhost]] [[[meta]]] x = 2 """) # reload the workflow and global config await commands.run_cmd( commands.reload_workflow(schd, reload_global=True) ) # Global config should have been reloaded assert log_filter(contains=('Reloading the global configuration.')) # Task platforms reflect the new config rtconf = schd.broadcast_mgr.get_updated_rtconfig( schd.pool.get_tasks()[0] ) platform = get_platform(rtconf) assert platform['meta']['x'] == '2' # Modify the global config file with an error global_config_path.write_text(""" [ERROR] [platforms] [[localhost]] [[[meta]]] x = 3 """) # reload the workflow and global config await commands.run_cmd( commands.reload_workflow(schd, reload_global=True) ) # Error is noted in the log assert log_filter( contains=( 'This is probably due to an issue with the new configuration.' ) ) # Task platforms should be the last valid value rtconf = schd.broadcast_mgr.get_updated_rtconfig( schd.pool.get_tasks()[0] ) platform = get_platform(rtconf) assert platform['meta']['x'] == '2' # reload the workflow in verbose mode flags.verbosity = 2 await commands.run_cmd( commands.reload_workflow(schd, reload_global=True) ) # Traceback is shown in the log # (match for ERROR, trace is not captured) assert log_filter(exact_match='ERROR') async def test_reload_global_platform_group( flow, scheduler, start, log_filter, tmp_path, monkeypatch, ): global_config_path = tmp_path / 'global.cylc' monkeypatch.setenv("CYLC_CONF_PATH", str(global_config_path.parent)) # Original global config file global_config_path.write_text(""" [platforms] [[foo]] [[[meta]]] x = 1 [platform groups] [[pg]] platforms = foo """) glbl_cfg(reload=True) # Task using the platform group conf = { 'scheduling': {'graph': {'R1': 'one'}}, 'runtime': { 'one': { 'platform': 'pg', } }, } id_ = flow(conf) schd = scheduler(id_) async with start(schd): # Task platforms reflect the original config rtconf = schd.broadcast_mgr.get_updated_rtconfig( schd.pool.get_tasks()[0] ) platform = get_platform(rtconf) assert platform['meta']['x'] == '1' # Modify the global config file global_config_path.write_text(""" [platforms] [[bar]] [[[meta]]] x = 2 [platform groups] [[pg]] platforms = bar """) # reload the workflow and global config await commands.run_cmd( commands.reload_workflow(schd, reload_global=True) ) # Global config should have been reloaded assert log_filter(contains=('Reloading the global configuration.')) # Task platforms reflect the new config rtconf = schd.broadcast_mgr.get_updated_rtconfig( schd.pool.get_tasks()[0] ) platform = get_platform(rtconf) assert platform['meta']['x'] == '2' async def test_orphan_reload( flow, scheduler, start, log_filter, ): """Reload should not fail because of orphaned tasks. The following aspects of reload-with-orphans are tested: - Broadcast deltas generated after reload. https://github.com/cylc/cylc-flow/issues/6814 - Removal of both xtrigger and associated active/incomplete task. https://github.com/cylc/cylc-flow/issues/6815 (Orphans being active/incomplete tasks removed from reloaded workflow cfg.) """ before = { 'scheduling': { 'initial cycle point': '20010101T0000Z', 'graph': { 'R1': ''' foo => bar @wall_clock => bar ''' } } } after = { 'scheduling': { 'initial cycle point': '20010101T0000Z', 'graph': { 'R1': 'foo' } } } id_ = flow(before) schd = scheduler(id_) async with start(schd): # spawn in bar foo = schd.pool._get_task_by_id('20010101T0000Z/foo') schd.pool.task_events_mgr.process_message(foo, 'INFO', 'succeeded') bar = schd.pool._get_task_by_id('20010101T0000Z/bar') # set bar to failed schd.pool.task_events_mgr.process_message(bar, 'INFO', 'failed') # Save our progress schd.workflow_db_mgr.put_task_pool(schd.pool) # Change workflow to one without bar and xtrigger flow(after, workflow_id=id_) # reload the workflow await commands.run_cmd(commands.reload_workflow(schd)) # test broadcast delta over orphaned task schd.data_store_mgr.delta_broadcast() # the reload should have completed successfully assert log_filter( contains=('Reload completed') ) async def test_data_store_tproxy(flow, scheduler, start): """Check N>0 task proxy in data store has correct info on reload. https://github.com/cylc/cylc-flow/issues/6973 """ schd: Scheduler = scheduler(flow('foo => bar')) def get_ds_tproxy(task): return schd.data_store_mgr.data[schd.id][TASK_PROXIES][ f'{schd.id}//1/{task}' ] async with start(schd): await schd.update_data_structure() assert str(get_ds_tproxy('bar').runtime) await commands.run_cmd(commands.reload_workflow(schd)) assert str(get_ds_tproxy('bar').runtime) cylc-flow-8.6.4/tests/integration/test_optional_outputs.py0000664000175000017500000006471115202510242024344 0ustar alastairalastair# 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 . """Tests optional output and task completion logic. This functionality is defined by the "optional-output-extension" proposal: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ from itertools import combinations from typing import TYPE_CHECKING import logging import pytest import itertools from cylc.flow.commands import ( run_cmd, force_trigger_tasks ) from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.cycling.iso8601 import ISO8601Point from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.id import TaskTokens, Tokens from cylc.flow.network.resolvers import TaskMsg from cylc.flow.task_events_mgr import ( TaskEventsManager, ) from cylc.flow.task_outputs import ( TASK_OUTPUTS, TASK_OUTPUT_EXPIRED, TASK_OUTPUT_FAILED, TASK_OUTPUT_FINISHED, TASK_OUTPUT_SUCCEEDED, get_completion_expression, ) from cylc.flow.task_state import ( TASK_STATUS_EXPIRED, TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_WAITING, ) if TYPE_CHECKING: from cylc.flow.task_proxy import TaskProxy from cylc.flow.scheduler import Scheduler OPT_BOTH_ERR = "Output {} can't be both required and optional" def reset_outputs(itask: 'TaskProxy'): """Undo the consequences of setting task outputs. This assumes you haven't completed the task. """ itask.state.outputs._completed = { message: False for message in itask.state.outputs._completed } itask.state_reset( TASK_STATUS_WAITING, is_queued=False, is_held=False, is_runahead=False, ) @pytest.mark.parametrize( 'graph, completion_outputs', [ pytest.param( 'a:x', [{TASK_OUTPUT_SUCCEEDED, 'x'}], id='1', ), pytest.param( 'a\na:x\na:expired?', [{TASK_OUTPUT_SUCCEEDED, 'x'}, {TASK_OUTPUT_EXPIRED}], id='2', ), ], ) async def test_task_completion( flow, scheduler, start, graph, completion_outputs, capcall, ): """Ensure that task completion is watertight. Run through every possible permutation of outputs MINUS the ones that would actually complete a task to ensure that task completion is correctly handled. Note, the building and evaluation of completion expressions is also tested, this is more of an end-to-end test to ensure everything is connected properly. """ # prevent tasks from being removed from the pool when complete capcall( 'cylc.flow.task_pool.TaskPool.remove_if_complete' ) id_ = flow({ 'scheduling': { 'graph': {'R1': graph}, }, 'runtime': { 'a': { 'outputs': { 'x': 'xxx', }, }, }, }) schd = scheduler(id_) all_outputs = { # all built-in outputs *TASK_OUTPUTS, # all registered custom outputs 'x' # but not the finished psudo output } - {TASK_OUTPUT_FINISHED} async with start(schd): a1 = schd.pool.get_task(IntegerPoint('1'), 'a') # try every set of outputs that *shouldn't* complete the task for combination in { comb # every possible combination of outputs for _length in range(1, len(all_outputs)) for comb in combinations(all_outputs, _length) # that doesn't contain the outputs that would satisfy the task if not any( set(comb) & output_set == output_set for output_set in completion_outputs ) }: # set the combination of outputs schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, combination, [], ['1'], ) # ensure these outputs do *not* complete the task assert not a1.state.outputs.is_complete() # reset any changes reset_outputs(a1) # now try the outputs that *should* satisfy the task for combination in completion_outputs: # set the combination of outputs schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'a')}, combination, [], ['1'], ) # ensure the task *is* completed assert a1.state.outputs.is_complete() # reset any changes reset_outputs(a1) async def test_expire_orthogonality(flow, scheduler, start): """Ensure "expired?" does not infer "succeeded?". Asserts proposal point 2: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a:expire? => e' }, }, }) schd: 'Scheduler' = scheduler(id_, paused_start=False) async with start(schd): a_1 = schd.pool.get_task(IntegerPoint('1'), 'a') # wait for the task to submit while not a_1.state(TASK_STATUS_WAITING, TASK_STATUS_PREPARING): schd.release_tasks_to_run() # NOTE: The submit number isn't presently incremented via this code # pathway so we have to hack it here. If the task messages in this test # get ignored because of some future change, then you can safely remove # this line (it's not what this test is testing). a_1.submit_num += 1 # tell the scheduler that the task *submit-failed* schd.message_queue.put( TaskMsg( Tokens('//1/a/01'), '2000-01-01T00:00:00+00', 'INFO', TaskEventsManager.EVENT_SUBMIT_FAILED ), ) schd.process_queued_task_messages() # ensure that the scheduler is stalled assert not a_1.state.outputs.is_complete() assert schd.pool.is_stalled() # tell the scheduler that the task *failed* schd.message_queue.put( TaskMsg( Tokens('//1/a/01'), '2000-01-01T00:00:00+00', 'INFO', TaskEventsManager.EVENT_FAILED, ), ) schd.process_queued_task_messages() # ensure that the scheduler is stalled assert not a_1.state.outputs.is_complete() assert schd.pool.is_stalled() # tell the scheduler that the task *expired* schd.message_queue.put( TaskMsg( Tokens('//1/a/01'), '2000-01-01T00:00:00+00', 'INFO', TaskEventsManager.EVENT_EXPIRED, ), ) schd.process_queued_task_messages() # ensure that the scheduler is *not* stalled assert a_1.state.outputs.is_complete() assert not schd.pool.is_stalled() @pytest.fixture(scope='module') def implicit_completion_config(mod_flow, mod_validate): id_ = mod_flow({ 'scheduling': { 'graph': { 'R1': ''' a b? c:x d:x? d:y? d:z? e:x e:y e:z f? f:x g:expired? h:succeeded? h:expired? i:expired? i:submitted j:expired? j:submitted? k:submit-failed? k:succeeded? l:expired? l:submit-failed? l:succeeded? ''' } }, 'runtime': { 'root': { 'outputs': {x: f'{x * 3}' for x in 'abcdefghijklxyz'} } } }) return mod_validate(id_) @pytest.mark.parametrize( 'task, condition', [ pytest.param('a', 'succeeded', id='a'), pytest.param('b', 'succeeded or failed', id='b'), pytest.param('c', '(succeeded and x)', id='c'), pytest.param('d', 'succeeded', id='d'), pytest.param('e', '(succeeded and x and y and z)', id='e'), pytest.param('f', '(x and succeeded) or failed', id='f'), pytest.param('g', 'succeeded or expired', id='h'), pytest.param('h', 'succeeded or failed or expired', id='h'), pytest.param('i', '(submitted and succeeded) or expired', id='i'), pytest.param('j', 'succeeded or submit_failed or expired', id='j'), pytest.param('k', 'succeeded or failed or submit_failed', id='k'), pytest.param( 'l', 'succeeded or failed or submit_failed or expired', id='l' ), ], ) async def test_implicit_completion_expression( implicit_completion_config, task, condition, ): """It should generate a completion expression from the graph. If no completion expression is provided in the runtime section, then it should auto generate one inferring whether outputs are required or not from the graph. """ completion_expression = get_completion_expression( implicit_completion_config.taskdefs[task] ) assert completion_expression == condition async def test_clock_expire_partially_satisfied_task( flow, scheduler, start, ): """Clock expire should take effect on a partially satisfied task. Tests proposal point 8: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ id_ = flow({ 'scheduling': { 'initial cycle point': '2000', 'runahead limit': 'P0', 'special tasks': { 'clock-expire': 'e', }, 'graph': { 'P1D': ''' # this prerequisite we will satisfy a => e # this prerequisite we will leave unsatisfied creating a # partially-satisfied task b => e ''' }, }, }) schd = scheduler(id_) async with start(schd): # satisfy one of the prerequisites a = schd.pool.get_task(ISO8601Point('20000101T0000Z'), 'a') assert a schd.pool.spawn_on_output(a, TASK_OUTPUT_SUCCEEDED) # the task "e" should now be spawned e = schd.pool.get_task(ISO8601Point('20000101T0000Z'), 'e') assert e # check for clock-expired tasks schd.pool.clock_expire_tasks() # the task should now be in the expired state assert e.state(TASK_STATUS_EXPIRED) async def test_clock_expiry( flow, scheduler, start, ): """Waiting tasks should be considered for clock-expiry. Tests two things: * Manually triggered tasks should not be considered for clock-expiry. Tests proposal point 10: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal * Active tasks should not be considered for clock-expiry. Closes https://github.com/cylc/cylc-flow/issues/6025 """ id_ = flow({ 'scheduling': { 'initial cycle point': '2000', 'runahead limit': 'P1', 'special tasks': { 'clock-expire': 'x' }, 'graph': { 'P1Y': 'x' }, }, }) schd = scheduler(id_) async with start(schd): # the first task (waiting) one = schd.pool.get_task(ISO8601Point('20000101T0000Z'), 'x') assert one # the second task (preparing) two = schd.pool.get_task(ISO8601Point('20010101T0000Z'), 'x') assert two two.state_reset(TASK_STATUS_PREPARING) # the third task (force-triggered) await run_cmd(force_trigger_tasks(schd, ['20100101T0000Z/x'], ['1'])) three = schd.pool.get_task(ISO8601Point('20100101T0000Z'), 'x') assert three # check for expiry schd.pool.clock_expire_tasks() # the first task should be expired (it was waiting) assert one.state(TASK_STATUS_EXPIRED) assert one.state.outputs.is_message_complete(TASK_OUTPUT_EXPIRED) # the second task should *not* be expired (it was active) assert not two.state(TASK_STATUS_EXPIRED) assert not two.state.outputs.is_message_complete(TASK_OUTPUT_EXPIRED) # the third task should *not* be expired (it was a manual submit) assert not three.state(TASK_STATUS_EXPIRED) assert not three.state.outputs.is_message_complete(TASK_OUTPUT_EXPIRED) async def test_removed_taskdef( flow, scheduler, start, ): """It should handle tasks being removed from the config. If the config of an active task is removed from the config by restart / reload, then we must provide a fallback completion expression, otherwise the expression will be blank (task has no required or optional outputs). The fallback is to consider the outputs complete if *any* final output is received. Since the task has been removed from the workflow its outputs should be inconsequential. See: https://github.com/cylc/cylc-flow/issues/5057 """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a & z' } } }) # start the workflow and mark the tasks as running schd: 'Scheduler' = scheduler(id_) async with start(schd): for itask in schd.pool.get_tasks(): itask.state_reset(TASK_STATUS_RUNNING) assert itask.state.outputs._completion_expression == 'succeeded' # remove the task "z" from the config id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a' } } }, workflow_id=id_) # restart the workflow schd: 'Scheduler' = scheduler(id_) async with start(schd): # 1/a: # * is still in the config # * is should still have a sensible completion expression # * its outputs should be incomplete if the task fails a_1 = schd.pool.get_task(IntegerPoint('1'), 'a') assert a_1 assert a_1.state.outputs._completion_expression == 'succeeded' a_1.state.outputs.set_message_complete(TASK_OUTPUT_FAILED) assert not a_1.is_complete() # 1/z: # * is no longer in the config # * should have a blank completion expression # * its outputs should be completed by any final output z_1 = schd.pool.get_task(IntegerPoint('1'), 'z') assert z_1 assert z_1.state.outputs._completion_expression == '' z_1.state.outputs.set_message_complete(TASK_OUTPUT_FAILED) assert z_1.is_complete() @pytest.mark.parametrize( 'graph, err', [ pytest.param( """ a a? """, OPT_BOTH_ERR.format("a:succeeded"), ), pytest.param( """ a => b a? """, OPT_BOTH_ERR.format("a:succeeded"), ), pytest.param( """ a? => b a """, OPT_BOTH_ERR.format("a:succeeded"), ), pytest.param( """ a => b? b """, OPT_BOTH_ERR.format("b:succeeded"), ), pytest.param( """ a => b:succeeded b? """, OPT_BOTH_ERR.format("b:succeeded"), ), pytest.param( """ a => b:succeeded c => b? """, OPT_BOTH_ERR.format("b:succeeded"), ), pytest.param( """ c:x => d a => c:x? """, OPT_BOTH_ERR.format("c:x"), ), pytest.param( """ c:x? => d a => c:x """, OPT_BOTH_ERR.format("c:x"), ), pytest.param( """ FAM:finish-all? """, "Family pseudo-output FAM:finish-all can't be optional", ), pytest.param( """ a => b => c b? """, OPT_BOTH_ERR.format("b:succeeded"), ), pytest.param( """ a => FAM => c """, "Family trigger required: FAM => c", ), ], ids=itertools.count() ) async def test_optional_outputs_consistency(flow, validate, graph, err): """Check that inconsistent output optionality fails validation.""" id_ = flow( { 'scheduling': { 'graph': { 'R1': graph }, }, 'runtime': { 'FAM': {}, 'm1, m2': { 'inherit': 'FAM', }, 'c': { 'outputs': { 'x': 'x', }, }, }, }, ) with pytest.raises(WorkflowConfigError) as exc_ctx: validate(id_) assert err in str(exc_ctx.value) @pytest.mark.parametrize( 'graph, expected', [ pytest.param( "a => b", { ("a", "succeeded"): True, # inferred ("b", "succeeded"): True, # default ("a", "failed"): None, # (not set) ("b", "failed"): None, # (not set) }, ), pytest.param( """ a => b b? """, { ("a", "succeeded"): True, # inferred ("b", "succeeded"): False, # inferred ("b", "failed"): None, # (not set) }, ), pytest.param( """ a:failed => b """, { ("a", "failed"): True, ("b", "succeeded"): True, ("a", "succeeded"): None, ("b", "failed"): None, }, ), pytest.param( """ a => b b """, { ("a", "succeeded"): True, ("b", "succeeded"): True, }, ), pytest.param( """ a => b b? """, { ("a", "succeeded"): True, ("b", "succeeded"): False, }, ), pytest.param( """ a? => b """, { ("a", "succeeded"): False, ("b", "succeeded"): True, }, ), pytest.param( """ a? => b b? """, { ("a", "succeeded"): False, ("b", "succeeded"): False, }, ), pytest.param( """ a? => b? """, { ("a", "succeeded"): False, ("b", "succeeded"): False, }, ), pytest.param( """ FAM """, { ("m1", "succeeded"): True, # family default ("m2", "succeeded"): True, # family default }, ), pytest.param( """ FAM:succeed-all? """, { ("m1", "succeeded"): False, # family default ("m2", "succeeded"): False, # family default }, ), pytest.param( """ FAM m1? """, { ("m1", "succeeded"): False, # inferred ("m2", "succeeded"): True, # family default }, ), pytest.param( """ a => FAM """, { ("a", "succeeded"): True, # inferred ("m1", "succeeded"): True, # default ("m2", "succeeded"): True, # default }, ), pytest.param( """ a => FAM m2? """, { ("a", "succeeded"): True, # inferred ("m1", "succeeded"): True, # default ("m2", "succeeded"): False, # explicit "?" }, ), pytest.param( """ a => FAM:finish-all """, { ("a", "succeeded"): True, # inferred ("m1", "succeeded"): False, # family default ("m2", "succeeded"): False, # family default }, ), pytest.param( """ FAM:succeed-any => a """, { ("a", "succeeded"): True, # inferred ("m1", "succeeded"): True, # family default ("m2", "succeeded"): True, # family default }, ), pytest.param( """ FAM:succeed-any? => a """, { ("a", "succeeded"): True, # inferred ("m1", "succeeded"): False, # family default ("m2", "succeeded"): False, # family default }, ), pytest.param( """ FAM:succeed-any => a m1? """, { ("a", "succeeded"): True, ("m1", "succeeded"): False, ("m2", "succeeded"): True, }, ), pytest.param( """ a & b? => c """, { ("a", "succeeded"): True, # inferred ("b", "succeeded"): False, # explicit "?" ("c", "succeeded"): True, # default }, ), pytest.param( """ a => c:x """, { ("a", "succeeded"): True, # inferred ("c", "succeeded"): True, # default ("c", "x"): True, # explicit ":x" }, ), pytest.param( """ a => c:x? """, { ("a", "succeeded"): True, # inferred ("c", "succeeded"): True, # default ("c", "x"): False, # explicit ":x?" }, ), pytest.param( """ a => b => c # infer :succeeded for b inside chain """, { ("a", "succeeded"): True, # inferred ("b", "succeeded"): True, # inferred ("c", "succeeded"): True, # default }, ), pytest.param( # Check we don't infer c:succeeded at end-of-chain # when there's an & at the end. """ a => b & c c? """, { ("a", "succeeded"): True, # inferred ("b", "succeeded"): True, # default ("c", "succeeded"): False, # inferred }, ), ], ids=itertools.count() ) async def test_optional_outputs_inference( flow, validate, graph, expected ): """Check task output optionality after graph parsing. This checks taskdef.outputs, which holds inferred and default values. """ id = flow( { 'scheduling': { 'graph': { 'R1': graph }, }, 'runtime': { 'FAM': {}, 'm1, m2': { 'inherit': 'FAM', }, 'c': { 'outputs': { 'x': 'x', }, }, }, } ) config = validate(id) for (task, output), exp in expected.items(): tdef = config.get_taskdef(task) (_, required) = tdef.outputs[output] assert required == exp async def test_log_outputs(flow, validate, caplog, monkeypatch): """Test logging of optional and required outputs inferred from the graph. This probes output optionality inferred by the graph parser, so it does not include RHS-only tasks that just default to :succeeded required. """ id = flow( { 'scheduling': { 'graph': { 'R1': """ # (b:succeeded required by default, not by inference) a? => FAM:succeed-all? => b m1 a? => c:x? a? => c:y """, }, }, 'runtime': { 'FAM': {}, 'm1, m2': { 'inherit': 'FAM', }, 'c': { "outputs": { "x": "x", "y": "y" } } } } ) monkeypatch.setattr('cylc.flow.flags.verbosity', 2) caplog.set_level(logging.DEBUG) validate(id) found_opt = False found_req = False for record in caplog.records: msg = record.message if "Optional outputs inferred from the graph:" in msg: found_opt = True for output in ["a:succeeded", "m2:succeeded", "c:x"]: assert output in msg for output in [ "b:succeeded", "m1:succeeded", "c:y", "c:succeeded" ]: assert output not in msg elif "Required outputs inferred from the graph:" in msg: found_req = True for output in ["m1:succeeded", "c:y"]: assert output in msg for output in [ "m2:succeeded", "b:succeeded", "a:succeeded", "c:x", "c:succeeded" ]: assert output not in msg assert found_opt assert found_req cylc-flow-8.6.4/tests/integration/test_task_events_mgr.py0000664000175000017500000003016315202510242024101 0ustar alastairalastair# 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 . import logging from types import SimpleNamespace from typing import Any as Fixture import pytest from cylc.flow.data_store_mgr import ( JOBS, TASK_STATUS_WAITING, ) from cylc.flow.id import Tokens from cylc.flow.run_modes import RunMode from cylc.flow.scheduler import Scheduler from cylc.flow.task_events_mgr import ( EventKey, TaskJobLogsRetrieveContext, ) from cylc.flow.task_state import ( TASK_STATUS_PREPARING, TASK_STATUS_SUBMIT_FAILED, ) from cylc.flow.network.resolvers import TaskMsg from .test_workflow_events import TEMPLATES # NOTE: we do not test custom event handlers here because these are tested # as a part of workflow validation (now also performed by cylc play) async def test_process_job_logs_retrieval_warns_no_platform( one_conf: Fixture, flow: Fixture, scheduler: Fixture, run: Fixture, db_select: Fixture, caplog: Fixture ): """Job log retrieval handles `NoHostsError`""" ctx = TaskJobLogsRetrieveContext( platform_name='skarloey', max_size=256, key='skarloey' ) id_: str = flow(one_conf) schd: 'Scheduler' = scheduler(id_, paused_start=True) # Run async with run(schd): schd.task_events_mgr._process_job_logs_retrieval( schd, ctx, 'foo' ) warning = caplog.records[-1] assert warning.levelname == 'WARNING' assert 'Unable to retrieve' in warning.msg async def test__reset_job_timers( one_conf: Fixture, flow: Fixture, scheduler: Fixture, start: Fixture, caplog: Fixture, mock_glbl_cfg: Fixture, ): """Integration test of pathway leading to process_execution_polling_intervals. """ schd = scheduler(flow(one_conf)) async with start(schd, level=logging.DEBUG): itask = schd.pool.get_tasks()[0] itask.state.status = 'running' itask.platform['execution polling intervals'] = [25] itask.platform['execution time limit polling intervals'] = [10] itask.summary['execution_time_limit'] = 30 caplog.records.clear() schd.task_events_mgr._reset_job_timers(itask) assert ( 'polling intervals=PT25S,PT15S,PT10S,...' in caplog.records[0].msg ) async def test__insert_task_job(flow, one_conf, scheduler, start, validate): """Simulation mode tasks are inserted into the Data Store, with correct submit number. """ conf = { 'scheduling': {'graph': {'R1': 'rhenas'}}, 'runtime': { 'rhenas': { 'simulation': { 'fail cycle points': '1', 'fail try 1 only': False, } } }, } id_ = flow(conf) schd = scheduler(id_) async with start(schd): # Set task to running: itask = schd.pool.get_tasks()[0] itask.state.status = 'running' itask.submit_num += 1 itask.run_mode = RunMode.SIMULATION # Not run _insert_task_job yet: assert not schd.data_store_mgr.added['jobs'].keys() # Insert task (twice): schd.task_events_mgr._insert_task_job(itask, 'now', 1) itask.submit_num += 1 schd.task_events_mgr._insert_task_job(itask, 'now', 1) # Check that there are two entries with correct submit # numbers waiting for data-store insertion: assert [ i.submit_num for i in schd.data_store_mgr.added['jobs'].values() ] == [1, 2] async def test__always_insert_task_job( flow, scheduler, mock_glbl_cfg, start ): """Insert Task Job _Always_ inserts a task into the data store. Bug https://github.com/cylc/cylc-flow/issues/6172 was caused by passing task state to data_store_mgr.insert_job: Where a submission retry was in progress the task state would be "waiting" which caused the data_store_mgr.insert_job to return without adding the task to the data store. This is testing two different cases: * Could not select host from platform * Could not select host from platform group """ global_config = """ [platforms] [[broken1]] hosts = no-such-host-1 job runner = abc [[broken2]] hosts = no-such-host-2 job runner = def [platform groups] [[broken_group]] platforms = broken1 """ mock_glbl_cfg('cylc.flow.platforms.glbl_cfg', global_config) id_ = flow({ 'scheduling': {'graph': {'R1': 'foo & bar'}}, 'runtime': { 'root': {'submission retry delays': 'PT10M'}, 'foo': {'platform': 'broken_group'}, 'bar': {'platform': 'broken2'} } }) schd: Scheduler = scheduler(id_, run_mode='live') schd.bad_hosts.update({'no-such-host-1', 'no-such-host-2'}) async with start(schd): schd.submit_task_jobs(schd.pool.get_tasks()) await schd.update_data_structure() # Both tasks are in a waiting state: assert all( i.state.status == TASK_STATUS_WAITING for i in schd.pool.get_tasks() ) # Both jobs are in the data store with submit-failed state: ds_jobs = schd.data_store_mgr.data[schd.id][JOBS] updates = { id_.split('//')[-1]: (job.state, job.platform, job.job_runner_name) for id_, job in ds_jobs.items() } assert updates == { '1/foo/01': ('submit-failed', 'broken_group', ''), '1/bar/01': ('submit-failed', 'broken2', 'def'), } for job in ds_jobs.values(): assert job.submitted_time async def test__submit_failed_job_id(flow, scheduler, start, db_select): """If a job is killed in the submitted state, the job ID should still be in the DB/data store. See https://github.com/cylc/cylc-flow/pull/6926 """ async def get_ds_job_id(schd: Scheduler): await schd.update_data_structure() return list(schd.data_store_mgr.data[schd.id][JOBS].values())[0].job_id id_ = flow('foo') schd: Scheduler = scheduler(id_) job_id = '1234' async with start(schd): itask = schd.pool.get_tasks()[0] itask.state_reset(TASK_STATUS_PREPARING) itask.submit_num = 1 itask.summary['submit_method_id'] = job_id schd.workflow_db_mgr.put_insert_task_jobs(itask, {}) schd.task_events_mgr.process_message( itask, 'INFO', schd.task_events_mgr.EVENT_SUBMITTED ) assert await get_ds_job_id(schd) == job_id schd.task_events_mgr.process_message( itask, 'CRITICAL', schd.task_events_mgr.EVENT_SUBMIT_FAILED ) assert itask.state(TASK_STATUS_SUBMIT_FAILED) assert await get_ds_job_id(schd) == job_id assert db_select(schd, False, 'task_jobs', 'job_id', 'submit_status') == [ (job_id, 1) ] # Restart and check data store again: schd = scheduler(id_) async with start(schd): assert await get_ds_job_id(schd) == job_id async def test__process_message_failed_with_retry( one: Scheduler, start, log_filter ): """Log job failure, even if a retry is scheduled. See: https://github.com/cylc/cylc-flow/pull/6169 """ async with start(one) as LOG: fail_once = one.pool.get_tasks()[0] # Add retry timers: one.task_job_mgr._set_retry_timers( fail_once, { 'execution retry delays': [1], 'submission retry delays': [1] }) # Process submit failed message with and without retries: one.task_events_mgr._process_message_submit_failed(fail_once, None) record = log_filter(contains='1/one:waiting(queued)] retrying in') assert record[0][0] == logging.WARNING one.task_events_mgr._process_message_submit_failed(fail_once, None) failed_record = log_filter(level=logging.ERROR)[-1] assert 'submission failed' in failed_record[1] # Process failed message with and without retries: one.task_events_mgr._process_message_failed( fail_once, None, 'failed', False, 'failed/OOK') last_record = LOG.records[-1] assert last_record.levelno == logging.WARNING assert 'retrying in' in last_record.message one.task_events_mgr._process_message_failed( fail_once, None, 'failed', False, 'failed/OOK') failed_record = log_filter(level=logging.ERROR)[-1] assert 'failed/OOK' in failed_record[1] @pytest.mark.parametrize('id_', ['1/no_such_task/01', '1/no_job']) async def test__unhandled_message(id_, one: Scheduler, start, log_filter): """It should log unhandled messages.""" async with start(one): one.message_queue.put( TaskMsg( Tokens(id_, relative=True), "time", 'INFO', "the quick brown" ) ) one.process_queued_task_messages() _, warning_msg = log_filter(level=logging.WARNING)[-1] assert ( 'Undeliverable task messages received and ignored:' in warning_msg ) assert f'{id_}: INFO - "the quick brown"' in warning_msg @pytest.mark.parametrize('template', TEMPLATES) async def test_mail_footer_template( mod_one, # use the same scheduler for each test start, mock_glbl_cfg, log_filter, capcall, template, ): """It should handle templating issues with the mail footer.""" # prevent emails from being sent mail_calls = capcall( 'cylc.flow.task_events_mgr.TaskEventsManager._send_mail' ) # configure mail footer mock_glbl_cfg( 'cylc.flow.workflow_events.glbl_cfg', f''' [scheduler] [[mail]] footer = 'footer={template}' ''', ) # start the workflow and get it to send an email ctx = SimpleNamespace(mail_to=None, mail_from=None) id_keys = [EventKey('none', 'failed', 'failed', Tokens('//1/a'))] async with start(mod_one): mod_one.task_events_mgr._process_event_email(mod_one, ctx, id_keys) # warnings should appear only when the template is invalid should_log = 'workflow' not in template # check that template issues are handled correctly assert bool(log_filter( contains='Ignoring bad mail footer template', )) == should_log assert bool(log_filter( contains=template, )) == should_log # check that the mail is sent even if there are issues with the footer assert len(mail_calls) == 1 async def test_event_email_body( mod_one, start, capcall, ): """It should send an email with the event context.""" mail_calls = capcall( 'cylc.flow.task_events_mgr.TaskEventsManager._send_mail' ) # start the workflow and get it to send an email ctx = SimpleNamespace(mail_to=None, mail_from=None) async with start(mod_one): # send a custom task message with the warning severity level id_keys = [ EventKey('none', 'warning', 'warning message', Tokens('//1/a/01')) ] mod_one.task_events_mgr._process_event_email(mod_one, ctx, id_keys) # test the email which would have been sent for this message email_body = mail_calls[0][0][3] assert 'event: warning' assert 'job: 1/a/01' in email_body assert 'message: warning message' in email_body assert f'workflow: {mod_one.tokens["workflow"]}' in email_body assert f'host: {mod_one.host}' in email_body assert f'port: {mod_one.server.port}' in email_body assert f'owner: {mod_one.owner}' in email_body cylc-flow-8.6.4/tests/integration/validate/0000775000175000017500000000000015202510242021063 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/validate/test_outputs.py0000664000175000017500000002470215202510242024224 0ustar alastairalastair# 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 . """Test validation of the [runtime][][outputs] section.""" from random import random import pytest from cylc.flow.exceptions import WorkflowConfigError from cylc.flow.unicode_rules import TaskOutputValidator, TaskMessageValidator @pytest.mark.parametrize( 'outputs, valid', [ pytest.param( [ 'foo', 'foo-bar', 'foo_bar', '0foo0', '123', ], True, id='valid', ), pytest.param( [ # special prefix '_cylc', # nasty chars 'foo bar', 'foo,bar', 'foo/bar', 'foo+bar', # keywords 'required', 'optional', 'all', # built-in qualifiers 'succeeded', 'succeed-all', # alternative qualifiers 'succeed', ], False, id='invalid', ), ], ) def test_outputs(outputs, valid, flow, validate): """It should validate task outputs. Outputs i.e. the keys of the [outputs] section. We don't want users adding outputs that override built-in outputs (e.g. succeeded, failed) or qualifiers (e.g. succeed-all). We don't want users adding outputs that conflict with keywords e.g. "required" or "all". """ # test that each output validates correctly for output in outputs: assert TaskOutputValidator.validate(output)[0] is valid # test that output validation is actually being performed id_ = flow({ 'scheduling': { 'graph': {'R1': 'foo'} }, 'runtime': { 'foo': { 'outputs': { output: str(random()) for output in outputs } } }, }) val = lambda: validate(id_) if valid: val() else: with pytest.raises(WorkflowConfigError): val() @pytest.mark.parametrize( 'messages, valid', [ pytest.param( [ 'foo bar baz', 'WARN:foo bar baz' ], True, id='valid', ), pytest.param( [ # special prefix '_cylc', # invalid colon usage 'foo bar: baz' # built-in qualifiers 'succeeded', 'succeed-all', # alternative qualifiers 'succeed', ], False, id='invalid', ), ], ) def test_messages(messages, valid, flow, validate): """It should validate task messages. Messages i.e. the values of the [outputs] section. We don't want users adding messages that override built-in outputs (e.g. succeeded, failed). To avoid confusion it's best to prohibit outputs which override built-in qualifiers (e.g. succeed-all) too. There's a special use of the colon character which users need to conform with too. """ # test that each message validates correctly for message in messages: assert TaskMessageValidator.validate(message)[0] is valid # test that output validation is actually being performed id_ = flow({ 'scheduling': { 'graph': {'R1': 'foo'} }, 'runtime': { 'foo': { 'outputs': { str(random())[2:]: message for message in messages } } }, }) val = lambda: validate(id_) if valid: val() else: with pytest.raises(WorkflowConfigError): val() @pytest.mark.parametrize( 'graph, expression, message', [ pytest.param( 'foo:x', 'succeeded and (x or y)', r'foo:x is required in the graph.*' r' but optional in the completion expression', id='required-in-graph-optional-in-completion', ), pytest.param( 'foo:x?', 'succeeded and x', r'foo:x is optional in the graph.*' r' but required in the completion expression', id='optional-in-graph-required-in-completion', ), pytest.param( 'foo:x', 'succeeded', 'foo:x is required in the graph.*' 'but not referenced in the completion expression', id='required-in-graph-not-referenced-in-completion', ), pytest.param( # tests proposal point 4: # https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal 'foo:expired', 'succeeded', 'foo:expired must be optional', id='expire-required-in-graph', ), pytest.param( 'foo:expired?', 'succeeded', 'foo:expired is permitted in the graph.*' '\nTry: completion = "succeeded or expired"', id='expire-optional-in-graph-but-not-used-in-completion' ), pytest.param( # tests part of proposal point 5: # https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal 'foo', 'finished and x', '"finished" output cannot be used in completion expressions', id='finished-output-used-in-completion-expression', ), pytest.param( # https://github.com/cylc/cylc-flow/pull/6046#issuecomment-2059266086 'foo?', 'x and failed', 'foo:failed is optional in the graph.*' 'but required in the completion expression', id='failed-implicitly-optional-in-graph-required-in-completion', ), pytest.param( 'foo', '(succeed and x) or failed', 'Use "succeeded" not "succeed" in completion expressions', id='alt-compvar1', ), pytest.param( 'foo? & foo:submitted?', 'submit_fail or succeeded', 'Use "submit_failed" not "submit_fail" in completion expressions', id='alt-compvar2', ), pytest.param( 'foo? & foo:submitted?', 'submit-failed or succeeded', 'Use "submit_failed" rather than "submit-failed"' ' in completion expressions.', id='submit-failed used in completion expression', ), pytest.param( 'foo:file-1', 'succeeded or file-1', 'Replace hyphens with underscores in task outputs when' ' used in completion expressions.', id='Hyphen used in completion expression', ), pytest.param( 'foo:x', 'not succeeded or x', 'Error in .*' '\nInvalid expression', id='Non-whitelisted syntax used in completion expression', ), ] ) def test_completion_expression_invalid( flow, validate, graph, expression, message, ): """It should ensure the completion is logically consistent with the graph. Tests proposal point 5: https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal """ id_ = flow({ 'scheduling': { 'graph': {'R1': graph}, }, 'runtime': { 'foo': { 'completion': expression, 'outputs': { 'x': 'xxx', 'y': 'yyy', 'file-1': 'asdf' }, }, }, }) with pytest.raises(WorkflowConfigError, match=message): validate(id_) @pytest.mark.parametrize( 'graph, expression', [ ('foo', 'succeeded and (x or y or z)'), ('foo?', 'succeeded and (x or y or z) or failed or expired'), ('foo', '(succeeded and x) or (expired and y)'), ] ) def test_completion_expression_valid( flow, validate, graph, expression, ): id_ = flow({ 'scheduling': { 'graph': {'R1': graph}, }, 'runtime': { 'foo': { 'completion': expression, 'outputs': { 'x': 'xxx', 'y': 'yyy', 'z': 'zzz', }, }, }, }) validate(id_) def test_completion_expression_cylc7_compat( flow, validate, monkeypatch ): id_ = flow({ 'scheduling': { 'graph': {'R1': 'foo'}, }, 'runtime': { 'foo': { 'completion': 'succeeded and x', 'outputs': { 'x': 'xxx', }, }, }, }) monkeypatch.setattr('cylc.flow.flags.cylc7_back_compat', True) with pytest.raises( WorkflowConfigError, match="completion cannot be used in Cylc 7 compatibility mode." ): validate(id_) def test_unique_messages( flow, validate ): """Task messages must be unique in the [outputs] section. See: https://github.com/cylc/cylc-flow/issues/6056 """ id_ = flow({ 'scheduling': { 'graph': {'R1': 'foo'} }, 'runtime': { 'foo': { 'outputs': { 'a': 'foo', 'b': 'bar', 'c': 'baz', 'd': 'foo', } }, } }) with pytest.raises( WorkflowConfigError, match=( r'"\[runtime\]\[foo\]\[outputs\]d = foo"' ' - messages must be unique' ), ): validate(id_) cylc-flow-8.6.4/tests/integration/validate/test_jinja2.py0000664000175000017500000000455115202510242023656 0ustar alastairalastair# 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 . import re from textwrap import dedent from cylc.flow.exceptions import InputError from cylc.flow.parsec.exceptions import Jinja2Error import pytest @pytest.fixture def flow_cylc(tmp_path): """Write a flow.cylc file containing the provided text.""" def _inner(text): (tmp_path / 'flow.cylc').write_text(dedent(text).strip()) return tmp_path return _inner @pytest.mark.parametrize( 'line', [ pytest.param("raise('some error message')", id='raise'), pytest.param("assert(False, 'some error message')", id='assert'), ], ) def test_raise_helper(flow_cylc, validate, line, monkeypatch): """It should raise an error from within Jinja2.""" # it should raise a minimal InputError # (because assert/raise are used to validate inputs) # - intended for users of the workflow src_dir = flow_cylc(f''' #!Jinja2 {{{{ {line} }}}} ''') with pytest.raises( InputError, match=( r'^some error message' r'\n\(add --verbose for more context\)$' ), ): validate(src_dir) # in verbose mode, it should raise the full error # (this includes the Jinja2 context including file/line info) # - intended for developers of the workflow monkeypatch.setattr('cylc.flow.flags.verbosity', 1) with pytest.raises( Jinja2Error, match=( r'^some error message' r'\nFile /.*/pytest-.*/test_.*/flow.cylc' r'\n #!Jinja2' r'\n \{\{ ' + re.escape(line) + r' \}\}' r'\t<-- Jinja2AssertionError$' ) ): validate(src_dir) cylc-flow-8.6.4/tests/integration/test_get_old_tvars.py0000664000175000017500000000535315202510242023545 0ustar alastairalastair# 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 . import pytest from pytest import param from cylc.flow.option_parsers import Options from cylc.flow.scripts.validate import ( run as validate, get_option_parser as validate_gop ) from cylc.flow.scripts.view import ( _main as view, get_option_parser as view_gop ) from cylc.flow.scripts.graph import ( _main as graph, get_option_parser as graph_gop ) from cylc.flow.scripts.config import ( _main as config, get_option_parser as config_gop ) from cylc.flow.scripts.list import ( _main as cylclist, get_option_parser as list_gop ) @pytest.fixture(scope='module') def _setup(mod_scheduler, mod_flow): """Provide an installed flow with a database to try assorted simple Cylc scripts against. """ conf = { '#!jinja2': '', 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': r'{{FOO}}' } } } schd = mod_scheduler(mod_flow(conf), templatevars=['FOO="bar"']) yield schd @pytest.mark.parametrize( 'function, parser, expect', ( param(validate, validate_gop, 'Valid for', id="validate"), param(view, view_gop, 'FOO', id="view"), param(graph, graph_gop, '1/bar', id='graph'), param(config, config_gop, 'R1 = bar', id='config'), param(cylclist, list_gop, 'bar', id='list') ) ) async def test_validate_with_old_tvars( _setup, mod_start, capsys, function, parser, expect, ): """It (A Cylc CLI Command - see parameters) can get template vars stored in db. Else the jinja2 in the config would cause these tasks to fail. """ parser = parser() opts = Options(parser)() if function == graph: opts.reference = True async with mod_start(_setup): if function in {view, cylclist, graph}: await function(opts, _setup.workflow_name) else: await function(parser, opts, _setup.workflow_name) assert expect in capsys.readouterr().out cylc-flow-8.6.4/tests/integration/test_id_cli.py0000664000175000017500000000672715202510242022142 0ustar alastairalastair# 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 . """Some integration tests running on live workflows to test filtering properly. """ import pytest from cylc.flow.pathutil import get_cylc_run_dir from cylc.flow.id_cli import parse_ids_async @pytest.fixture(scope='module') async def harness( mod_run, mod_scheduler, mod_flow, mod_one_conf, mod_test_dir, ): """Create three workflows, two running, one stopped.""" reg_prefix = mod_test_dir.relative_to(get_cylc_run_dir()) # abc:running reg1 = mod_flow(mod_one_conf, name='abc') schd1 = mod_scheduler(reg1) # def:running reg2 = mod_flow(mod_one_conf, name='def') schd2 = mod_scheduler(reg2) # ghi:stopped reg3 = mod_flow(mod_one_conf, name='ghi') async with mod_run(schd1): async with mod_run(schd2): yield reg_prefix, reg1, reg2, reg3 async def test_glob_wildcard(harness): """It should search for workflows using globs.""" reg_prefix, reg1, reg2, reg3 = harness # '*' should return all workflows workflows, _ = await parse_ids_async( f'{reg_prefix / "*"}', constraint='workflows', match_workflows=True, ) assert sorted(workflows) == sorted([reg1, reg2]) workflows, _ = await parse_ids_async( f'{reg_prefix / "z*"}', constraint='workflows', match_workflows=True, ) assert sorted(workflows) == sorted([]) async def test_glob_pattern(harness): """It should support fnmatch syntax including square brackets.""" # [a]* should match workflows starting with "a" reg_prefix, reg1, reg2, reg3 = harness workflows, _ = await parse_ids_async( f'{reg_prefix / "[a]*"}', constraint='workflows', match_workflows=True, ) assert sorted(workflows) == sorted([reg1]) workflows, _ = await parse_ids_async( f'{reg_prefix / "[z]*"}', constraint='workflows', match_workflows=True, ) assert sorted(workflows) == sorted([]) async def test_state_filter(harness): """It should filter by workflow state.""" reg_prefix, reg1, reg2, reg3 = harness # '*' should return all workflows workflows, _ = await parse_ids_async( f'{reg_prefix / "*"}', constraint='workflows', match_workflows=True, match_active=None, ) assert sorted(workflows) == sorted([reg1, reg2, reg3]) workflows, _ = await parse_ids_async( f'{reg_prefix / "*"}', constraint='workflows', match_workflows=True, match_active=True, ) assert sorted(workflows) == sorted([reg1, reg2]) workflows, _ = await parse_ids_async( f'{reg_prefix / "*"}', constraint='workflows', match_workflows=True, match_active=False, ) assert sorted(workflows) == sorted([reg3]) cylc-flow-8.6.4/tests/integration/test_xtrigger_mgr.py0000664000175000017500000006217215202510242023413 0ustar alastairalastair# 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 . """Tests for the behaviour of xtrigger manager.""" import asyncio from pathlib import Path from textwrap import dedent from typing import cast, Iterable from cylc.flow import commands from cylc.flow.data_messages_pb2 import PbTaskProxy from cylc.flow.data_store_mgr import FAMILY_PROXIES, TASK_PROXIES from cylc.flow.id import TaskTokens from cylc.flow.pathutil import get_workflow_run_dir from cylc.flow.scheduler import Scheduler from cylc.flow.subprocctx import SubFuncContext from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED async def test_2_xtriggers(flow, start, scheduler, monkeypatch): """Test that if an itask has 4 wall_clock triggers with different offsets that xtrigger manager gets all of them. https://github.com/cylc/cylc-flow/issues/5783 n.b. Clock 3 exists to check the memoization path is followed, and causing this test to give greater coverage. Clock 4 & 5 test higher precision offsets than the CPF. """ task_point = 1588636800 # 2020-05-05 ten_years_ahead = 1904169600 # 2030-05-05 PT2H35M31S_ahead = 1588646131 # 2020-05-05 02:35:31 PT2H35M31S_behind = 1588627469 # 2020-05-04 21:24:29 monkeypatch.setattr( 'cylc.flow.xtriggers.wall_clock.time', lambda: ten_years_ahead - 1 ) id_ = flow({ 'scheduler': { 'cycle point format': 'CCYY-MM-DD', }, 'scheduling': { 'initial cycle point': '2020-05-05', 'xtriggers': { 'clock_1': 'wall_clock()', 'clock_2': 'wall_clock(offset=P10Y)', 'clock_3': 'wall_clock(offset=P10Y)', 'clock_4': 'wall_clock(offset=PT2H35M31S)', 'clock_5': 'wall_clock(offset=-PT2H35M31S)', }, 'graph': { 'R1': ( '@clock_1 & @clock_2 & @clock_3 & @clock_4 & @clock_5' ' => foo' ) } } }) schd = scheduler(id_) async with start(schd): foo_proxy = schd.pool.get_tasks()[0] clock_1_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_1') clock_2_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_2') clock_3_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_2') clock_4_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_4') clock_5_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_5') assert clock_1_ctx.func_kwargs['trigger_time'] == task_point assert clock_2_ctx.func_kwargs['trigger_time'] == ten_years_ahead assert clock_3_ctx.func_kwargs['trigger_time'] == ten_years_ahead assert clock_4_ctx.func_kwargs['trigger_time'] == PT2H35M31S_ahead assert clock_5_ctx.func_kwargs['trigger_time'] == PT2H35M31S_behind schd.xtrigger_mgr.call_xtriggers_async(foo_proxy) assert foo_proxy.state.xtriggers == { 'clock_1': True, 'clock_2': False, 'clock_3': False, 'clock_4': True, 'clock_5': True, } async def test_1_xtrigger_2_tasks(flow, start, scheduler, mocker): """ If two tasks depend on the same satisfied xtrigger, put_xtriggers should only be called once - when the xtrigger gets satisfied. - https://github.com/cylc/cylc-flow/pull/5908 So long as both tasks are active at once: - https://github.com/cylc/cylc-flow/issues/7027 """ id_ = flow({ 'scheduling': { 'initial cycle point': '2020', 'graph': { 'R1': '@wall_clock => foo & bar' } } }) schd = scheduler(id_) spy = mocker.spy(schd.workflow_db_mgr, 'put_xtriggers') async with start(schd): # Call the clock trigger via its dependent tasks, to get it satisfied. for task in schd.pool.get_tasks(): # (For clock triggers this is synchronous) schd.xtrigger_mgr.call_xtriggers_async(task) # It should now be satisfied. assert task.state.xtriggers == {'wall_clock': True} # Check one put_xtriggers call only, not two. assert spy.call_count == 1 # Note on master prior to GH #5908 the call is made from the # scheduler main loop when the two tasks become satisfied, # resulting in two calls to put_xtriggers. This test fails # on master, but with call count 0 (not 2) because the main # loop doesn't run in this test. async def test_1_xtrigger_2_tasks_async( flow, start, scheduler, mocker, caplog ): """ Like test_1_xtrigger_2_tasks but for async (not clock) xtriggers. If two tasks depend on the same satisfied xtrigger, put_xtriggers should only be called once - when the xtrigger gets satisfied (so long as both are active at once - https://github.com/cylc/cylc-flow/issues/7027 """ id_ = flow({ 'scheduling': { 'cycling mode': 'integer', 'xtriggers': { 'echo': 'echo("whatever", succeed=False)', }, 'graph': { 'R1': ''' @echo => foo & bar ''' }, }, }) schd = scheduler(id_) spy = mocker.spy(schd.workflow_db_mgr, 'put_xtriggers') async with start(schd): foo = schd.pool._get_task_by_id('1/foo') bar = schd.pool._get_task_by_id('1/bar') schd.xtrigger_mgr.call_xtriggers_async(foo) assert "Commencing xtrigger" in caplog.text caplog.clear() schd.xtrigger_mgr.call_xtriggers_async(bar) assert "Commencing xtrigger" not in caplog.text caplog.clear() satisfy_xtrigger_functions(schd) # mock results assert "xtrigger succeeded" in caplog.text caplog.clear() schd.xtrigger_mgr.call_xtriggers_async(foo) # process callbacks assert "satisfying xtrigger" in caplog.text caplog.clear() schd.xtrigger_mgr.call_xtriggers_async(bar) # process callbacks assert "satisfying xtrigger" in caplog.text caplog.clear() # It should now be satisfied. assert foo.state.xtriggers == {'echo': True} assert bar.state.xtriggers == {'echo': True} # Check put_xtriggers called once, not twice. assert spy.call_count == 1 async def test_1_xtrigger_2_tasks_later( flow, start, scheduler, mocker, caplog ): """ If two tasks depend on the same satisfied xtrigger, but are not both active at the same time, the xtrigger will need to be satisfied twice - https://github.com/cylc/cylc-flow/issues/7027 """ id_ = flow({ 'scheduling': { 'cycling mode': 'integer', 'xtriggers': { 'echo': 'echo("whatever", succeed=False)', }, 'graph': { 'R1': ''' @echo => foo & bar # spawn bar after @echo is housekept post satisfying foo foo => bar ''' }, }, }) schd = scheduler(id_) spy = mocker.spy(schd.workflow_db_mgr, 'put_xtriggers') async with start(schd): # Get foo at startup foo = schd.pool._get_task_by_id('1/foo') # call the xtrigger schd.xtrigger_mgr.call_xtriggers_async(foo) assert "Commencing xtrigger" in caplog.text caplog.clear() # satisfy it satisfy_xtrigger_functions(schd) # mock results assert "xtrigger succeeded" in caplog.text caplog.clear() # process callback schd.xtrigger_mgr.call_xtriggers_async(foo) assert "satisfying xtrigger" in caplog.text caplog.clear() # foo should now be satisfied. assert foo.state.xtriggers == {'echo': True} # this will delete the xtrigger - nothing else depends on it schd.xtrigger_mgr.housekeep([foo]) # Spawn bar and remove foo schd.pool.spawn_on_output(foo, TASK_OUTPUT_SUCCEEDED) bar = schd.pool._get_task_by_id('1/bar') # the xtrigger should be called again for bar schd.xtrigger_mgr.call_xtriggers_async(bar) assert "Commencing xtrigger" in caplog.text caplog.clear() # satisfy it satisfy_xtrigger_functions(schd) # mock results assert "xtrigger succeeded" in caplog.text caplog.clear() # process callback schd.xtrigger_mgr.call_xtriggers_async(bar) assert "satisfying xtrigger" in caplog.text caplog.clear() # bar should now be satisfied. assert bar.state.xtriggers == {'echo': True} # Check put_xtriggers called twice, not once. assert spy.call_count == 2 async def test_xtriggers_restart(flow, start, scheduler, db_select): """It should write satisfied xtriggers to the DB and load on restart. This checks persistence of xtrigger function results across restarts. See also test_set_xtriggers_restart for task dependence on xtriggers. """ id_ = flow({ 'scheduling': { 'xtriggers': { 'x100': 'xrandom(100)', # always succeeds 'x0': 'xrandom(0)' # never succeeds }, 'graph': { 'R1': ''' @x100 => foo @x0 = > bar ''' }, } }) # start the workflow & run the xtrigger schd = scheduler(id_) async with start(schd): # run all xtriggers for task in schd.pool.get_tasks(): schd.xtrigger_mgr.call_xtriggers_async(task) # two xtriggers should have been scheduled to run # and x100 should succeed assert len(schd.proc_pool.queuings) + len(schd.proc_pool.runnings) == 2 # wait for it to return for _ in range(50): await asyncio.sleep(0.1) schd.proc_pool.process() if len(schd.proc_pool.runnings) == 0: break else: raise Exception('Process pool did not clear') # the satisfied x100 should be written to the DB db_xtriggers = db_select(schd, True, 'xtriggers') assert len(db_xtriggers) == 1 assert db_xtriggers[0][0] == 'xrandom(100)' assert db_xtriggers[0][1].startswith('{"COLOR":') # (xrandom result dict) # restart the workflow, the xtrigger should *not* run again schd = scheduler(id_) async with start(schd): # run all xtriggers for task in schd.pool.get_tasks(): schd.xtrigger_mgr.call_xtriggers_async(task) # satisfied x100 should have been loaded from the DB # so only one xtrigger should be scheduled to run now assert len(schd.proc_pool.queuings) + len(schd.proc_pool.runnings) == 1 # x0 should not be satisfied bar = schd.pool._get_task_by_id('1/bar') assert not bar.state.xtriggers["x0"] # x100 should now be satisfied in the task pool and the datastore foo = schd.pool._get_task_by_id('1/foo') assert foo.state.xtriggers["x100"] await schd.update_data_structure() [xtrig] = [ p for t in cast( 'Iterable[PbTaskProxy]', schd.data_store_mgr.data[ schd.data_store_mgr.workflow_id ][ TASK_PROXIES ].values() ) for p in t.xtriggers.values() if p.label == "x100" ] assert xtrig.id == "xrandom(100)" assert xtrig.satisfied # check the DB to ensure no additional entries have been created assert db_select(schd, True, 'xtriggers') == db_xtriggers async def test_set_xtrig_prereq_restart(flow, start, scheduler, db_select): """Satisfied xtrigger prerequisites should persist across restart. (Task prerequisites can be artificially satisfied by "cylc set"). See also test_xtriggers_restart, for persistence of xtrigger results. """ id_ = flow({ 'scheduling': { 'xtriggers': { 'x0': 'xrandom(0)' # never succeeds naturally }, 'graph': { 'R1': ''' @x0 = > foo & bar ''' }, } }) schd = scheduler(id_) async with start(schd): # artificially set dependence of foo on x0 schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, [], ['xtrigger/x0:succeeded'], [] ) # the satisfied x0 prerequisite should be written to the DB [db_pre] = db_select(schd, True, 'task_prerequisites') assert db_pre == ('1', 'foo', '[1]', 'x0', 'xtrigger', 'succeeded', '1') # restart the workflow schd = scheduler(id_) async with start(schd): # run all xtriggers for task in schd.pool.get_tasks(): schd.xtrigger_mgr.call_xtriggers_async(task) # foo's dependence on x0 should be satisfied from the DB # but bar still depends on it so the scheduler should still call it. assert len(schd.proc_pool.queuings) + len(schd.proc_pool.runnings) == 1 # "@x0 => bar" should not be satisfied bar = schd.pool._get_task_by_id('1/bar') assert not bar.state.xtriggers["x0"] # but "x0 => foo" should be, in the task pool and the datastore foo = schd.pool._get_task_by_id('1/foo') assert foo.state.xtriggers["x0"] await schd.update_data_structure() xtrigs = [ (t.id, p) for t in cast( 'Iterable[PbTaskProxy]', schd.data_store_mgr.data[ schd.data_store_mgr.workflow_id ][ TASK_PROXIES ].values() ) for p in t.xtriggers.values() ] assert len(xtrigs) == 2 for (id, xtrig) in xtrigs: if id.endswith('foo'): assert xtrig.id == "xrandom(0)" assert xtrig.satisfied elif id.endswith('bar'): assert xtrig.id == "xrandom(0)" assert not xtrig.satisfied else: assert False async def test_error_in_xtrigger(flow, start, scheduler): """Failure in an xtrigger is handled nicely. """ id_ = flow({ 'scheduling': { 'xtriggers': { 'mytrig': 'mytrig()' }, 'graph': { 'R1': '@mytrig => foo' }, } }) # add a custom xtrigger to the workflow run_dir = Path(get_workflow_run_dir(id_)) xtrig_dir = run_dir / 'lib/python' xtrig_dir.mkdir(parents=True) (xtrig_dir / 'mytrig.py').write_text(dedent(''' def mytrig(*args, **kwargs): raise Exception('This Xtrigger is broken') ''')) schd = scheduler(id_) async with start(schd) as log: foo = schd.pool.get_tasks()[0] schd.xtrigger_mgr.call_xtriggers_async(foo) for _ in range(50): await asyncio.sleep(0.1) schd.proc_pool.process() if len(schd.proc_pool.runnings) == 0: break else: raise Exception('Process pool did not clear') error = log.messages[-1].split('\n') assert error[-2] == 'Exception: This Xtrigger is broken' assert error[0] == 'ERROR in xtrigger mytrig()' async def test_1_seq_clock_trigger_2_tasks(flow, start, scheduler): """Test that all tasks dependent on a sequential clock trigger continue to spawn after the first cycle. See https://github.com/cylc/cylc-flow/issues/6204 """ id_ = flow({ 'scheduler': { 'cycle point format': 'CCYY', }, 'scheduling': { 'initial cycle point': '1990', 'graph': { 'P1Y': '@wall_clock => foo & bar', }, }, }) schd: Scheduler = scheduler(id_) async with start(schd): start_task_pool = schd.pool.get_task_ids() assert start_task_pool == {'1990/foo', '1990/bar'} for _ in range(3): await schd._main_loop() assert schd.pool.get_task_ids() == start_task_pool.union( f'{year}/{name}' for year in range(1991, 1994) for name in ('foo', 'bar') ) async def test_set_xtrig_prereq_reload(flow, start, scheduler, db_select): """Satisfied xtrigger prerequisites should persist across reload. (Task prerequisites can be artificially satisfied by "cylc set"). """ id_ = flow({ 'scheduling': { 'xtriggers': { 'x0': 'xrandom(0)' # never succeeds naturally }, 'graph': { 'R1': ''' @x0 = > foo & bar ''' }, } }) schd = scheduler(id_) async with start(schd): # artificially set dependence of foo on x0 schd.pool.set_prereqs_and_outputs( {TaskTokens('1', 'foo')}, [], ['xtrigger/x0:succeeded'], [] ) # reload the workflow await commands.run_cmd(commands.reload_workflow(schd)) # run all xtriggers for task in schd.pool.get_tasks(): schd.xtrigger_mgr.call_xtriggers_async(task) # foo's dependence on x0 should remain satisfied, but bar still depends # on it so the function should still be called by the scheduler. assert len(schd.proc_pool.queuings) + len(schd.proc_pool.runnings) == 1 # "@x0 => bar" should not be satisfied bar = schd.pool._get_task_by_id('1/bar') assert not bar.state.xtriggers["x0"] # but "x0 => foo" should be, in the task pool and the datastore foo = schd.pool._get_task_by_id('1/foo') assert foo.state.xtriggers["x0"] await schd.update_data_structure() xtrigs = [ (t.id, p) for t in cast( 'Iterable[PbTaskProxy]', schd.data_store_mgr.data[ schd.data_store_mgr.workflow_id ][ TASK_PROXIES ].values() ) for p in t.xtriggers.values() ] assert len(xtrigs) == 2 for (id, xtrig) in xtrigs: if id.endswith('foo'): assert xtrig.id == "xrandom(0)" assert xtrig.satisfied elif id.endswith('bar'): assert xtrig.id == "xrandom(0)" assert not xtrig.satisfied else: assert False async def test_force_satisfy(flow, start, scheduler, log_filter): """It should satisfy valid xtriggers and ignore invalid ones.""" id_ = flow({ 'scheduling': { 'xtriggers': { 'x': 'xrandom(0)' }, 'graph': { 'R1': '@x => foo' }, } }) schd = scheduler(id_) async with start(schd): foo = schd.pool.get_tasks()[0] # check x not satisfied yet assert not foo.state.xtriggers['x'] # not satisfied # force satisfy it xtrigs = { "x": True, # it should satisfy this one "y": True # it should just ignore this one } schd.xtrigger_mgr.force_satisfy(foo, xtrigs) assert foo.state.xtriggers['x'] # satisfied assert log_filter( contains=('prerequisite force-satisfied: x = xrandom(0)')) # force satisfy it again schd.xtrigger_mgr.force_satisfy(foo, xtrigs) assert foo.state.xtriggers['x'] # satisfied assert log_filter( contains=('prerequisite already satisfied: x = xrandom(0)')) # force unsatisfy it schd.xtrigger_mgr.force_satisfy(foo, {"x": False}) assert not foo.state.xtriggers['x'] # not satisfied assert log_filter( contains=('prerequisite force-unsatisfied: x = xrandom(0)')) # force unsatisfy it again schd.xtrigger_mgr.force_satisfy(foo, {"x": False}) assert not foo.state.xtriggers['x'] # not satisfied assert log_filter( contains=('prerequisite already unsatisfied: x = xrandom(0)') ) async def test_data_store(flow, start, scheduler): """It should update the data store with xtrigger state.""" id_ = flow({ 'scheduling': { 'initial cycle point': 'previous(T00)', 'xtriggers': { 'clock1': 'wall_clock()', 'clock2': 'wall_clock(offset="P1Y")', }, 'graph': { 'R1': ''' @clock1 => foo @clock2 => foo ''' } } }) schd: Scheduler = scheduler(id_) async with start(schd): await schd.update_data_structure() itask = schd.pool.get_tasks()[0] # extract xtrigger entry from the data store xtriggers = schd.data_store_mgr.data[ schd.tokens.id ][TASK_PROXIES][itask.tokens.id].xtriggers clock1, clock2 = sorted(x for x in xtriggers if 'wall_clock' in x) # it should not be satisfied (yet) assert xtriggers[clock1].label == 'clock1' assert xtriggers[clock1].satisfied is False # execute the xtrigger schd.xtrigger_mgr.call_xtriggers_async(itask) # an update delta should be produced # NOTE: both xtriggers should be present in the update # (see https://github.com/cylc/cylc-flow/issues/6307) task_delta = schd.data_store_mgr.updated[TASK_PROXIES][itask.tokens.id] assert task_delta.xtriggers[clock1].satisfied is True assert task_delta.xtriggers[clock2].satisfied is False # the xtrigger should be satisfied in the data store await schd.update_data_structure() assert xtriggers[clock1].label == 'clock1' assert xtriggers[clock1].satisfied is True def satisfy_xtrigger_functions(schd, stdout='[true, {}]', ret_code=0): """Satisfy and dequeue any xtrigger subprocesses.""" for item in list(schd.proc_pool.queuings): ctx, _, callback, *_ = item if isinstance(schd.proc_pool.queuings[0][0], SubFuncContext): # dequeue from the proc pool schd.proc_pool.queuings.remove(item) # mock the xtrigger output ctx.ret_code = ret_code ctx.out = stdout # run the callback callback(ctx) async def test_xtrigger_modifiers(flow, scheduler, start): """It should update xtrigger derived task modifiers.""" id_ = flow({ 'scheduling': { 'initial cycle point': 'previous(T00)', 'xtriggers': { 'echo': 'echo("whatever", succeed=True)', }, 'graph': { 'R1': ''' @wall_clock => foo @echo => foo ''' }, }, 'runtime': { 'foo': { 'execution retry delays': 'PT0S', }, }, }) schd = scheduler(id_) async with start(schd): # configure the task to retry itask = schd.pool.get_tasks()[0] schd.task_events_mgr._retry_task(itask, 0) await schd.update_data_structure() # the task proxy object in the data store ds_tproxy = schd.data_store_mgr.data[schd.tokens.id][TASK_PROXIES][ itask.tokens.id ] # the "root" family proxy object in the data store ds_fproxy = schd.data_store_mgr.data[schd.tokens.id][FAMILY_PROXIES][ itask.tokens.duplicate(task='root').id ] # the task modifiers should be initialised on task creation assert len(itask.state.xtriggers) == 3 assert ds_tproxy.is_retry is True assert ds_tproxy.is_wallclock is True assert ds_tproxy.is_xtriggered is True # the modifiers should bubble up the family tree assert ds_fproxy.is_retry is True assert ds_fproxy.is_wallclock is True assert ds_fproxy.is_xtriggered is True # run the xtriggers schd.xtrigger_mgr.call_xtriggers_async(itask) # run xtrigs satisfy_xtrigger_functions(schd) # mock results schd.xtrigger_mgr.call_xtriggers_async(itask) # process callbacks await schd.update_data_structure() # the task modifiers should have been updated assert ds_tproxy.is_retry is False assert ds_tproxy.is_wallclock is False assert ds_tproxy.is_xtriggered is False # the modifiers should bubble up the family tree assert ds_fproxy.is_retry is False assert ds_fproxy.is_wallclock is False assert ds_fproxy.is_xtriggered is False cylc-flow-8.6.4/tests/integration/test_dbstatecheck.py0000664000175000017500000001122015202510242023323 0ustar alastairalastair# 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 . """Tests for the backend method of workflow_state""" import pytest from cylc.flow.commands import ( run_cmd, force_trigger_tasks ) from cylc.flow.dbstatecheck import CylcWorkflowDBChecker from cylc.flow.scheduler import Scheduler @pytest.fixture(scope='module') async def checker( mod_flow, mod_scheduler, mod_run, mod_complete ): """Make a real world database. We could just write the database manually but this is a better test of the overall working of the function under test. """ wid = mod_flow({ 'scheduling': { 'initial cycle point': '1000', 'final cycle point': '1001', 'graph': { 'P1Y': ''' good:succeeded bad:failed? output:custom_output ''' }, }, 'runtime': { 'bad': {'simulation': {'fail cycle points': '1000'}}, 'output': { 'outputs': {'trigger': 'message', 'custom_output': 'foo'} } } }) schd: Scheduler = mod_scheduler(wid, paused_start=False) async with mod_run(schd): # allow a cycle of the main loop to pass so that flow 2 can be # added to db await mod_complete(schd) # trigger a new task in flow 2 await run_cmd(force_trigger_tasks(schd, ['1000/good'], ['2'])) # update the database schd.process_workflow_db_queue() # yield a DB checker with CylcWorkflowDBChecker( 'somestring', 'utterbunkum', schd.workflow_db_mgr.pub_path ) as _checker: yield _checker def test_basic(checker): """Pass no args, get unfiltered output""" result = checker.workflow_state_query() expect = [ ['bad', '10000101T0000Z', 'failed'], ['bad', '10010101T0000Z', 'succeeded'], ['good', '10000101T0000Z', 'succeeded'], ['good', '10010101T0000Z', 'succeeded'], ['output', '10000101T0000Z', 'succeeded'], ['output', '10010101T0000Z', 'succeeded'], ['good', '10000101T0000Z', 'waiting', '(flows=2)'], ['good', '10010101T0000Z', 'waiting', '(flows=2)'], ] assert sorted(result) == sorted(expect) def test_task(checker): """Filter by task name""" result = checker.workflow_state_query(task='bad') assert sorted(result) == ([ ['bad', '10000101T0000Z', 'failed'], ['bad', '10010101T0000Z', 'succeeded'] ]) def test_point(checker): """Filter by point""" result = checker.workflow_state_query(cycle='10000101T0000Z') assert sorted(result) == sorted([ ['bad', '10000101T0000Z', 'failed'], ['good', '10000101T0000Z', 'succeeded'], ['output', '10000101T0000Z', 'succeeded'], ['good', '10000101T0000Z', 'waiting', '(flows=2)'], ]) def test_status(checker): """Filter by status""" result = checker.workflow_state_query(selector='failed') expect = [ ['bad', '10000101T0000Z', 'failed'], ] assert result == expect def test_output(checker): """Filter by flow number""" result = checker.workflow_state_query(selector='message', is_message=True) expect = [ [ 'output', '10000101T0000Z', "{'submitted': 'submitted', 'started': 'started', 'succeeded': " "'succeeded', 'trigger': 'message', 'custom_output': 'foo'}", ], [ 'output', '10010101T0000Z', "{'submitted': 'submitted', 'started': 'started', 'succeeded': " "'succeeded', 'trigger': 'message', 'custom_output': 'foo'}", ], ] assert result == expect def test_flownum(checker): """Pass no args, get unfiltered output""" result = checker.workflow_state_query(flow_num=2) expect = [ ['good', '10000101T0000Z', 'waiting', '(flows=2)'], ['good', '10010101T0000Z', 'waiting', '(flows=2)'], ] assert result == expect cylc-flow-8.6.4/tests/integration/test_examples.py0000664000175000017500000002153715202510242022531 0ustar alastairalastair# 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 . """Working documentation for the integration test framework. Here are some examples which cover a range of uses (and also provide some useful testing in the process 😀.) """ import asyncio import logging from pathlib import Path import pytest from cylc.flow import __version__ from cylc.flow.scheduler import Scheduler async def test_create_flow(flow, run_dir): """Use the flow fixture to create workflows on the file system.""" # Ensure a flow.cylc file gets written out id_ = flow({ 'scheduler': { 'allow implicit tasks': True }, 'scheduling': { 'graph': { 'R1': 'foo' } } }) workflow_dir = run_dir / id_ flow_file = workflow_dir / 'flow.cylc' assert workflow_dir.exists() assert flow_file.exists() async def test_run(flow, scheduler, run, one_conf): """Create a workflow, initialise the scheduler and run it.""" # Ensure the scheduler can survive for at least one second without crashing id_ = flow(one_conf) schd = scheduler(id_) async with run(schd): await asyncio.sleep(1) # this yields control to the main loop async def test_logging(flow, scheduler, start, one_conf, log_filter): """We can capture log records when we run a scheduler.""" # Ensure that the cylc version is logged on startup. id_ = flow(one_conf) schd = scheduler(id_) async with start(schd): # this returns a list of log records containing __version__ assert log_filter(contains=__version__) async def test_scheduler_arguments(flow, scheduler, start, one_conf): """We can provide options to the scheduler when we __init__ it. These options match their command line equivalents. Use the `dest` value specified in the option parser. """ # Ensure the paused_start option is obeyed by the scheduler. id_ = flow(one_conf) schd = scheduler(id_, paused_start=True) async with start(schd): assert schd.is_paused id_ = flow(one_conf) schd = scheduler(id_, paused_start=False) async with start(schd): assert not schd.is_paused async def test_shutdown(flow, scheduler, start, one_conf): """Shut down a workflow. The scheduler automatically shuts down once you exit the `async with` block, however you can manually shut it down within this block if you like. """ # Ensure the TCP server shuts down with the scheduler. id_ = flow(one_conf) schd = scheduler(id_) async with start(schd): pass assert schd.server.replier.socket.closed async def test_install(flow, scheduler, one_conf, run_dir): """You don't have to run workflows, it's usually best not to! You can take the scheduler through the startup sequence as far as needed for your test. """ # Ensure the installation of the job script is completed. id_ = flow(one_conf) schd = scheduler(id_) await schd.install() assert Path( run_dir, schd.workflow, '.service', 'etc', 'job.sh' ).exists() async def test_task_pool(one, start): """You don't have to run the scheduler to play with the task pool. There are two fixtures to start a scheduler: `start` Takes a scheduler through the startup sequence. `run` Takes a scheduler through the startup sequence, then sets the main loop going. Unless you need the Scheduler main loop running, use `start`. This test uses a pre-prepared Scheduler called "one". """ # Ensure that the correct number of tasks get added to the task pool. async with start(one): # pump the scheduler's heart manually one.pool.release_runahead_tasks() assert len(one.pool.active_tasks) == 1 async def test_exception(one, run, log_filter): """Through an exception into the scheduler to see how it will react. You have to do this from within the scheduler itself. The easy way is to patch the object. """ class MyException(Exception): pass # replace the main loop with something that raises an exception def killer(): raise MyException('mess') one._main_loop = killer # make sure that this error causes the flow to shutdown with pytest.raises(MyException): async with run(one): # The `run` fixture's shutdown logic waits for the main loop to run pass # make sure the exception was logged assert len(log_filter(logging.CRITICAL, contains='mess')) == 1 # make sure the server socket has closed - a good indication of a # successful clean shutdown assert one.server.replier.socket.closed @pytest.fixture(scope='module') async def myflow(mod_flow, mod_scheduler, mod_one_conf): """You can save setup/teardown time by reusing fixtures Write a module-scoped fixture and it can be shared by all tests in the current module. The standard fixtures all have `mod_` alternatives to allow you to do this. Pytest has been configured to run all tests from the same module in the same xdist worker, in other words, module scoped fixtures only get created once per module, even when distributing tests. Obviously this goes with the usual warnings about not mutating the object you are testing in the tests. """ id_ = mod_flow(mod_one_conf) schd = mod_scheduler(id_) return schd def test_module_scoped_fixture(myflow): """Ensure the host is set on __init__. The myflow fixture will be shared between all test functions within this Python module. """ assert myflow.host async def test_db_select(one, start, db_select): """Demonstrate and test querying the workflow database.""" # run a workflow schd: Scheduler = one async with start(schd): # Select all from workflow_params table: assert ('UTC_mode', '0') in db_select(schd, False, 'workflow_params') # Select name & status columns from task_states table: results = db_select(schd, False, 'task_states', 'name', 'status') assert results[0] == ('one', 'waiting') # Select all columns where name==one & status==waiting from # task_states table: results = db_select( schd, False, 'task_states', name='one', status='waiting') assert len(results) == 1 async def test_reflog(flow, scheduler, run, reflog, complete): """Test the triggering of tasks. This is the integration test version of "reftest" in the funtional tests. It works by capturing the triggers which caused each submission so that they can be compared with the expected outcome. """ id_ = flow({ 'scheduling': { 'initial cycle point': '1', 'final cycle point': '1', 'cycling mode': 'integer', 'graph': { 'P1': ''' a => b => c x => b => z b[-P1] => b ''' } } }) schd = scheduler(id_, paused_start=False) async with run(schd): triggers = reflog(schd) # Note: add flow_nums=True to capture flows await complete(schd) assert triggers == { # 1/a was triggered by nothing (i.e. it's parentless) ('1/a', None), # 1/b was triggered by three tasks (note the pre-initial dependency) ('1/b', ('0/b', '1/a', '1/x')), ('1/c', ('1/b',)), ('1/x', None), ('1/z', ('1/b',)), } async def test_reftest(flow, scheduler, reftest): """Test the triggering of tasks. This uses the reftest fixture which combines the reflog and complete fixtures. Suitable for use when you just want to do a simple reftest. """ id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a => b' } } }) schd = scheduler(id_, paused_start=False) assert await reftest(schd) == { ('1/a', None), ('1/b', ('1/a',)), } async def test_show(one: Scheduler, start, cylc_show): """Demonstrate the `cylc_show` fixture""" async with start(one): out = await cylc_show(one, '1/one') assert list(out.keys()) == ['1/one'] assert out['1/one']['state'] == 'waiting' cylc-flow-8.6.4/tests/integration/test_workflow_events.py0000664000175000017500000002531615202510242024150 0ustar alastairalastair# 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 . import asyncio from types import MethodType import pytest from cylc.flow.scheduler import SchedulerError from cylc.flow.workflow_events import WorkflowEventHandler EVENTS = ( 'startup', 'shutdown', 'abort', 'workflow timeout', 'stall', 'stall timeout', 'inactivity timeout', 'restart timeout', ) @pytest.fixture async def test_scheduler(flow, scheduler, capcall): events = capcall( 'cylc.flow.scheduler.Scheduler.run_event_handlers', ) def get_events(): return {e[0][1] for e in events} def _schd(event_config=None, config=None, **opts): assert not (event_config and config) if not config: config = { 'scheduler': { 'events': { 'mail events': ', '.join(EVENTS), **(event_config or {}), }, }, 'scheduling': { 'graph': { 'R1': 'a' } }, } id_ = flow(config) schd = scheduler(id_, **opts) schd.get_events = get_events return schd return _schd async def test_startup_and_shutdown(test_scheduler, run): """Test the startup and shutdown events. * "startup" should fire every time a scheduler is started. * "shutdown" should fire every time a scheduler does a controlled exit. (i.e. excluding aborts on unexpected internal errors). """ schd = test_scheduler() async with run(schd): # NOTE: the "startup" event is only yielded with "run" not "start" pass assert schd.get_events() == {'startup', 'shutdown'} async def test_workflow_timeout(test_scheduler, run): """Test the workflow timeout. This counts down from scheduler start. """ schd = test_scheduler({'workflow timeout': 'PT0S'}) async with asyncio.timeout(4): async with run(schd): await asyncio.sleep(0.1) assert schd.get_events() == {'startup', 'workflow timeout', 'shutdown'} async def test_inactivity_timeout(test_scheduler, start): """Test the inactivity timeout. This counts down from things like state changes. """ schd = test_scheduler({ 'inactivity timeout': 'PT0S', 'abort on inactivity timeout': 'True', }) async with asyncio.timeout(4): with pytest.raises(SchedulerError): async with start(schd): await asyncio.sleep(0) await schd._main_loop() assert schd.get_events() == {'inactivity timeout', 'shutdown'} async def test_abort(test_scheduler, run): """Test abort. This should fire when uncaught internal exceptions are raised. Note, this is orthogonal to shutdown (i.e. a scheduler either shuts down or aborts, not both). Note, this is orthogonal to the "abort on " configurations. """ schd = test_scheduler() # get the main-loop to raise an exception def killer(): raise Exception(':(') schd._main_loop = killer # start the scheduler and wait for it to hit the exception with pytest.raises(Exception): async with run(schd): for _ in range(10): # allow initialisation to complete await asyncio.sleep(0.1) # the abort event should be called # note, "abort" and "shutdown" are orthogonal assert schd.get_events() == {'startup', 'abort'} async def test_stall(test_scheduler, start): """Test the stall event. This should fire when the scheduler enters the stalled state. """ schd = test_scheduler() async with start(schd): # set the failed output schd.pool.spawn_on_output( schd.pool.get_tasks()[0], 'failed' ) # set the failed status schd.pool.get_tasks()[0].state_reset('failed') # check for workflow stall condition schd.is_paused = False schd.check_workflow_stalled() assert schd.get_events() == {'shutdown', 'stall'} async def test_restart_timeout_workflow_completion( test_scheduler, scheduler, run, complete, ): """Test restart timeout for completed workflows. This should fire when a completed workflow is restarted. """ schd = test_scheduler({'restart timeout': 'PT0S'}, paused_start=False) # run to completion async with run(schd): await complete(schd) assert schd.get_events() == {'startup', 'shutdown'} # restart schd2 = scheduler(schd.workflow) schd2.get_events = schd.get_events async with run(schd2): await asyncio.sleep(0.1) assert schd2.get_events() == {'startup', 'restart timeout', 'shutdown'} async def test_restart_timeout_workflow_stop_after_cycle_point( test_scheduler, scheduler, run, complete, ): """Test restart timeout with the "stop after cycle point" config. This should fire when a completed workflow is restarted. """ schd = test_scheduler( config={ 'scheduler': { 'cycle point format': 'CCYY', 'events': {'restart timeout': 'PT0S'}, }, 'scheduling': { 'initial cycle point': '2000', 'stop after cycle point': '2000', 'graph': { 'P1Y': 'foo[-P1Y] => foo', }, }, }, paused_start=False, ) # run to completion async with run(schd): await complete(schd) assert schd.get_events() == {'startup', 'shutdown'} # restart schd2 = scheduler(schd.workflow) schd2.get_events = schd.get_events async with run(schd2): await asyncio.sleep(0.1) assert schd2.get_events() == {'startup', 'restart timeout', 'shutdown'} async def test_shutdown_handler_timeout_kill( test_scheduler, run, monkeypatch, mock_glbl_cfg, caplog ): """Test shutdown handlers get killed on the process pool timeout. Has to be done differently as the process pool is closed during shutdown. See GitHub #6639 """ def mock_run_event_handlers(self, event, reason=""): """To replace scheduler.run_event_handlers(...). Run workflow event handlers even in simulation mode. """ self.workflow_event_handler.handle(self, event, str(reason)) # Configure a long-running shutdown handler. schd = test_scheduler({ 'shutdown handlers': 'sleep 10; echo', 'mail events': '', }) # Set a low process pool timeout value. mock_glbl_cfg( 'cylc.flow.subprocpool.glbl_cfg', ''' [scheduler] process pool timeout = PT1S ''' ) async with asyncio.timeout(30): async with run(schd): # Replace a scheduler method, to call handlers in simulation mode. monkeypatch.setattr( schd, 'run_event_handlers', MethodType(mock_run_event_handlers, schd), ) await asyncio.sleep(0.1) assert ( "[('workflow-event-handler-00', 'shutdown') err] killed on " "timeout (PT1S)" ) in caplog.text TEMPLATES = [ # perfectly valid pytest.param('%(workflow)s', id='good'), # no template variable of that name pytest.param('%(no_such_variable)s', id='bad'), # missing the 's' pytest.param('%(broken_syntax)', id='ugly'), ] @pytest.mark.parametrize('template', TEMPLATES) async def test_mail_footer_template( mod_one, # use the same scheduler for each test start, mock_glbl_cfg, log_filter, capcall, template, ): """It should handle templating issues with the mail footer.""" # prevent emails from being sent mail_calls = capcall( 'cylc.flow.workflow_events.WorkflowEventHandler._send_mail' ) # configure Cylc to send an email on startup with the configured footer mock_glbl_cfg( 'cylc.flow.workflow_events.glbl_cfg', f''' [scheduler] [[mail]] footer = 'footer={template}' [[events]] mail events = startup ''', ) # start the workflow and get it to send an email async with start(mod_one) as one_log: one_log.clear() # clear previous log messages mod_one.workflow_event_handler.handle( mod_one, WorkflowEventHandler.EVENT_STARTUP, 'event message' ) # warnings should appear only when the template is invalid should_log = 'workflow' not in template # check that template issues are handled correctly assert bool(log_filter( contains='Ignoring bad mail footer template', )) == should_log assert bool(log_filter( contains=template, )) == should_log # check that the mail is sent even if there are issues with the footer assert len(mail_calls) == 1 @pytest.mark.parametrize('template', TEMPLATES) async def test_custom_event_handler_template( mod_one, # use the same scheduler for each test start, mock_glbl_cfg, log_filter, template, ): """It should handle templating issues with custom event handlers.""" # configure Cylc to send an email on startup with the configured footer mock_glbl_cfg( 'cylc.flow.workflow_events.glbl_cfg', f''' [scheduler] [[events]] startup handlers = echo "{template}" ''' ) # start the workflow and get it to send an email async with start(mod_one) as one_log: one_log.clear() # clear previous log messages mod_one.workflow_event_handler.handle( mod_one, WorkflowEventHandler.EVENT_STARTUP, 'event message' ) # warnings should appear only when the template is invalid should_log = 'workflow' not in template # check that template issues are handled correctly assert bool(log_filter( contains='bad template', )) == should_log assert bool(log_filter( contains=template, )) == should_log cylc-flow-8.6.4/tests/integration/test_id_match.py0000664000175000017500000001631215202510242022456 0ustar alastairalastair# 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 . from typing import Set, Tuple, cast, TYPE_CHECKING from cylc.flow import commands from cylc.flow.id import Tokens from cylc.flow.scheduler import Scheduler if TYPE_CHECKING: from cylc.flow.id import TaskTokens async def test_id_match(flow, scheduler, start, caplog): id_ = flow({ 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': '1', 'final cycle point': '3', 'graph': { 'P2': ''' a1 => b1 => c1 a2 => b2 => c2 b1[-P1] => b1 b2[-P1] => b2 ''', }, }, 'runtime': { 'a1, a2': {'inherit': 'A'}, 'A': {}, 'b1, b2': {'inherit': 'B'}, 'B': {}, 'c1, c2': {}, }, }) schd: Scheduler = scheduler(id_) def match(*ids: str) -> Tuple[Set[str], Set[str]]: matched, unmatched = schd.pool.id_match( {cast('TaskTokens', Tokens(id_, relative=True)) for id_ in ids}, ) return {id_.relative_id for id_ in matched}, { id_.relative_id_with_selectors for id_ in unmatched } async with start(schd): await commands.run_cmd( commands.set_prereqs_and_outputs( schd, ['1/a2'], ['1'], ['succeeded'], None ) ) await commands.run_cmd( commands.set_prereqs_and_outputs( schd, ['1/b2'], ['1'], ['failed'], None ) ) # task pool state: # * cycle 1 # * n=0 a1 waiting # * n=1 b1 waiting # * n=2 c1 waiting # * n=1 a2 succeeded # * n=0 b2 failed # * n=1 c2 waiting # * cycle 2 # * n=0 a1 waiting # * n=1 b1 waiting # * n=2 c1 waiting # * n=0 a2 waiting # * n=1 b2 waiting # * n=2 c2 waiting # check the n=0 window matches expecations before proceeding assert { itask.tokens.relative_id for itask in schd.pool.get_tasks() } == {'1/a1', '1/b2', '3/a1', '3/a2'} # test active task selection: waiting selector assert ( match('*:waiting') == match('*/root:waiting') == match('*/*:waiting') == match('*/A:waiting') == match('*/a*:waiting') == match('1/a1:waiting', '3/a1:waiting', '3/a2:waiting') == match('^/a1:waiting', '3/a1:waiting', '$/a2:waiting') == ({'1/a1', '3/a1', '3/a2'}, set()) ) # test active task selection: failed selector assert ( match('*:failed') == match('*/root:failed') == match('*/*:failed') == match('*/B:failed') == match('*/b*:failed') == match('1/b2:failed') == match('^/b2:failed') == ({'1/b2'}, set()) ) # test active task selection: failed selector assert ( match('1/b1:failed', '1/b2:failed') == match('1/B:failed', '1/b1:failed') == match('*:failed', '1/B:failed', '1/b1:failed') == ({'1/b2'}, {'1/b1:failed'}) ) # test active task selection: submit-failed selector assert match('*:submit-failed') == (set(), {'*:submit-failed'}) # test globs, cycle and family matching assert ( match('*') == match('*/*') == match('*/root') == match('*/A', '*/B', '*/c1', '*/c2') == match('*/a*', '*/b*', '*/c*') == ( { '1/a1', '1/a2', '1/b1', '1/b2', '1/c1', '1/c2', '3/a1', '3/a2', '3/b1', '3/b2', '3/c1', '3/c2', }, set(), ) ) # test globs, cycle and family matching assert ( match('1/A') == match('1/a*') == match('^/a*') == match('1/a1', '1/a2') == ({'1/a1', '1/a2'}, set()) ) assert match('1/X') == (set(), {'1/X'}) assert match('5:waiting') == (set(), {'5:waiting'}) # test invalid IDs assert match('not-a-cycle/*', '*/not_a_task', '*:not-a-state') == ( set(), {'not-a-cycle/*', '*/not_a_task', '*:not-a-state'}, ) # test invalid cycle point in combination with a selector assert match('x/y:succeeded') == (set(), {'x/y:succeeded'}) # ensure that off-sequence IDs are filtered out # NOTE: cycle 2 is not on sequence for task a1 assert match('1/a1', '2/a1', '3/a1') == ({'1/a1', '3/a1'}, {'2/a1'}) # ensure warnings are raised for off-sequence tasks if the user # explcitly specified them # NOTE: 2/a1 should result in a warning (because the user asked for # this exact combination), however, "2/a*" should not (because there # may be a combination of tasks which are or are not valid at the given # cycle(s) which match the task name pattern) caplog.clear() assert match('2/a1', '2/a*') == (set(), {'2/a1', '2/a*'}) assert caplog.messages == ['Invalid cycle point for task: a1, 2'] async def test_match_removed_task(flow, scheduler, run, complete): """It should match and operate on tasks no longer in the config.""" config = { 'scheduling': { 'graph': {'R1': 'foo & bar'}, }, } id_ = flow(config) schd = scheduler(id_, paused_start=False) def list_tasks(): return { itask.tokens.relative_id for itask in schd.pool.get_tasks() } async with run(schd): # workflow starts with "foo" and "bar" in the pool assert list_tasks() == {'1/foo', '1/bar'} # remove "bar" from the config and reload config['scheduling']['graph']['R1'] = 'x => foo' flow(config, name=id_) await commands.run_cmd(commands.reload_workflow(schd)) # both "foo" and "bar" are still in the pool ("bar" is orphaned) assert list_tasks() == {'1/foo', '1/bar'} # remove all tasks from the workflow await commands.run_cmd(commands.remove_tasks(schd, {'*'}, ['1'])) # this should match the orphaned task "bar" assert list_tasks() == set() cylc-flow-8.6.4/tests/integration/tui/0000775000175000017500000000000015202510242020073 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/tui/__init__.py0000664000175000017500000000000015202510242022172 0ustar alastairalastaircylc-flow-8.6.4/tests/integration/tui/test_logs.py0000664000175000017500000003453415202510242022461 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import asyncio from pathlib import Path from typing import TYPE_CHECKING from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.exceptions import ClientError from cylc.flow.scheduler import Scheduler from cylc.flow.task_job_logs import get_task_job_log from cylc.flow.task_state import ( TASK_STATUS_FAILED, TASK_STATUS_SUCCEEDED, ) from cylc.flow.tui.data import _get_log import pytest if TYPE_CHECKING: from cylc.flow.id import Tokens def get_job_log(tokens: 'Tokens', suffix: str) -> Path: """Return the path to a job log file. Args: tokens: Job tokens. suffix: Filename. """ return Path(get_task_job_log( tokens['workflow'], tokens['cycle'], tokens['task'], tokens['job'], suffix=suffix, )) @pytest.fixture(scope='module') def standarise_host_and_path(mod_monkeypatch): """Replace variable content in the log view. The log view displays the "Host" and "Path" of the log file. These will differer from user to user, so we mock away the difference to produce stable results. """ def _parse_log_header(contents): _header, text = contents.split('\n', 1) return 'myhost', 'mypath', text mod_monkeypatch.setattr( 'cylc.flow.tui.data._parse_log_header', _parse_log_header, ) @pytest.fixture def wait_log_loaded(monkeypatch): """Wait for Tui to successfully open a log file.""" # previous log open count before = 0 # live log open count count = 0 # wrap the Tui "_get_log" method to count the number of times it has # returned def __get_log(*args, **kwargs): nonlocal count try: ret = _get_log(*args, **kwargs) except ClientError as exc: count += 1 raise exc count += 1 return ret monkeypatch.setattr( 'cylc.flow.tui.data._get_log', __get_log, ) async def _wait_log_loaded(tries: int = 25, delay: float = 0.1): """Wait for the log file to be loaded. Args: tries: The number of (re)tries to attempt before failing. delay: The delay between retries. """ nonlocal before for _try in range(tries): if count > before: await asyncio.sleep(0) before += 1 return await asyncio.sleep(delay) raise Exception(f'Log file was not loaded within {delay * tries}s') return _wait_log_loaded @pytest.fixture(scope='module') async def workflow( mod_flow, mod_scheduler, mod_start, standarise_host_and_path ): """Test fixture providing a workflow with some log files to poke at.""" id_ = mod_flow({ 'scheduling': { 'graph': { 'R1': 'a', } }, 'runtime': { 'a': {}, } }, name='one') schd: Scheduler = mod_scheduler(id_) async with mod_start(schd): # create some log files for tests to inspect # create a scheduler log # (note the scheduler log doesn't get created in integration tests) scheduler_log = Path(schd.workflow_log_dir, '01-start-01.log') with open(scheduler_log, 'w+') as logfile: logfile.write( 'this is the scheduler log file' + '\n' + '\n'.join(f'line {x}' for x in range(2, 1000)) ) # task 1/a itask = schd.pool.get_task(IntegerPoint('1'), 'a') # mark 1/a/01 as failed job_1 = schd.tokens.duplicate(cycle='1', task='a', job='01') schd.data_store_mgr.insert_job( itask, TASK_STATUS_SUCCEEDED, {'submit_num': 1, 'platform': {'name': 'x'}} ) schd.data_store_mgr.delta_job_state(itask, TASK_STATUS_FAILED) # mark 1/a/02 as succeeded itask.submit_num = 2 job_2 = schd.tokens.duplicate(cycle='1', task='a', job='02') schd.data_store_mgr.insert_job( itask, TASK_STATUS_SUCCEEDED, {'submit_num': 2, 'platform': {'name': 'x'}} ) schd.data_store_mgr.delta_job_state(itask, TASK_STATUS_SUCCEEDED) schd.data_store_mgr.delta_task_state(itask) # mark 1/a as succeeded itask.state_reset(TASK_STATUS_SUCCEEDED) schd.data_store_mgr.delta_task_state(itask) # 1/a/01 - job.out job_1_out = get_job_log(job_1, 'job.out') job_1_out.parent.mkdir(parents=True) with open(job_1_out, 'w+') as log: log.write(f'job: {job_1.relative_id}\nthis is a job log\n') # 1/a/02 - job.out job_2_out = get_job_log(job_2, 'job.out') job_2_out.parent.mkdir(parents=True) with open(job_2_out, 'w+') as log: log.write(f'job: {job_2.relative_id}\nthis is a job log\n') # 1/a/02 - job.err job_2_err = get_job_log(job_2, 'job.err') with open(job_2_err, 'w+') as log: log.write(f'job: {job_2.relative_id}\nthis is a job error\n') # 1/a/NN -> 1/a/02 (job_2_out.parent.parent / 'NN').symlink_to( (job_2_out.parent.parent / '02'), target_is_directory=True, ) # populate the data store await schd.update_data_structure() yield schd async def test_scheduler_logs( workflow, mod_rakiura, wait_log_loaded, ): """Test viewing the scheduler log files.""" with mod_rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the workflow in Tui rk.user_input('down', 'right') rk.wait_until_loaded(workflow.tokens.id) # open the log view for the workflow rk.user_input('enter') rk.user_input('down', 'down', 'enter') # wait for the default log file to load await wait_log_loaded() rk.compare_screenshot( 'scheduler-log-file', 'the scheduler log file should be open', ) # jump to the bottom of the file rk.user_input('end') rk.compare_screenshot( 'scheduler-log-file-bottom', 'we should be looking at the bottom of the file' ) # jump back to the top of the file rk.user_input('home') rk.compare_screenshot( 'scheduler-log-file', 'we should be looking at the bottom of the file' ) # open the list of log files rk.user_input('enter') rk.compare_screenshot( 'log-file-selection', 'the list of available log files should be displayed' ) # select the processed workflow configuration file rk.user_input('down', 'enter') # wait for the file to load await wait_log_loaded() rk.compare_screenshot( 'workflow-configuration-file', 'the workflow configuration file should be open' ) async def test_task_logs( workflow, mod_rakiura, wait_log_loaded, ): """Test viewing task log files. I.E. Test viewing job log files by opening the log view on a task. """ with mod_rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the workflow in Tui rk.user_input('down', 'right') rk.wait_until_loaded(workflow.tokens.id) # open the context menu for the task 1/a rk.user_input('down', 'down', 'enter') # open the log view for the task 1/a rk.user_input('down', 'down', 'down', 'enter') # wait for the default log file to load await wait_log_loaded() rk.compare_screenshot( 'latest-job.out', 'the job.out file for the second job should be open', ) rk.user_input('enter') rk.user_input('enter') # wait for the job.err file to load await wait_log_loaded() rk.compare_screenshot( 'latest-job.err', 'the job.out file for the second job should be open', ) async def test_job_logs( workflow, mod_rakiura, wait_log_loaded, ): """Test viewing the job log files. I.E. Test viewing job log files by opening the log view on a job. """ with mod_rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the workflow in Tui rk.user_input('down', 'right') rk.wait_until_loaded(workflow.tokens.id) # open the context menu for the job 1/a/02 rk.user_input('down', 'down', 'right', 'down', 'enter') # open the log view for the job 1/a/02 rk.user_input('down', 'down', 'down', 'enter') # wait for the default log file to load await wait_log_loaded() rk.compare_screenshot( '02-job.out', 'the job.out file for the *second* job should be open', ) # close log view rk.user_input('q') # open the log view for the job 1/a/01 rk.user_input('down', 'enter') rk.user_input('down', 'down', 'down', 'enter') # wait for the default log file to load await wait_log_loaded() rk.compare_screenshot( '01-job.out', 'the job.out file for the *first* job should be open', ) async def test_errors( workflow, mod_rakiura, wait_log_loaded, monkeypatch, ): """Test error handing of cat-log commands.""" # make it look like cat-log commands are failing def cli_cmd_fail(*args, **kwargs): raise ClientError('Something went wrong :(') monkeypatch.setattr( 'cylc.flow.tui.data.cli_cmd', cli_cmd_fail, ) with mod_rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the log view on scheduler rk.user_input('down', 'enter', 'down', 'down', 'enter') # it will fail to open await wait_log_loaded() rk.compare_screenshot( 'open-error', 'the error message should be displayed in the log view header', ) # open the file selector rk.user_input('enter') # it will fail to list avialable log files rk.compare_screenshot( 'list-error', 'the error message should be displayed in a pop up', ) async def test_external_editor( workflow, mod_rakiura, wait_log_loaded, monkeypatch, capsys, ): """Test the "open in external editor" functionality. This test covers the relevant code about as well as we can in an integration test. * The integration tests write HTML fragments to a file rather ANSI to a terminal. * Suspending / restoring the Tui session involves shell interaction that we cannot simulate here. * We're also not testing subprocesses in this integration test. But this test passing tells us that the relevant code does indeed run without falling over in a heap, so it will detect interface breakages and the like which is useful. """ fake_popen_instances = [] class FakePopen: def __init__(self, cmd, *args, raises=None, **kwargs): fake_popen_instances.append(self) self.cmd = cmd self.args = args self.kwargs = kwargs self.raises = raises def wait(self): if self.raises: raise self.raises() return 0 # mock out subprocess.Popen monkeypatch.setattr( 'cylc.flow.tui.overlay.Popen', FakePopen, ) # mock out time.sleep monkeypatch.setattr( 'cylc.flow.tui.overlay.sleep', lambda x: None, ) with mod_rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the log view on scheduler rk.user_input('down', 'enter', 'down', 'down', 'enter') # it will fail to open await wait_log_loaded() assert len(fake_popen_instances) == 0 assert capsys.readouterr()[1] == '' # select the open in "$EDITOR" option rk.user_input('down', 'left', 'left', 'left') # make a note of what the screen looks like rk.compare_screenshot( 'before-opening-editor', 'The open in $EDITOR option should be selected', ) # launch the external tool rk.user_input('enter') # the subprocess should be started and a message logged to stderr assert len(fake_popen_instances) == 1 assert 'launching external tool' in capsys.readouterr()[1].lower() # once the subprocess exist, the Tui session should be restored # exactly as it was before rk.compare_screenshot( 'before-opening-editor', 'The Tui session should restore exactly as it was before', ) # get the subprocess to fail in a nasty way from functools import partial monkeypatch.setattr( 'cylc.flow.tui.overlay.Popen', partial(FakePopen, raises=OSError), ) # launch the external tool rk.user_input('enter') # the subprocess should be started, the error should be logged # to stderr assert len(fake_popen_instances) == 2 assert 'error running' in capsys.readouterr()[1].lower() # once the subprocess exist, the Tui session should be restored # exactly as it was before rk.compare_screenshot( 'before-opening-editor', 'The Tui session should restore exactly as it was before', ) cylc-flow-8.6.4/tests/integration/tui/test_app.py0000664000175000017500000005421015202510242022266 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from random import random import pytest import urwid from cylc.flow import __version__ from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.id import TaskTokens from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED from cylc.flow.task_state import ( TASK_STATUS_EXPIRED, TASK_STATUS_FAILED, TASK_STATUS_RUNNING, TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, ) from cylc.flow.tui.util import MODIFIER_ATTR_MAPPING from cylc.flow.workflow_files import get_contact_file_path from cylc.flow.workflow_status import StopMode def _add_xtrig(schd, itask, sig, label, satisfied=False): """Register a new xtrigger. This is enough to get the data store to reflect the xtrig, but don't expect it to be functional! """ ( schd.data_store_mgr.xtrigger_tasks .setdefault(sig, set()) .add((itask.tokens.id, label)) ) schd.data_store_mgr.delta_xtrigger(sig, satisfied) def set_task_state(schd, task_states): """Force tasks into the desired states. Task states should be of the format (cycle, task, state, is_held). """ for cycle, task, state, modifiers in task_states: itask = schd.pool.get_task(cycle, task) if not itask: itask = schd.pool.spawn_task(task, cycle, {1}) if modifiers.pop('is_retry', None): _add_xtrig( schd, itask, 'wall_clock(offset=0)', f'_cylc_retry_{random()}' ) if modifiers.pop('is_wallclock', None): _add_xtrig(schd, itask, 'wall_clock(offset=1)', 'my_clock') if modifiers.pop('is_xtriggered', None): _add_xtrig(schd, itask, 'my_custom()', 'my_custom') itask.state_reset(state, **modifiers) schd.data_store_mgr.delta_task_state(itask) schd.data_store_mgr.increment_graph_window( itask.tokens, cycle, ) async def test_tui_basics(rakiura): """Test basic Tui interaction with no workflows.""" with rakiura(size='80,40') as rk: # the app should open rk.compare_screenshot('test-rakiura', 'the app should have loaded') # "h" should bring up the onscreen help rk.user_input('h') rk.compare_screenshot( 'test-rakiura-help', 'the help screen should be visible' ) # "q" should close the popup rk.user_input('q') rk.compare_screenshot( 'test-rakiura', 'the help screen should have closed', ) # "enter" should bring up the context menu rk.user_input('enter') rk.compare_screenshot( 'test-rakiura-enter', 'the context menu should have opened', ) # "enter" again should close it via the "cancel" button rk.user_input('enter') rk.compare_screenshot( 'test-rakiura', 'the context menu should have closed', ) # "ctrl d" should exit Tui with pytest.raises(urwid.ExitMainLoop): rk.user_input('ctrl d') # "q" should exit Tui with pytest.raises(urwid.ExitMainLoop): rk.user_input('q') async def test_subscribe_unsubscribe( one_conf, flow, scheduler, start, rakiura ): """Test a simple workflow with one task.""" id_ = flow(one_conf, name='one') schd = scheduler(id_) async with start(schd): await schd.update_data_structure() with rakiura(size='80,15') as rk: rk.compare_screenshot( 'unsubscribed', 'the workflow should be collapsed' ' (no subscription no state totals)', ) # expand the workflow to subscribe to it rk.user_input('down', 'right') rk.wait_until_loaded() rk.compare_screenshot( 'subscribed', 'the workflow should be expanded', ) # collapse the workflow to unsubscribe from it rk.user_input('left', 'up') rk.force_update() rk.compare_screenshot( 'unsubscribed', 'the workflow should be collapsed' ' (no subscription no state totals)', ) async def test_workflow_states(one_conf, flow, scheduler, start, rakiura): """Test viewing multiple workflows in different states.""" # one => stopping id_1 = flow(one_conf, name='one') schd_1 = scheduler(id_1) # two => paused id_2 = flow(one_conf, name='two') schd_2 = scheduler(id_2) # tre => stopped flow(one_conf, name='tre') async with start(schd_1): schd_1.stop_mode = StopMode.AUTO # make it look like we're stopping await schd_1.update_data_structure() async with start(schd_2): await schd_2.update_data_structure() with rakiura(size='80,15') as rk: rk.compare_screenshot( 'unfiltered', 'All workflows should be visible (one, two, tree)', ) # filter for active workflows (i.e. paused, running, stopping) rk.user_input('p') rk.compare_screenshot( 'filter-active', 'Only active workflows should be visible (one, two)' ) # invert the filter so we are filtering for stopped workflows rk.user_input('W', 'enter', 'q') rk.compare_screenshot( 'filter-stopped', 'Only stopped workflow should be visible (tre)' ) # filter in paused workflows rk.user_input('W', 'down', 'enter', 'q') rk.force_update() rk.compare_screenshot( 'filter-stopped-or-paused', 'Only stopped or paused workflows should be visible' ' (two, tre)', ) # reset the state filters rk.user_input('W', 'down', 'down', 'enter', 'down', 'enter') # scroll to the id filter text box rk.user_input('down', 'down', 'down', 'down') # scroll to the end of the ID rk.user_input(*['right'] * ( len(schd_1.tokens['workflow'].rsplit('/', 1)[0]) + 1) ) # type the letter "t" # (this should filter for workflows starting with "t") rk.user_input('t') rk.force_update() # this is required for the tests rk.user_input('page up', 'q') # close the dialogue rk.compare_screenshot( 'filter-starts-with-t', 'Only workflows starting with the letter "t" should be' ' visible (two, tre)', ) async def test_task_states(flow, scheduler, start, rakiura): id_ = flow({ 'scheduler': { 'allow implicit tasks': 'true', }, 'scheduling': { 'initial cycle point': '1', 'cycling mode': 'integer', 'runahead limit': 'P1', 'graph': { 'P1': ''' a & b & c ''' }, }, 'runtime': { 'X': {}, 'Y': {}, 'Y1': {'inherit': 'Y'}, 'a': {'inherit': 'X'}, 'b': {'inherit': 'Y'}, 'c': {'inherit': 'Y1'}, }, }, name='test_task_states') schd = scheduler(id_) async with start(schd): set_task_state( schd, [ ( IntegerPoint('1'), 'a', TASK_STATUS_SUCCEEDED, {'is_held': False} ), ( IntegerPoint('1'), 'b', TASK_STATUS_FAILED, {'is_held': False} ), ( IntegerPoint('1'), 'c', TASK_STATUS_EXPIRED, {'is_held': False} ), ( IntegerPoint('2'), 'a', TASK_STATUS_SUBMITTED, {'is_held': False} ), ( IntegerPoint('2'), 'b', TASK_STATUS_RUNNING, {'is_held': True} ), ( IntegerPoint('2'), 'c', TASK_STATUS_SUBMIT_FAILED, {'is_held': True} ), ] ) await schd.update_data_structure() with rakiura(schd.tokens.id, size='80,30') as rk: rk.compare_screenshot( 'unfiltered', 'all tasks should be displayed' ' (i.e. 1/*, 2/* and 3/* should be displayed)', ) # filter OUT waiting tasks rk.user_input('T', 'down', 'enter', 'q') # select waiting rk.compare_screenshot( 'filter-not-waiting', 'waiting tasks should be filtered out' ' (i.e. 1/* and 2/* should be displayed)', ) # filter OUT waiting & expired tasks rk.user_input('T', 'down', 'down', 'enter', 'q') # select expired rk.compare_screenshot( 'filter-not-waiting-or-expired', 'waiting & expired tasks should be filtered out' ' (i.e. only 1/a, 1/b and 2/* should be displayed)', ) # filter FOR waiting & expired tasks rk.user_input('T', 'enter', 'q') # select invert rk.compare_screenshot( 'filter-waiting-or-expired', 'only waiting and expired tasks should be displayed' ' (i.e. only 1/c and 3/* should be displayed)', ) # filter FOR submitted tasks (using shortcuts) rk.user_input('R', 's') # reset filters and apply submitted filter rk.compare_screenshot( 'filter-submitted', 'only submitted tasks should be displayed' ' (i.e. only 2/a should be displayed)', ) async def test_task_modifiers(flow, scheduler, start, rakiura): """It should display task modifiers and text summaries of them.""" id_ = flow({ 'scheduling': { 'graph': { 'R1': '\n'.join( modifier for modifier, _ in MODIFIER_ATTR_MAPPING.values() ) + '\nall' }, }, }, name='test_task_modifiers') schd = scheduler(id_) async with start(schd): set_task_state( schd, [ *[ ( IntegerPoint('1'), modifier, TASK_STATUS_WAITING, # NOTE: set is_queued=False as the default because # parentless tasks are autoqueued on startup {**{'is_queued': False}, modifier: True}, ) for modifier, _ in MODIFIER_ATTR_MAPPING.values() ], ( IntegerPoint('1'), 'all', TASK_STATUS_WAITING, { modifier: True for modifier, _ in MODIFIER_ATTR_MAPPING.values() }, ) ] ) await schd.update_data_structure() with rakiura(schd.tokens.id, size='80,30') as rk: # test modifier icon rendering rk.compare_screenshot( 'task-modifiers', 'all tasks should be displayed along with their modifiers' ) # test modifier text summary rk.user_input('down', 'down', 'down', 'enter') # select task "all" rk.compare_screenshot( 'task-context-menu', 'all modifiers should be listed along with the task state' ' (i.e. the text "held, runahead, queued, retry scheduled,' ' wallclock, xtriggered") should be present' ) async def test_navigation(flow, scheduler, start, rakiura): """Test navigating with the arrow keys.""" id_ = flow({ 'scheduling': { 'graph': { 'R1': 'A & B1 & B2', } }, 'runtime': { 'A': {}, 'B': {}, 'B1': {'inherit': 'B'}, 'B2': {'inherit': 'B'}, 'a1': {'inherit': 'A'}, 'a2': {'inherit': 'A'}, 'b11': {'inherit': 'B1'}, 'b12': {'inherit': 'B1'}, 'b21': {'inherit': 'B2'}, 'b22': {'inherit': 'B2'}, } }, name='one') schd = scheduler(id_) async with start(schd): await schd.update_data_structure() with rakiura(size='80,30') as rk: # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') rk.compare_screenshot( 'on-load', 'the workflow should be collapsed when Tui is loaded', ) # pressing "right" should connect to the workflow # and expand it once the data arrives rk.user_input('down', 'right') rk.wait_until_loaded(schd.tokens.id) rk.compare_screenshot( 'workflow-expanded', 'the workflow should be expanded', ) # pressing "left" should collapse the node rk.user_input('down', 'down', 'left') rk.compare_screenshot( 'family-A-collapsed', 'the family "1/A" should be collapsed', ) # the "page up" and "page down" buttons should navigate to the top # and bottom of the screen rk.user_input('page down') rk.compare_screenshot( 'cursor-at-bottom-of-screen', 'the cursor should be at the bottom of the screen', ) async def test_auto_expansion(flow, scheduler, start, rakiura): """It should automatically expand cycles and top-level families. When a workflow is expanded, Tui should auto expand cycles and top-level families. Any new cycles and top-level families should be auto-expanded when added. """ id_ = flow({ 'scheduling': { 'runahead limit': 'P1', 'initial cycle point': '1', 'cycling mode': 'integer', 'graph': { 'P1': 'b[-P1] => a => b' }, }, 'runtime': { 'A': {}, 'a': {'inherit': 'A'}, 'b': {}, }, }, name='one') schd = scheduler(id_) with rakiura(size='80,20') as rk: async with start(schd): await schd.update_data_structure() # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # open the workflow rk.force_update() rk.user_input('down', 'right') rk.wait_until_loaded(schd.tokens.id) rk.compare_screenshot( 'on-load', 'cycle "1" and top-level family "1/A" should be expanded', ) for task in ('a', 'b'): schd.pool.set_prereqs_and_outputs( items={TaskTokens('1', task)}, outputs=[TASK_OUTPUT_SUCCEEDED], prereqs=[], flow=[] ) await schd.update_data_structure() schd.update_data_store() rk.compare_screenshot( 'later-time', 'cycle "2" and top-level family "2/A" should be expanded', ) async def test_restart_reconnect(one_conf, flow, scheduler, start, rakiura): """It should handle workflow shutdown and restart. The Cylc client can raise exceptions e.g. WorkflowStopped. Any text written to stdout/err will mess with Tui. The purpose of this test is to ensure Tui can handle shutdown / restart without any errors occuring and any spurious text appearing on the screen. """ with rakiura(size='80,20') as rk: schd = scheduler(flow(one_conf, name='one')) # 1- start the workflow async with start(schd): await schd.update_data_structure() # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # expand the workflow (subscribes to updates from it) rk.force_update() rk.user_input('down', 'right') # wait for workflow to appear (expanded) rk.wait_until_loaded(schd.tokens.id) rk.compare_screenshot( '1-workflow-running', 'the workflow should appear in tui and be expanded', ) # 2 - stop the worlflow rk.compare_screenshot( '2-workflow-stopped', 'the stopped workflow should be collapsed with a message saying' ' workflow stopped', ) # 3- restart the workflow schd = scheduler(flow(one_conf, name='one')) async with start(schd): await schd.update_data_structure() rk.wait_until_loaded(schd.tokens.id) rk.compare_screenshot( '3-workflow-restarted', 'the restarted workflow should be expanded', ) async def test_states(flow, scheduler, start, rakiura): """It should dim no-flow tasks and display state summary in context menus. """ id_ = flow( { 'scheduling': { 'graph': { 'R1': 'a & b & c', }, }, }, name='one', ) from cylc.flow.scheduler import Scheduler schd: Scheduler = scheduler(id_) async with start(schd): a = schd.pool.get_task(IntegerPoint('1'), 'a') b = schd.pool.get_task(IntegerPoint('1'), 'b') c = schd.pool.get_task(IntegerPoint('1'), 'c') assert a and b and c # set task flow numbers assert a.flow_nums == {1} b.flow_nums = {1, 2} c.flow_nums = {} # set task state a.state_reset(TASK_STATUS_SUCCEEDED, is_held=True) b.state_reset(TASK_STATUS_WAITING, is_queued=True) c.state_reset(TASK_STATUS_WAITING, is_queued=False, is_runahead=True) # update data store for task in (a, b, c): schd.data_store_mgr.delta_task_state(task) schd.data_store_mgr.delta_task_flow_nums(task) await schd.update_data_structure() with rakiura(schd.tokens.id, size='80,15') as rk: rk.compare_screenshot( 'on-load', 'the workflow should be expanded,' ' no-flow task 1/c should be dimmed' ) # workflow node rk.user_input('down', 'enter') rk.compare_screenshot( 'workflow-context--paused', 'the workflow should show as paused in the context menu', ) # cycle: 1 rk.user_input('q', 'down', 'enter') rk.compare_screenshot( 'cycle-context--waiting', 'the cycle should show as waiting in the context menu' ) # task:a rk.user_input('q', 'down', 'enter') rk.compare_screenshot( 'task-context--succeeded+held', 'the task should show as succeeded+held in the context menu,' ' no flow numbers should be displayed', ) # task:b rk.user_input('q', 'down', 'enter') rk.compare_screenshot( 'task-context--waiting+queued', 'the task should show as waiting+queued in the context menu,' ' the flow numbers 1,2 should be displayed', ) # task:c rk.user_input('q', 'down', 'enter') rk.compare_screenshot( 'task-context--waiting+runahead', 'the task should show as waiting+runahead in the context menu,' ' the task should be marked as flows=None' ) async def test_incompat_scheduler_version( one_conf, flow, scheduler, start, rakiura ): """It should handle workflows it cannot subscribe to.""" schd = scheduler(flow(one_conf, name='one')) async with start(schd): # make it look like the scheduler is reallllyyyy old contact = get_contact_file_path(schd.workflow) with open(contact, 'r') as contact_file: contact_lines = contact_file.read().replace(__version__, '6.11.4') with open(contact, 'w') as contact_file: contact_file.write(''.join(contact_lines)) with rakiura(size='80,20') as rk: await schd.update_data_structure() # wait for the workflow to appear (collapsed) rk.wait_until_loaded('#spring') # expand the workflow (subscribes to updates from it) rk.force_update() rk.user_input('down', 'right') # it should be marked as incompatible rk.compare_screenshot( 'on-load', 'the workflow should be marked as incompatible', ) cylc-flow-8.6.4/tests/integration/tui/test_mutations.py0000664000175000017500000002121015202510242023523 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import asyncio import pytest from cylc.flow.exceptions import ClientError async def process_command(schd, tries=10, interval=0.1): """Wait for command(s) to be queued and run. Waits for at least one command to be queued and for all queued commands to be run. """ # wait for the command to be queued for _try in range(tries): await asyncio.sleep(interval) if not schd.command_queue.empty(): break else: raise Exception(f'No command was queued after {tries * interval}s') # run the command await schd.process_command_queue() # push out updates await schd.update_data_structure() # make sure it ran assert schd.command_queue.empty(), 'command queue has not emptied' async def test_online_mutation( one_conf, flow, scheduler, start, rakiura, monkeypatch, log_filter, ): """Test a simple workflow with one task.""" id_ = flow(one_conf, name='one') schd = scheduler(id_) with rakiura(size='80,15') as rk: async with start(schd): await schd.update_data_structure() assert schd.command_queue.empty() # open the workflow rk.force_update() rk.user_input('down', 'right') rk.wait_until_loaded(schd.tokens.id) # focus on a task rk.user_input('down', 'right', 'down', 'right') rk.compare_screenshot( # take a screenshot to ensure we have focused on the task # successfully 'task-selected', 'the cursor should be on the task 1/foo', ) # focus on the hold mutation for a task rk.user_input('enter', 'down') rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'hold-mutation-selected', 'the cursor should be on the "hold" mutation', ) # run the hold mutation rk.user_input('enter') # the mutation should be in the scheduler's command_queue await asyncio.sleep(0) assert log_filter(contains="hold(tasks=['1/one'])") # close the dialogue and re-run the hold mutation rk.user_input('q', 'q', 'enter') rk.compare_screenshot( 'command-failed-workflow-stopped', 'an error should be visible explaining that the operation' ' cannot be performed on a stopped workflow', # NOTE: don't update so Tui still thinks the workflow is running force_update=False, ) # force mutations to raise ClientError def _get_client(*args, **kwargs): raise ClientError('mock error') monkeypatch.setattr( 'cylc.flow.tui.data.get_client', _get_client, ) # close the dialogue and re-run the hold mutation rk.user_input('q', 'q', 'enter') rk.compare_screenshot( 'command-failed-client-error', 'an error should be visible explaining that the operation' ' failed due to a client error', # NOTE: don't update so Tui still thinks the workflow is running force_update=False, ) @pytest.fixture def standardise_cli_cmds(monkeypatch): """This remove the variable bit of the workflow ID from CLI commands. The workflow ID changes from run to run. In order to make screenshots stable, this """ from cylc.flow.tui.data import extract_context def _extract_context(selection): context = extract_context(selection) if 'workflow' in context: context['workflow'] = [ workflow.rsplit('/', 1)[-1] for workflow in context.get('workflow', []) ] return context monkeypatch.setattr( 'cylc.flow.tui.data.extract_context', _extract_context, ) @pytest.fixture def capture_commands(monkeypatch): ret = [] returncode = [0] class _Popen: def __init__(self, *args, **kwargs): ret.append(args) def communicate(self): return 'mock-stdout', 'mock-stderr' @property def returncode(self): return returncode[0] monkeypatch.setattr( 'cylc.flow.tui.data.Popen', _Popen, ) return ret, returncode async def test_offline_mutation( one_conf, flow, rakiura, capture_commands, standardise_cli_cmds, ): flow(one_conf, name='one') commands, returncode = capture_commands with rakiura(size='80,15') as rk: # run the stop-all mutation rk.wait_until_loaded('root') rk.user_input('enter', 'down') rk.compare_screenshot( # take a screenshot to ensure we have focused on the task # successfully 'stop-all-mutation-selected', 'the stop-all mutation should be selected', ) rk.user_input('enter') # the command "cylc stop '*'" should have been run assert commands == [(['cylc', 'stop', '*'],)] commands.clear() # run the clean command on the workflow rk.user_input('down', 'enter', 'down') rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'clean-mutation-selected', 'the clean mutation should be selected', ) rk.user_input('enter') # the command "cylc clean " should have been run assert commands == [(['cylc', 'clean', '--yes', 'one'],)] commands.clear() # make commands fail returncode[:] = [1] rk.user_input('enter', 'down') rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'clean-mutation-selected', 'the clean mutation should be selected', ) rk.user_input('enter') assert commands == [(['cylc', 'clean', '--yes', 'one'],)] rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'clean-command-error', 'there should be a box displaying the error containing the stderr' ' returned by the command', ) async def test_set_mutation( flow, scheduler, start, rakiura, ): id_ = flow({ 'scheduling': { 'graph': { 'R1': 'a => z' }, }, }, name='one') schd = scheduler(id_) async with start(schd): await schd.update_data_structure() with rakiura(schd.tokens.id, size='80,15') as rk: # open the context menu on 1/a rk.user_input('down', 'down', 'down', 'enter') rk.force_update() # select the "set" mutation rk.user_input(*(('down',) * 7)) # 7th command down rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'set-command-selected', 'The command menu should be open for the task 1/a with the' ' set command selected.' ) # issue the "set" mutation rk.user_input('enter') # wait for the command to be received and run it await process_command(schd) # close the error dialogue # NOTE: This hides an asyncio error that does not occur outside of # the tests rk.user_input('q', 'q', 'q') rk.compare_screenshot( # take a screenshot to ensure we have focused on the mutation # successfully 'task-state-updated', '1/a should now show as succeeded,' ' there should be no associated job.' ) cylc-flow-8.6.4/tests/integration/tui/conftest.py0000664000175000017500000002360515202510242022300 0ustar alastairalastairfrom contextlib import contextmanager from difflib import unified_diff import os from pathlib import Path import re from time import sleep from secrets import token_hex import pytest from urwid.display import html_fragment from cylc.flow.id import Tokens from cylc.flow.tui.app import TuiApp from cylc.flow.tui.overlay import _get_display_id SCREENSHOT_DIR = Path(__file__).parent / 'screenshots' def configure_screenshot(v_term_size): """Configure Urwid HTML screenshots.""" screen = html_fragment.HtmlGenerator() screen.set_terminal_properties(256) screen.register_palette(TuiApp.palette) html_fragment.screenshot_init( [tuple(map(int, v_term_size.split(',')))], [] ) return screen, html_fragment def format_test_failure(expected, got, description): """Return HTML to represent a screenshot test failure. Args: expected: HTML fragment for the expected screenshot. got: HTML fragment for the test screenshot. description: Test description. """ diff = '\n'.join(unified_diff( expected.splitlines(), got.splitlines(), fromfile='expected', tofile='got', )) return f'''

{description}

Expected Got
{expected} {got}

Diff:

{diff}
''' class RakiuraSession: """Convenience class for accessing Rakiura functionality.""" def __init__(self, app, html_fragment, test_dir, test_name): self.app = app self.html_fragment = html_fragment self.test_dir = test_dir self.test_name = test_name def user_input(self, *keys): """Simulate a user pressing keys. Each "key" is a keyboard button e.g. "x" or "enter". If you provide more than one key, each one will be pressed, one after another. You can combine keys in a single string, e.g. "ctrl d". """ return self.app.loop.process_input(keys) def compare_screenshot( self, name, description, retries=3, delay=0.1, force_update=True, ): """Take a screenshot and compare it to one taken previously. To update the screenshot, set the environment variable "CYLC_UPDATE_SCREENSHOTS" to "true". Note, if the comparison fails, "force_update" is called and the test is retried. Arguments: name: The name to use for the screenshot, this is used in the filename for the generated HTML fragment. description: A description of the test to be used on test failure. retries: The maximum number of retries for this test before failing. delay: The delay between retries. This helps overcome timing issues with data provision. Raises: Exception: If the screenshot does not match the reference. """ filename = SCREENSHOT_DIR / f'{self.test_name}.{name}.html' exc = None for _try in range(retries): # load the expected result expected = '' if filename.exists(): with open(filename, 'r') as expected_file: expected = expected_file.read() # update to pick up latest data if force_update: self.force_update() # force urwid to draw the screen # (the main loop isn't runing so this doesn't happen automatically) self.app.loop.draw_screen() # take a screenshot screenshot = self.html_fragment.screenshot_collect()[-1] try: if expected != screenshot: # screenshot does not match # => write an html file with the visual diff out = self.test_dir / filename.name with open(out, 'w+') as out_file: out_file.write( format_test_failure( expected, screenshot, description, ) ) raise Exception( 'Screenshot differs:' '\n* Set "CYLC_UPDATE_SCREENSHOTS=true" to update' f'\n* To debug see: file:////{out}' ) break except Exception as exc_: exc = exc_ # wait a while to allow the updater to do its job sleep(delay) else: if os.environ.get('CYLC_UPDATE_SCREENSHOTS', '').lower() == 'true': with open(filename, 'w+') as expected_file: expected_file.write(screenshot) else: raise exc def force_update(self): """Run Tui's update method. This is done automatically by compare_screenshot but you may want to call it in a test, e.g. before pressing navigation keys. With Rakiura, the Tui event loop is not running so the data is never refreshed. You do NOT need to call this method for key presses, but you do need to call this if the data has changed (e.g. if you've changed a task state) OR if you've changed any filters (because filters are handled by the update code). """ # flush any prior updates self.app.get_update() # wait for the next update while not self.app.update(): pass def wait_until_loaded(self, *ids, retries=20): """Wait until the given ID appears in the Tui tree, then expand them. Useful for waiting whilst Tui loads a workflow. Note, this is a blocking wait with no timeout! """ exc = None try: ids = self.app.wait_until_loaded(*ids, retries=retries) except Exception as _exc: exc = _exc if ids: msg = ( 'Requested nodes did not appear in Tui after' f' {retries} retries: ' + ', '.join(ids) ) if exc: msg += f'\n{exc}' self.compare_screenshot(f'fail-{token_hex(4)}', msg, 1) @pytest.fixture def rakiura(test_dir, request, monkeypatch): """Visual regression test framework for Urwid apps. Like Cypress but for Tui so named after a NZ island with lots of Tuis. When called this yields a RakiuraSession object loaded with test utilities. All tests have default retries to avoid flaky tests. Similar to the "start" fixture, which starts a Scheduler without running the main loop, rakiura starts Tui without running the main loop. Arguments: workflow_id: The "WORKFLOW" argument of the "cylc tui" command line. size: The virtual terminal size for screenshots as a comma separated string e.g. "80,50" for 80 cols wide by 50 rows tall. Returns: A RakiuraSession context manager which provides useful utilities for testing. """ return _rakiura(test_dir, request, monkeypatch) @pytest.fixture def mod_rakiura(test_dir, request, monkeypatch): """Same as rakiura but configured to view module-scoped workflows. Note: This is *not* a module-scoped fixture (no need, creating Tui sessions is not especially slow), it is configured to display module-scoped "scheduler" fixtures (which may be more expensive to create/destroy). """ return _rakiura(test_dir.parent, request, monkeypatch) def _rakiura(test_dir, request, monkeypatch): # make the workflow and scan update intervals match (more reliable) # and speed things up a little whilst we're at it monkeypatch.setattr( 'cylc.flow.tui.updater.Updater.BASE_UPDATE_INTERVAL', 0.1, ) monkeypatch.setattr( 'cylc.flow.tui.updater.Updater.BASE_SCAN_INTERVAL', 0.1, ) # the user name and the prefix of workflow IDs are both variable # so we patch the render functions to make test output stable def get_display_id(id_): tokens = Tokens(id_) return _get_display_id( tokens.duplicate( user='cylc', workflow=tokens.get('workflow', '').rsplit('/', 1)[-1], ).id ) monkeypatch.setattr('cylc.flow.tui.util.ME', 'cylc') monkeypatch.setattr('cylc.flow.tui.app.CYLC_VERSION', '1.2.3') monkeypatch.setattr( 'cylc.flow.tui.util._display_workflow_id', lambda data: data['name'].rsplit('/', 1)[-1] ) monkeypatch.setattr( 'cylc.flow.tui.overlay._get_display_id', get_display_id, ) # standardise environment for tests monkeypatch.setenv('EDITOR', 'nvim') monkeypatch.setenv('GEDITOR', 'gvim -f') monkeypatch.setenv('PAGER', 'less') # filter Tui so that only workflows created within our test show up id_base = str(test_dir.relative_to(Path("~/cylc-run").expanduser())) workflow_filter = re.escape(id_base) + r'/.*' @contextmanager def _rakiura(workflow_id=None, size='80,50'): screen, html_fragment = configure_screenshot(size) app = TuiApp(screen=screen) with app.main( workflow_id, id_filter=workflow_filter, interactive=False, ): yield RakiuraSession( app, html_fragment, test_dir, request.function.__name__, ) return _rakiura cylc-flow-8.6.4/tests/integration/tui/test_show.py0000664000175000017500000000443215202510242022467 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.exceptions import ClientError async def test_show(flow, scheduler, start, rakiura, monkeypatch): """Test "cylc show" support in Tui.""" id_ = flow({ 'scheduling': { 'graph': { 'R1': 'foo' }, }, 'runtime': { 'foo': { 'meta': { 'title': 'Foo', 'description': 'The first metasyntactic variable.' }, }, }, }, name='one') schd = scheduler(id_) async with start(schd): await schd.update_data_structure() with rakiura(size='80,40') as rk: rk.user_input('down', 'right') rk.wait_until_loaded(schd.tokens.id) # select a task rk.user_input('down', 'down', 'enter') # select the "show" context option rk.user_input(*(['down'] * 8), 'enter') rk.compare_screenshot( 'success', 'the show output should be displayed', ) # make it look like "cylc show" failed def cli_cmd_fail(*args, **kwargs): raise ClientError(':(') monkeypatch.setattr( 'cylc.flow.tui.data.cli_cmd', cli_cmd_fail, ) # select the "show" context option rk.user_input('q', 'enter', *(['down'] * 8), 'enter') rk.compare_screenshot( 'fail', 'the error should be displayed', ) cylc-flow-8.6.4/tests/integration/tui/screenshots/0000775000175000017500000000000015202510242022433 5ustar alastairalastaircylc-flow-8.6.4/tests/integration/tui/screenshots/test_show.success.html0000664000175000017500000003372315202510242027017 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
           ̿○ foo                                                                
                                                                                
                                                                                
                                                                                
                                                                                
               ────────────────────────────────────────────────               
                 title: Foo                                                   
                 description: The first metasyntactic                         
                 variable.                                                    
                 URL: (not given)                                             
                 state: waiting (queued,run mode=Simulation)                  
                 prerequisites: (None)                                        
                 outputs: ('⨯': not completed)                                
                   ⨯ 1/foo expired                                            
                   ⨯ 1/foo submitted                                          
                   ⨯ 1/foo submit-failed                                      
                   ⨯ 1/foo started                                            
                   ⨯ 1/foo succeeded                                          
                   ⨯ 1/foo failed                                             
                 output completion: incomplete                                
                   ⨯ ┆  succeeded                                             
                                                                              
                                                                              
                q to close                                                    
               ────────────────────────────────────────────────               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_scheduler_logs.log-file-selection.html0000664000175000017500000003553715202510242033057 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  this is the scheduler log file                                              
  line 2                                                                      
  line 3                                                                      
  line 4                                                                      
  line 5                                                                      
  line 6                                                                      
  line 7             ──────────────────────────────────────                 
  line 8               Select File                                          
  line 9                                                                    
  line 10              < config/01-start-01.cylc        >                   
  line 11              < config/flow-processed.cylc     >                   
  line 12              < scheduler/01-start-01.log      >                   
  line 13                                                                   
  line 14             q to close                                            
  line 15            ──────────────────────────────────────                 
  line 16                                                                     
  line 17                                                                     
  line 18                                                                     
  line 19                                                                     
  line 20                                                                     
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_external_editor.before-opening-editor.html0000664000175000017500000002703715202510242033743 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  this is the scheduler log file                                              
  line 2                                                                      
  line 3                                                                      
  line 4                                                                      
  line 5                                                                      
  line 6                                                                      
  line 7                                                                      
  line 8                                                                      
  line 9                                                                      
  line 10                                                                     
  line 11                                                                     
  line 12                                                                     
  line 13                                                                     
  line 14                                                                     
  line 15                                                                     
  line 16                                                                     
  line 17                                                                     
  line 18                                                                     
  line 19                                                                     
  line 20                                                                     
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_workflow_states.filter-stopped-or-paused.html0000664000175000017500000001047715202510242034463 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + tre - stopped                                                              
   + two - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_logs.latest-job.out.html0000664000175000017500000002703715202510242031230 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  job: 1/a/02                                                                 
  this is a job log                                                           
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_workflow_states.filter-stopped.html0000664000175000017500000000775215202510242032570 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + tre - stopped                                                              
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_set_mutation.set-command-selected.html0000664000175000017500000002447015202510242033076 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                                                                              
- ~cylc          Action                                                       
   - one - paus  < (cancel)                                 >                 
      - ̿○ 1                                                                   
           ̿○ a   < hold                                     >                 
            z   < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                 < remove                                   >                 
                 < set                                      >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_auto_expansion.on-load.html0000664000175000017500000001332315202510242030746 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
         - ̿○ A                                                                  
              ̿○ a                                                               
            b                                                                  
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_errors.list-error.html0000664000175000017500000004763615202510242030015 0ustar alastairalastair
────────────────────────────────────────────────────────────────────────────
  Error: Somethi  Error                                                     
                                                                            
  < Select File   Something went wrong :(                                >  
  Open in:  < nv                                                            
  (Configure app                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
                                                                            
 q to close      q to close                                                 
────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_offline_mutation.clean-mutation-selected.html0000664000175000017500000002146415202510242034436 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: ~cylc/one                                                
- ~cylc          stopped                                                      
   + one - stop                                                               
                 Action                                                       
                 < (cancel)                                 >                 
                                                                              
                 < clean                                    >                 
                 < log                                      >                 
                 < play                                     >                 
                 < reinstall-reload                         >                 
                                                                              
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_restart_reconnect.3-workflow-restarted.html0000664000175000017500000001226115202510242034112 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
           ̿○ one                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_show.fail.html0000664000175000017500000005625515202510242026267 0ustar alastairalastair
Cylc Tui 1.2.3   wor────────────────────────────────────────────────          
                      Error                                                   
- ~cylc                                                                       
   - one - paused     :(                                                      
      - ̿○ 1                                                                   
           ̿○ foo                                                              
                                                                              
                                                                              
                                                                              
                                                                              
               ────                                                          
                 id                                                          
                 wa                                                          
                                                                             
                 Ac                                                          
                 <                                                           
                                                                             
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                                                                             
                                                                             
                q t                                                          
               ────                                                          
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
quit: q  help: h  co q to close                                     ome End   
filter tasks: T f s ────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_job_logs.02-job.out.html0000664000175000017500000002703715202510242027765 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  job: 1/a/02                                                                 
  this is a job log                                                           
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.filter-submitted.html0000664000175000017500000001705115202510242032173 0ustar alastairalastair
Cylc Tui 1.2.3   tasks filtered (T - edit, R - reset)   workflows filtered (W - 
edit, E - reset)                                                                
                                                                                
- ~cylc                                                                         
   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
      - ̎⊘ 2                                                                     
         - ̿⊙ X                                                                  
              ̿⊙ a                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_errors.open-error.html0000664000175000017500000002703715202510242027774 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Error: Something went wrong :(                                              
                                                                              
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_tui_basics.test-rakiura-enter.html0000664000175000017500000003314615202510242032241 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
               ────────────────────────────────────────────────               
                 id: ~cylc/root                                               
                                                                              
                                                                              
                 Action                                                       
                 < (cancel)                                 >                 
                                                                              
                 < stop-all                                 >                 
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                q to close                                                    
               ────────────────────────────────────────────────               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootcylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.filter-not-waiting-or-expired.htmlcylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.filter-not-waiting-or-expired.htm0000664000175000017500000002362515202510242034337 0ustar alastairalastair
Cylc Tui 1.2.3   tasks filtered (T - edit, R - reset)   workflows filtered (W - 
edit, E - reset)                                                                
                                                                                
- ~cylc                                                                         
   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
      - ̿⊗ 1                                                                     
         - ̿● X                                                                  
              ̿● a                                                               
         - ̿⊗ Y                                                                  
              ̿⊗ b                                                               
      - ̎⊘ 2                                                                     
         - ̿⊙ X                                                                  
              ̿⊙ a                                                               
         - ̎⊘ Y                                                                  
            - ̎⊘ Y1                                                              
                 ̎⊘ c                                                            
              ̎⊙ b                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_restart_reconnect.1-workflow-running.html0000664000175000017500000001226115202510242033573 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
           ̿○ one                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_offline_mutation.clean-command-error.html0000664000175000017500000002413715202510242033555 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────────          
                 id  Error                                                   
- ~cylc          st                                                          
   + one - stop      Error in command cylc clean --yes one                   
                 Ac  mock-stderr                                             
                 <                                                           
                                                                             
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                                                                             
                                                                             
quit: q  help:  q t q to close                                     ome End   
filter tasks: T────────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_navigation.on-load.html0000664000175000017500000001372315202510242030055 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + one - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_tui_basics.test-rakiura.html0000664000175000017500000001570415202510242031126 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.hold-mutation-selected.html0000664000175000017500000002300015202510242034130 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: 1/one                                                    
- ~cylc          waiting (queued)                                             
   - one - paus                                                               
      - ̿○ 1      Action                                                       
           ̿○ on  < (cancel)                                 >                 
                                                                              
                 < hold                                     >                 
                 < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_workflow_states.unfiltered.html0000664000175000017500000001122415202510242031755 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + one - stopping                                                             
   + tre - stopped                                                              
   + two - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_subscribe_unsubscribe.unsubscribed.html0000664000175000017500000000775215202510242033447 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + one - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.filter-not-waiting.html0000664000175000017500000002466515202510242032444 0ustar alastairalastair
Cylc Tui 1.2.3   tasks filtered (T - edit, R - reset)   workflows filtered (W - 
edit, E - reset)                                                                
                                                                                
- ~cylc                                                                         
   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
      - ̿⊗ 1                                                                     
         - ̿● X                                                                  
              ̿● a                                                               
         - ̿⊗ Y                                                                  
            -  Y1                                                              
                  c                                                            
              ̿⊗ b                                                               
      - ̎⊘ 2                                                                     
         - ̿⊙ X                                                                  
              ̿⊙ a                                                               
         - ̎⊘ Y                                                                  
            - ̎⊘ Y1                                                              
                 ̎⊘ c                                                            
              ̎⊙ b                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.task-context--succeeded+held.html0000664000175000017500000002430615202510242033222 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: 1/a                                                      
- ~cylc          succeeded (held, queued)                                     
   - one - paus                                                               
      - ̎○ 1      Action                                                       
           ̎● a   < (cancel)                                 >                 
           ̿○ b                                                                
           ̀○ c   < hold                                     >                 
                 < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_subscribe_unsubscribe.subscribed.html0000664000175000017500000001101615202510242033070 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
           ̿○ one                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.filter-waiting-or-expired.html0000664000175000017500000002311115202510242033703 0ustar alastairalastair
Cylc Tui 1.2.3   tasks filtered (T - edit, R - reset)   workflows filtered (W - 
edit, E - reset)                                                                
                                                                                
- ~cylc                                                                         
   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
      - ̿⊗ 1                                                                     
         - ̿⊗ Y                                                                  
            -  Y1                                                              
                  c                                                            
      - ̀○ 3                                                                     
         - ̀○ X                                                                  
              ̀○ a                                                               
         - ̀○ Y                                                                  
            - ̀○ Y1                                                              
                 ̀○ c                                                            
              ̀○ b                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.cycle-context--waiting.html0000664000175000017500000002430615202510242032165 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: 1                                                        
- ~cylc          waiting (held, runahead, queued)                             
   - one - paus                                                               
      - ̎○ 1      Action                                                       
           ̎● a   < (cancel)                                 >                 
           ̿○ b                                                                
           ̀○ c   < hold                                     >                 
                 < kill                                     >                 
                 < poll                                     >                 
                 < release                                  >                 
                 < trigger                                  >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.workflow-context--paused.html0000664000175000017500000002430615202510242032557 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: ~cylc/one                                                
- ~cylc          paused                                                       
   - one - paus                                                               
      - ̎○ 1      Action                                                       
           ̎● a   < (cancel)                                 >                 
           ̿○ b                                                                
           ̀○ c   < clean                                    >                 
                 < log                                      >                 
                 < pause                                    >                 
                 < play                                     >                 
                 < reinstall-reload                         >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootcylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.command-failed-client-error.htmlcylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.command-failed-client-error.h0000664000175000017500000002520615202510242034316 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────────          
                 id  Error                                                   
- ~cylc          wa                                                          
   - one - paus      Error connecting to workflow: mock error                
      - ̿○ 1      Ac                                                          
           ̿○ on  <                                                           
                                                                             
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                                                                             
quit: q  help:  q t q to close                                     ome End   
filter tasks: T────────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_tui_basics.test-rakiura-help.html0000664000175000017500000004520615202510242032054 0ustar alastairalastair
Cylc Tui 1──────────────────────────────────────────────────────────          
                                                                              
- ~cylc                        _        _         _                           
                              | |      | |       (_)                          
                     ___ _   _| | ___  | |_ _   _ _                           
                    / __| | | | |/ __| | __| | | | |                          
                   | (__| |_| | | (__  | |_| |_| | |                          
                    \___|\__, |_|\___|  \__|\__,_|_|                          
                          __/ |                                               
                         |___/                                                
                                                                              
                      ( scroll using arrow keys )                             
                                                                              
                                                                              
                                                                              
                                       _,@@@@@@.                              
                                     <=@@@, `@@@@@.                           
                                        `-@@@@@@@@@@@'                        
                                           :@@@@@@@@@@.                       
                                          (.@@@@@@@@@@@                       
                                         ( '@@@@@@@@@@@@.                     
                                        ;.@@@@@@@@@@@@@@@                     
                                      '@@@@@@@@@@@@@@@@@@,                    
                                    ,@@@@@@@@@@@@@@@@@@@@'                    
                                  :.@@@@@@@@@@@@@@@@@@@@@.                    
                                .@@@@@@@@@@@@@@@@@@@@@@@@.                    
                              '@@@@@@@@@@@@@@@@@@@@@@@@@.                     
                            ;@@@@@@@@@@@@@@@@@@@@@@@@@@@                      
                           .@@@@@@@@@@@@@@@@@@@@@@@@@@.                       
                          .@@@@@@@@@@@@@@@@@@@@@@@@@@,                        
                         .@@@@@@@@@@@@@@@@@@@@@@@@@'                          
                        .@@@@@@@@@@@@@@@@@@@@@@@@'     ,                      
                      :@@@@@@@@@@@@@@@@@@@@@..''';,,,;::-                     
                     '@@@@@@@@@@@@@@@@@@@.        `.   `                      
                    .@@@@@@.: ,.@@@@@@@.            `                         
                  :@@@@@@@,         ;.@,                                      
                 '@@@@@@.              `@'                                    
                                                                              
quit: q  h q to close                                               ome End   
filter tas──────────────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_scheduler_logs.scheduler-log-file.html0000664000175000017500000002703715202510242033044 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  this is the scheduler log file                                              
  line 2                                                                      
  line 3                                                                      
  line 4                                                                      
  line 5                                                                      
  line 6                                                                      
  line 7                                                                      
  line 8                                                                      
  line 9                                                                      
  line 10                                                                     
  line 11                                                                     
  line 12                                                                     
  line 13                                                                     
  line 14                                                                     
  line 15                                                                     
  line 16                                                                     
  line 17                                                                     
  line 18                                                                     
  line 19                                                                     
  line 20                                                                     
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.command-failed.html0000664000175000017500000002465615202510242032440 0ustar alastairalastair
Cylc Tui   work────────────────────────────────────────────────────          
                 id  Error                                                   
- ~cylc                                                                      
   - one - paus  Ac  Cannot peform command hold on a stopped                 
      - ̿○ 1      <   workflow                                                
           ̿○ on                                                              
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                                                                             
                                                                             
quit: q  help:  q t q to close                                     ome End   
filter tasks: T────────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.task-context--waiting+queued.html0000664000175000017500000002430615202510242033314 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: 1/b                                                      
- ~cylc          waiting (queued), flows=1,2                                  
   - one - paus                                                               
      - ̎○ 1      Action                                                       
           ̎● a   < (cancel)                                 >                 
           ̿○ b                                                                
           ̀○ c   < hold                                     >                 
                 < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_navigation.workflow-expanded.html0000664000175000017500000002171715202510242032166 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
         - ̿○ A                                                                  
              ̿○ a1                                                              
              ̿○ a2                                                              
         - ̿○ B                                                                  
            - ̿○ B1                                                              
                 ̿○ b11                                                          
                 ̿○ b12                                                          
            - ̿○ B2                                                              
                 ̿○ b21                                                          
                 ̿○ b22                                                          
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_logs.latest-job.err.html0000664000175000017500000002703715202510242031211 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  job: 1/a/02                                                                 
  this is a job error                                                         
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_set_mutation.task-state-updated.html0000664000175000017500000001152415202510242032601 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
            a                                                                  
           ̿○ z                                                                  
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_workflow_states.filter-active.html0000664000175000017500000001047715202510242032363 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + one - stopping                                                             
   + two - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootcylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.command-failed-workflow-stopped.htmlcylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.command-failed-workflow-stopp0000664000175000017500000002520615202510242034500 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────────          
                 id  Error                                                   
- ~cylc          wa                                                          
   - one - paus      Cannot peform command hold on a stopped                 
      - ̿○ 1      Ac  workflow                                                
           ̿○ on  <                                                           
                                                                             
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                 <                                                           
                                                                             
quit: q  help:  q t q to close                                     ome End   
filter tasks: T────────────────────────────────────────────────────          
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_modifiers.task-modifiers.html0000664000175000017500000002065215202510242032310 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - test_task_modifiers - paused                                               
      - ̎○ 1                                                                     
           ̎○ all                                                                
           ̎○ is_held                                                            
           ̿○ is_queued                                                          
           ⃗○ is_retry                                                           
           ̀○ is_runahead                                                        
           ፝○ is_wallclock                                                       
           ᷉○ is_xtriggered                                                      
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_auto_expansion.later-time.html0000664000175000017500000001436315202510242031465 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      -  1                                                                     
            b                                                                  
      - ̿○ 2                                                                     
         - ̿○ A                                                                  
              ̿○ a                                                               
            b                                                                  
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.on-load.html0000664000175000017500000001241415202510242027215 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused 1■                                                            
      - ̎○ 1                                                                     
           ̎● a                                                                  
           ̿○ b                                                                  
           ̀○ c                                                                  
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootcylc-flow-8.6.4/tests/integration/tui/screenshots/test_offline_mutation.stop-all-mutation-selected.htmlcylc-flow-8.6.4/tests/integration/tui/screenshots/test_offline_mutation.stop-all-mutation-selected.h0000664000175000017500000002023215202510242034362 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: ~cylc/root                                               
- ~cylc                                                                       
   + one - stop                                                               
                 Action                                                       
                 < (cancel)                                 >                 
                                                                              
                 < stop-all                                 >                 
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_scheduler_logs.scheduler-log-file-bottom.html0000664000175000017500000002454315202510242034345 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  line 974                                                                    
  line 975                                                                    
  line 976                                                                    
  line 977                                                                    
  line 978                                                                    
  line 979                                                                    
  line 980                                                                    
  line 981                                                                    
  line 982                                                                    
  line 983                                                                    
  line 984                                                                    
  line 985                                                                    
  line 986                                                                    
  line 987                                                                    
  line 988                                                                    
  line 989                                                                    
  line 990                                                                    
  line 991                                                                    
  line 992                                                                    
  line 993                                                                    
  line 994                                                                    
  line 995                                                                    
  line 996                                                                    
  line 997                                                                    
  line 998                                                                    
  line 999                                                                    
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_restart_reconnect.2-workflow-stopped.html0000664000175000017500000001145715202510242033600 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - stopped                                                              
        Workflow is not running                                                 
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_states.unfiltered.html0000664000175000017500000002771515202510242031061 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
      - ̿⊗ 1                                                                     
         - ̿● X                                                                  
              ̿● a                                                               
         - ̿⊗ Y                                                                  
            -  Y1                                                              
                  c                                                            
              ̿⊗ b                                                               
      - ̎⊘ 2                                                                     
         - ̿⊙ X                                                                  
              ̿⊙ a                                                               
         - ̎⊘ Y                                                                  
            - ̎⊘ Y1                                                              
                 ̎⊘ c                                                            
              ̎⊙ b                                                               
      - ̀○ 3                                                                     
         - ̀○ X                                                                  
              ̀○ a                                                               
         - ̀○ Y                                                                  
            - ̀○ Y1                                                              
                 ̀○ c                                                            
              ̀○ b                                                               
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_online_mutation.task-selected.html0000664000175000017500000001101615202510242032312 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
           ̿○ one                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootcylc-flow-8.6.4/tests/integration/tui/screenshots/test_scheduler_logs.workflow-configuration-file.htmlcylc-flow-8.6.4/tests/integration/tui/screenshots/test_scheduler_logs.workflow-configuration-file.ht0000664000175000017500000002703715202510242034475 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  [scheduling]                                                                
      [[graph]]                                                               
          R1 = a                                                              
  [runtime]                                                                   
      [[a]]                                                                   
      [[root]]                                                                
          [[[simulation]]]                                                    
              default run length = PT0S                                       
  [scheduler]                                                                 
      allow implicit tasks = True                                             
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_workflow_states.filter-starts-with-t.html0000664000175000017500000001047715202510242033642 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   + tre - stopped                                                              
   + two - paused                                                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_navigation.cursor-at-bottom-of-screen.html0000664000175000017500000002047715202510242033630 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
         + ̿○ A                                                                  
         - ̿○ B                                                                  
            - ̿○ B1                                                              
                 ̿○ b11                                                          
                 ̿○ b12                                                          
            - ̿○ B2                                                              
                 ̿○ b21                                                          
                 ̿○ b22                                                          
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_incompat_scheduler_version.on-load.html0000664000175000017500000001145715202510242033335 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
        Error - Scheduler version 6.11.4 is not supported                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_task_modifiers.task-context-menu.html0000664000175000017500000004070215202510242032753 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - test_task_modifiers - paused                                               
      - ̎○ 1                                                                     
           ̎○ al────────────────────────────────────────────────               
           ̎○ is  id: 1/all                                                    
           ̿○ is  waiting (held, runahead, queued, retry                       
           ⃗○ is  scheduled, wallclock, xtriggered)                            
           ̀○ is                                                               
           ፝○ is  Action                                                       
           ᷉○ is  < (cancel)                                 >                 
                                                                              
                 < hold                                     >                 
                 < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                 < remove                                   >                 
                 < set                                      >                 
                 < show                                     >                 
                 < trigger                                  >                 
                                                                              
                q to close                                                    
               ────────────────────────────────────────────────               
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_navigation.family-A-collapsed.html0000664000175000017500000002047715202510242032133 0ustar alastairalastair
Cylc Tui 1.2.3   workflows filtered (W - edit, E - reset)                       
                                                                                
- ~cylc                                                                         
   - one - paused                                                               
      - ̿○ 1                                                                     
         + ̿○ A                                                                  
         - ̿○ B                                                                  
            - ̿○ B1                                                              
                 ̿○ b11                                                          
                 ̿○ b12                                                          
            - ̿○ B2                                                              
                 ̿○ b21                                                          
                 ̿○ b22                                                          
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
filter tasks: T f s r R  filter workflows: W E p                                
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_job_logs.01-job.out.html0000664000175000017500000002703715202510242027764 0ustar alastairalastair
──────────────────────────────────────────────────────────────────────────────
  Host: myhost                                                                
  Path: mypath                                                                
  < Select File                                                            >  
  Open in:  < nvim >< gvim -f >< less >                                       
  (Configure apps to open logs via $EDITOR, $GEDITOR, $PAGER)                 
                                                                              
  job: 1/a/01                                                                 
  this is a job log                                                           
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
                                                                              
 q to close                                                                   
──────────────────────────────────────────────────────────────────────────────
cylc-flow-8.6.4/tests/integration/tui/screenshots/test_states.task-context--waiting+runahead.html0000664000175000017500000002430615202510242033613 0ustar alastairalastair
Cylc Tui 1.2.3 ────────────────────────────────────────────────               
                 id: 1/c                                                      
- ~cylc          waiting (runahead), flows=None                               
   - one - paus                                                               
      - ̎○ 1      Action                                                       
           ̎● a   < (cancel)                                 >                 
           ̿○ b                                                                
           ̀○ c   < hold                                     >                 
                 < kill                                     >                 
                 < log                                      >                 
                 < poll                                     >                 
                 < release                                  >                 
                                                                              
quit: q  help:  q to close                                     ↥ ↧ Home End   
filter tasks: T────────────────────────────────────────────────               
cylc-flow-8.6.4/tests/integration/tui/test_updater.py0000664000175000017500000001754615202510242023165 0ustar alastairalastair#!/usr/bin/env python3 # 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 . import asyncio from copy import deepcopy from pathlib import Path from queue import Queue import re from time import time import pytest from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.id import Tokens from cylc.flow.tui.updater import ( Updater, get_default_filters, ) from cylc.flow.workflow_status import WorkflowStatus @pytest.fixture def updater(monkeypatch, test_dir): """Return an updater ready for testing.""" # patch the update intervals so that everything runs for every update monkeypatch.setattr( 'cylc.flow.tui.updater.Updater.BASE_UPDATE_INTERVAL', 0, ) monkeypatch.setattr( 'cylc.flow.tui.updater.Updater.BASE_SCAN_INTERVAL', 0, ) # create the updater updater = Updater() # swap multiprocessing.Queue for queue.Queue # (this means queued operations are instant making tests more stable) updater.update_queue = Queue() updater._command_queue = Queue() # set up the filters # (these filter for the workflows created in this test only) filters = get_default_filters() id_base = str(test_dir.relative_to(Path("~/cylc-run").expanduser())) filters['workflows']['id'] = f'^{re.escape(id_base)}/.*' updater._update_filters(filters) return updater def get_child_tokens(root_node, types, relative=False): """Return all ID of the specified types contained within the provided tree. Args: root_node: The Tui tree you want to look for IDs in. types: The Tui types (e.g. 'workflow' or 'task') you want to extract. relative: If True, the relative IDs will be returned. """ ret = set() stack = [root_node] while stack: node = stack.pop() stack.extend(node['children']) if node['type_'] in types: tokens = Tokens(node['id_']) if relative: ret.add(tokens.relative_id) else: ret.add(tokens.id) return ret async def test_subscribe(one_conf, flow, scheduler, run, updater): """It should subscribe and unsubscribe from workflows.""" id_ = flow(one_conf) schd = scheduler(id_) async with run(schd): # run the updater and the test async with asyncio.timeout(10): # wait for the first update root_node = await updater._update() # there should be a root root_node assert root_node['id_'] == 'root' # a single root_node representing the workflow assert root_node['children'][0]['id_'] == schd.tokens.id # and a "spring" root_node used to active the subscription # mechanism assert root_node['children'][0]['children'][0]['id_'] == '#spring' # subscribe to the workflow updater.subscribe(schd.tokens.id) root_node = await updater._update() # check the workflow contains one cycle with one task in it workflow_node = root_node['children'][0] assert len(workflow_node['children']) == 1 cycle_node = workflow_node['children'][0] assert Tokens(cycle_node['id_']).relative_id == '1' # cycle ID assert len(cycle_node['children']) == 1 task_node = cycle_node['children'][0] assert Tokens(task_node['id_']).relative_id == '1/one' # task ID # unsubscribe from the workflow updater.unsubscribe(schd.tokens.id) root_node = await updater._update() # the workflow should be replaced by a "spring" node again assert root_node['children'][0]['children'][0]['id_'] == '#spring' async def test_filters(one_conf, flow, scheduler, run, updater): """It should filter workflow and task states. Note: The workflow ID filter is not explicitly tested here, but it is indirectly tested, otherwise other workflows would show up in the updater results. """ one = scheduler(flow({ 'scheduler': { 'allow implicit tasks': 'True', }, 'scheduling': { 'graph': { 'R1': 'a & b & c', } }, 'runtime': { # TODO: remove this runtime section in # https://github.com/cylc/cylc-flow/pull/5721 'root': { 'simulation': { 'default run length': 'PT1M', }, }, }, }, name='one'), paused_start=True) two = scheduler(flow(one_conf, name='two')) tre = scheduler(flow(one_conf, name='tre')) # start workflow "one" async with run(one): # mark "1/a" as running and "1/b" as succeeded one_a = one.pool.get_task(IntegerPoint('1'), 'a') one_a.summary['started_time'] = time() one_a.state_reset('running') one.data_store_mgr.delta_task_state(one_a) one.pool.get_task(IntegerPoint('1'), 'b').state_reset('succeeded') # start workflow "two" async with run(two): # run the updater and the test filters = deepcopy(updater.filters) root_node = await updater._update() assert {child['id_'] for child in root_node['children']} == { one.tokens.id, two.tokens.id, tre.tokens.id, } # filter out paused workflows filters = deepcopy(filters) filters['workflows'][WorkflowStatus.STOPPED.value] = True filters['workflows'][WorkflowStatus.PAUSED.value] = False updater.update_filters(filters) # "one" and "two" should now be filtered out root_node = await updater._update() assert {child['id_'] for child in root_node['children']} == { tre.tokens.id, } # filter out stopped workflows filters = deepcopy(filters) filters['workflows'][WorkflowStatus.STOPPED.value] = False filters['workflows'][WorkflowStatus.PAUSED.value] = True updater.update_filters(filters) # "tre" should now be filtered out root_node = await updater._update() assert {child['id_'] for child in root_node['children']} == { one.tokens.id, two.tokens.id, } # subscribe to "one" updater._subscribe(one.tokens.id) root_node = await updater._update() assert get_child_tokens( root_node, types={'task'}, relative=True ) == { '1/a', '1/b', '1/c', } # filter out running tasks # TODO: see https://github.com/cylc/cylc-flow/issues/5716 # filters = deepcopy(filters) # filters['tasks'][TASK_STATUS_RUNNING] = False # updater.update_filters(filters) # root_node = await updater._update() # assert get_child_tokens( # root_node, # types={'task'}, # relative=True # ) == { # '1/b', # '1/c', # } cylc-flow-8.6.4/tests/integration/test_flow_assignment.py0000664000175000017500000001423415202510242024106 0ustar alastairalastair# 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 . """Test for flow-assignment in triggered/set tasks.""" import functools import logging import time from typing import Callable import pytest from cylc.flow.flow_mgr import ( FLOW_NEW, FLOW_NONE, ) from cylc.flow.scheduler import Scheduler from cylc.flow.commands import ( run_cmd, force_trigger_tasks, set_prereqs_and_outputs, ) async def test_trigger_no_flows(one, start): """Test triggering a task with no flows present. It should get the flow numbers of the most recent active tasks. """ async with start(one): # Remove the task (flow 1) --> pool empty task = one.pool.get_tasks()[0] one.pool.remove(task) assert len(one.pool.get_tasks()) == 0 # Trigger the task, with new flow nums. time.sleep(2) # The flows need different timestamps! await run_cmd( force_trigger_tasks(one, [task.identity], flow=['5', '9'])) assert len(one.pool.get_tasks()) == 1 # Ensure the new flow is in the db. one.pool.workflow_db_mgr.process_queued_ops() # Remove the task --> pool empty task = one.pool.get_tasks()[0] one.pool.remove(task) assert len(one.pool.get_tasks()) == 0 # Trigger the task; it should get flow nums 5, 9 await run_cmd(force_trigger_tasks(one, [task.identity], [])) assert len(one.pool.get_tasks()) == 1 task = one.pool.get_tasks()[0] assert task.flow_nums == {5, 9} async def test_cli_to_flow_nums(one: Scheduler, start): """Test the flow manager cli_to_flow_nums method.""" async with start(one): # flow 1 is already present task = one.pool.get_tasks()[0] assert one.pool.flow_mgr.cli_to_flow_nums([FLOW_NEW]) == {2} one.pool.merge_flows(task, {2}) # now we have flows {1, 2}: assert one.pool.flow_mgr.cli_to_flow_nums([FLOW_NONE]) == set() assert one.pool.flow_mgr.cli_to_flow_nums([]) == set() assert one.pool.flow_mgr.cli_to_flow_nums([FLOW_NEW]) == {3} assert one.pool.flow_mgr.cli_to_flow_nums(['4', '5']) == {4, 5} @pytest.mark.parametrize('command', ['trigger', 'set']) async def test_flow_assignment( flow, scheduler, start, command: str, log_filter: Callable ): """Test flow assignment when triggering/setting tasks. Active tasks: By default keep existing flows, else merge with requested flows. Inactive tasks: By default assign active flows; else assign requested flows. """ conf = { 'scheduler': { 'allow implicit tasks': 'True' }, 'scheduling': { 'graph': { 'R1': "foo & bar => a & b & c & d & e" } }, 'runtime': { 'foo': { 'outputs': {'x': 'x'} } }, } id_ = flow(conf) schd: Scheduler = scheduler(id_, run_mode='simulation', paused_start=True) async with start(schd): if command == 'set': do_command = functools.partial( set_prereqs_and_outputs, schd, outputs=['x'], prerequisites=[], ) else: do_command = functools.partial( force_trigger_tasks, schd, ) active_1, active_2 = schd.pool.get_tasks() # foo, bar in any order schd.pool.merge_flows( active_2, schd.pool.flow_mgr.cli_to_flow_nums([FLOW_NEW])) assert active_1.flow_nums == {1} assert active_2.flow_nums == {1, 2} # -----(1. Test active tasks)----- await run_cmd(do_command([active_1.identity], flow=[])) if command == "set": # By default active tasks merge existing and active flows. assert active_1.flow_nums == {1, 2} else: # By default active tasks keep existing flows. assert active_1.flow_nums == {1} # Else merge existing and requested flows. await run_cmd(do_command([active_1.identity], flow=['2'])) assert active_1.flow_nums == {1, 2} # (no-flow is ignored for active tasks) await run_cmd(do_command([active_1.identity], flow=[FLOW_NONE])) assert active_1.flow_nums == {1, 2} assert log_filter( contains=("Already active - ignoring"), level=logging.WARNING ) await run_cmd(do_command([active_1.identity], flow=[FLOW_NEW])) assert active_1.flow_nums == {1, 2, 3} # -----(2. Test inactive tasks)----- if command == 'set': do_command = functools.partial( set_prereqs_and_outputs, schd, outputs=[], prerequisites=['all'], ) # By default inactive tasks get all active flows. await run_cmd(do_command(['1/a'], flow=[])) assert schd.pool._get_task_by_id('1/a').flow_nums == {1, 2, 3} # Else assign requested flows. # Run as no-flow: await run_cmd(do_command(['1/b'], flow=[FLOW_NONE])) assert schd.pool._get_task_by_id('1/b').flow_nums == set() await run_cmd(do_command(['1/c'], flow=[FLOW_NEW])) assert schd.pool._get_task_by_id('1/c').flow_nums == {4} await run_cmd(do_command(['1/d'], flow=[])) assert schd.pool._get_task_by_id('1/d').flow_nums == {1, 2, 3, 4} await run_cmd(do_command(['1/e'], flow=["7"])) assert schd.pool._get_task_by_id('1/e').flow_nums == {7} cylc-flow-8.6.4/tests/integration/test_template_variables_cli.py0000664000175000017500000000273615202510242025405 0ustar alastairalastair#!/usr/bin/env python3 # 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 . from cylc.flow.scripts.view import get_option_parser, _main as view from cylc.flow.option_parsers import Options async def test_list_tvars(tmp_path, capsys): """View shows that lists of comma separated args are converted into strings: """ (tmp_path / 'flow.cylc').write_text( '#!jinja2\n' '{% for i in FOO %}\n' '# {{i}} is string: {{i is string}}\n' '{% endfor %}\n' ) options = Options(get_option_parser())() options.jinja2 = True options.templatevars_lists = ['FOO="w,x",y,z'] await view(options, str(tmp_path)) result = capsys.readouterr().out.split('\n') for string in ['w,x', 'y', 'z']: assert f'# {string} is string: True' in result cylc-flow-8.6.4/tests/integration/test_scheduler.py0000664000175000017500000003460615202510242022672 0ustar alastairalastair# 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 . import asyncio import logging from pathlib import Path import pytest import re from signal import SIGHUP, SIGINT, SIGTERM from typing import Any, Callable from cylc.flow import commands from cylc.flow.exceptions import CylcError from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.scheduler import Scheduler, SchedulerStop from cylc.flow.task_state import ( TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING, TASK_STATUS_FAILED ) from cylc.flow.workflow_status import AutoRestartMode, StopMode Fixture = Any TRACEBACK_MSG = "Traceback (most recent call last):" async def test_is_paused_after_stop( one_conf: Fixture, flow: Fixture, scheduler: Fixture, run: Fixture, db_select: Fixture): """Test the paused status is unset on normal shutdown.""" id_: str = flow(one_conf) schd: 'Scheduler' = scheduler(id_, paused_start=True) # Run async with run(schd): assert not schd.is_restart assert schd.is_paused # Stopped assert ('is_paused', '1') not in db_select(schd, False, 'workflow_params') # Restart schd = scheduler(id_, paused_start=None) async with run(schd): assert schd.is_restart assert not schd.is_paused async def test_is_paused_after_crash( one_conf: Fixture, flow: Fixture, scheduler: Fixture, run: Fixture, db_select: Fixture): """Test the paused status is not unset for an interrupted workflow.""" id_: str = flow(one_conf) schd: 'Scheduler' = scheduler(id_, paused_start=True) def ctrl_c(): raise asyncio.CancelledError("Mock keyboard interrupt") # Patch this part of the main loop _schd_workflow_shutdown = schd.workflow_shutdown setattr(schd, 'workflow_shutdown', ctrl_c) # Run with pytest.raises(asyncio.CancelledError): async with run(schd): assert not schd.is_restart assert schd.is_paused # Stopped assert ('is_paused', '1') in db_select(schd, False, 'workflow_params') # Reset patched method setattr(schd, 'workflow_shutdown', _schd_workflow_shutdown) # Restart schd = scheduler(id_, paused_start=None) async with run(schd): assert schd.is_restart assert schd.is_paused async def test_shutdown_CylcError_log(one: Scheduler, run: Callable): """Test that if a CylcError occurs during shutdown, it is logged in one line.""" schd = one # patch the shutdown to raise an error async def mock_shutdown(*a, **k): raise CylcError("Error on shutdown") setattr(schd, '_shutdown', mock_shutdown) # run the workflow log: pytest.LogCaptureFixture with pytest.raises(CylcError) as exc: async with run(schd) as log: pass # check the log records after attempted shutdown assert str(exc.value) == "Error on shutdown" last_record = log.records[-1] assert last_record.message == "CylcError: Error on shutdown" assert last_record.levelno == logging.ERROR # shut down the scheduler properly await Scheduler._shutdown(schd, SchedulerStop('because I said so')) async def test_shutdown_general_exception_log(one: Scheduler, run: Callable): """Test that if a non-CylcError occurs during shutdown, it is logged with traceback (but not excessive).""" schd = one # patch the shutdown to raise an error async def mock_shutdown(*a, **k): raise ValueError("Error on shutdown") setattr(schd, '_shutdown', mock_shutdown) # run the workflow log: pytest.LogCaptureFixture with pytest.raises(ValueError) as exc: async with run(schd) as log: pass # check the log records after attempted shutdown assert str(exc.value) == "Error on shutdown" last_record = log.records[-1] assert last_record.message == "Error on shutdown" assert last_record.levelno == logging.ERROR assert last_record.exc_text is not None assert last_record.exc_text.startswith(TRACEBACK_MSG) assert ("During handling of the above exception, " "another exception occurred") not in last_record.exc_text # shut down the scheduler properly await Scheduler._shutdown(schd, SchedulerStop('because I said so')) async def test_holding_tasks_whilst_scheduler_paused( capture_submission, flow, one_conf, start, scheduler, ): """It should hold tasks irrespective of workflow state. See https://github.com/cylc/cylc-flow/issues/4278 """ id_ = flow(one_conf) one = scheduler(id_, paused_start=True) # run the workflow async with start(one): # capture any job submissions submitted_tasks = capture_submission(one) assert submitted_tasks == set() # release runahead/queued tasks # (nothing should happen because the scheduler is paused) one.pool.release_runahead_tasks() one.release_tasks_to_run() assert submitted_tasks == set() # hold all tasks & resume the workflow await commands.run_cmd(commands.hold(one, ['*/*'])) one.resume_workflow() # release queued tasks # (there should be no change because the task is still held) one.release_tasks_to_run() assert submitted_tasks == set() # release all tasks await commands.run_cmd(commands.release(one, ['*/*'])) # release queued tasks # (the task should be submitted) one.release_tasks_to_run() assert len(submitted_tasks) == 1 async def test_no_poll_waiting_tasks( capture_polling, flow, one_conf, start, scheduler, ): """Waiting tasks shouldn't be polled. If a waiting task previously it will have the submit number of its previous job, and polling would erroneously return the state of that job. See https://github.com/cylc/cylc-flow/issues/4658 """ id_: str = flow(one_conf) # start the scheduler in live mode in order to activate regular polling # logic one: Scheduler = scheduler(id_, run_mode='live') log: pytest.LogCaptureFixture async with start(one) as log: # Test assumes start up with a waiting task. task = (one.pool.get_tasks())[0] assert task.state.status == TASK_STATUS_WAITING polled_tasks = capture_polling(one) # Waiting tasks should not be polled. await commands.run_cmd(commands.poll_tasks(one, ['*/*'])) assert polled_tasks == set() # Even if they have a submit number. task.submit_num = 1 await commands.run_cmd(commands.poll_tasks(one, ['*/*'])) assert len(polled_tasks) == 0 # But these states should be: for state in [ TASK_STATUS_SUBMIT_FAILED, TASK_STATUS_FAILED, TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING ]: task.state.status = state await commands.run_cmd(commands.poll_tasks(one, ['*/*'])) assert len(polled_tasks) == 1 polled_tasks.clear() # Shut down with a running task. task.state.status = TASK_STATUS_RUNNING # For good measure, check the faked running task is reported at shutdown. assert "Orphaned tasks:\n* 1/one (running)" in log.messages async def test_unexpected_ParsecError( one: Scheduler, start: Callable, log_filter: Callable, monkeypatch: pytest.MonkeyPatch ): """Test that ParsecErrors - that occur at any time other than config load when running a workflow - are displayed with traceback, because they are not expected.""" log: pytest.LogCaptureFixture def raise_ParsecError(*a, **k): raise ParsecError("Mock error") monkeypatch.setattr(one, '_configure_contact', raise_ParsecError) with pytest.raises(ParsecError): async with start(one) as log: pass assert log_filter( logging.CRITICAL, exact_match="Workflow shutting down - Mock error" ) assert TRACEBACK_MSG in log.text async def test_error_during_auto_restart( one: Scheduler, run: Callable, log_filter: Callable, monkeypatch: pytest.MonkeyPatch, ): """Test that an error during auto-restart does not get swallowed""" log: pytest.LogCaptureFixture err_msg = "Mock error: sugar in water" def mock_auto_restart(*a, **k): raise RuntimeError(err_msg) monkeypatch.setattr(one, 'workflow_auto_restart', mock_auto_restart) monkeypatch.setattr( one, 'auto_restart_mode', AutoRestartMode.RESTART_NORMAL ) with pytest.raises(RuntimeError, match=err_msg): async with run(one) as log: pass assert log_filter(logging.ERROR, err_msg) assert TRACEBACK_MSG in log.text async def test_uuid_unchanged_on_restart( one: Scheduler, scheduler: Callable, start: Callable, ): """Restart gets UUID from Database: See https://github.com/cylc/cylc-flow/issues/5615 Process: * Create a scheduler then shut it down. * Create a new scheduler for the same workflow and check that it has retrieved the UUID from the Daatabase. """ uuid_re = re.compile('CYLC_WORKFLOW_UUID=(.*)') contact_file = Path(one.workflow_run_dir) / '.service/contact' async with start(one): pass schd = scheduler(one.workflow_name, paused_start=True) async with start(schd): # UUID in contact file should be the same as that set in the database # and the scheduler. cf_uuid = uuid_re.findall(contact_file.read_text()) assert cf_uuid == [schd.uuid_str] async def test_restart_timeout( flow, one_conf, scheduler, run, log_filter, complete, capture_submission, ): """It should wait for user input if there are no tasks in the pool. When restarting a completed workflow there are no tasks in the pool so the scheduler is inclined to shutdown before the user has had the chance to trigger tasks in order to allow the workflow to continue. In order to make this easier, the scheduler should enter the paused state and wait around for a configured period before shutting itself down. See: https://github.com/cylc/cylc-flow/issues/5078 """ id_ = flow(one_conf) # run the workflow to completion schd: Scheduler = scheduler(id_, paused_start=False) async with run(schd): await complete(schd) # restart the completed workflow schd = scheduler(id_, paused_start=False) async with run(schd): # it should detect that the workflow has completed and alert the user assert log_filter( logging.WARNING, contains='This workflow already ran to completion.' ) # it should activate a timeout assert log_filter(logging.WARNING, contains='restart timer starts NOW') capture_submission(schd) # when we trigger tasks the timeout should be cleared await commands.run_cmd( commands.force_trigger_tasks(schd, ['1/one'], [])) await asyncio.sleep(0) # yield control to the main loop assert log_filter(logging.INFO, contains='restart timer stopped') @pytest.mark.parametrize("signal", ((SIGHUP), (SIGINT), (SIGTERM))) async def test_signal_escallation(one, start, signal, log_filter): """Double signal should escalate shutdown. If a term-like signal is received whilst the workflow is already stopping in NOW mode, then the shutdown should be escalated to NOW_NOW mode. See https://github.com/cylc/cylc-flow/pull/6444 """ async with start(one): # put the workflow in the stopping state one._set_stop(StopMode.REQUEST_CLEAN) assert one.stop_mode.name == 'REQUEST_CLEAN' # one signal should escalate this from CLEAN to NOW one._handle_signal(signal, None) assert log_filter(contains=signal.name) assert one.stop_mode.name == 'REQUEST_NOW' # two signals should escalate this from NOW to NOW_NOW one._handle_signal(signal, None) assert one.stop_mode.name == 'REQUEST_NOW_NOW' async def test_set_stall_interaction(flow, scheduler, start): """Test the interaction between manual set and workflow stall. A set operation can put a scheduler into the stalled state and can also get a scheduler out of the stalled state. """ id_ = flow({ 'scheduling': { 'xtriggers': {'never': 'xrandom(0)'}, 'graph': { 'R1': ''' # add a pending xtrigger so that when we un-stall the # workflow (via "cylc set") there are no downstream impacts # See https://github.com/cylc/cylc-flow/issues/7157 @never => b a => b ''' }, }, }) schd: 'Scheduler' = scheduler(id_, paused_start=False) async with start(schd): # set 1/a to failed await commands.run_cmd( commands.set_prereqs_and_outputs( schd, ['^/a'], [], [TASK_STATUS_FAILED] ) ) # the scheduler should be stalled await schd._main_loop() await schd._main_loop() assert schd.is_stalled is True assert ( schd.data_store_mgr.data[schd.tokens.id]['workflow'].status_msg == 'stalled' ) # set 1/a to succeeded await commands.run_cmd( commands.set_prereqs_and_outputs( schd, ['^/a'], [], [TASK_STATUS_SUCCEEDED] ) ) # the scheduler should NOT be stalled await schd._main_loop() await schd._main_loop() assert schd.is_stalled is False assert ( schd.data_store_mgr.data[schd.tokens.id]['workflow'].status_msg != 'stalled' ) cylc-flow-8.6.4/tests/integration/test_queues.py0000664000175000017500000001041115202510242022207 0ustar alastairalastair# 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 . from typing import TYPE_CHECKING import pytest from cylc.flow import commands if TYPE_CHECKING: from cylc.flow.scheduler import Scheduler @pytest.fixture def param_workflow(flow, scheduler): def _schd(paused_start, queue_limit): id_ = flow({ 'scheduler': { 'allow implicit tasks': True, }, 'task parameters': { 'x': '1..10', }, 'scheduling': { 'queues': { 'default': { 'limit': queue_limit, }, }, 'graph': { 'R1': ''' ''', }, }, }) return scheduler(id_, paused_start=paused_start) return _schd @pytest.mark.parametrize( 'start_paused,queue_limit', [ (False, 1), (False, 5), (True, 1), (True, 5), ] ) async def test_queue_release( param_workflow, start, capture_submission, start_paused, queue_limit, ): """Tasks should be released up to the limit if the scheduler is not paused. When the scheudler is paused the scheduler should not release tasks from queues because tasks may subsequently be held by the user and we don't want them clogging up the queue limit. https://github.com/cylc/cylc-flow/issues/4627 """ expected_submissions = queue_limit if not start_paused else 0 # start the scheduler (but don't set the main loop running) schd = param_workflow(start_paused, queue_limit) async with start(schd): # capture task submissions (prevents real submissions) submitted_tasks = capture_submission(schd) # release runahead/queued tasks # (if scheduler is paused we should not have any submissions) # (otherwise a number of tasks up to the limit should be released) schd.pool.release_runahead_tasks() schd.release_tasks_to_run() assert len(submitted_tasks) == expected_submissions for _ in range(3): # release runahead/queued tasks # (no further tasks should be released) schd.release_tasks_to_run() assert len(submitted_tasks) == expected_submissions async def test_queue_held_tasks( param_workflow, start, capture_submission ): """Held tasks should not be released from queues. Users can hold tasks whilst they are queued. These held tasks should not be released from their queues. https://github.com/cylc/cylc-flow/issues/4628 """ schd: Scheduler = param_workflow(paused_start=True, queue_limit=1) async with start(schd): # capture task submissions (prevents real submissions) submitted_tasks = capture_submission(schd) # release runahead tasks to their queues schd.pool.release_runahead_tasks() # hold all tasks and resume the workflow # (nothing should have run yet because the workflow started paused) await commands.run_cmd(commands.hold(schd, ['*/*'])) schd.resume_workflow() # release queued tasks # (no tasks should be released from the queues because they are held) schd.release_tasks_to_run() assert len(submitted_tasks) == 0 # un-hold tasks await commands.run_cmd(commands.release(schd, ['*/*'])) # release queued tasks # (tasks should now be released from the queues) schd.release_tasks_to_run() assert len(submitted_tasks) == 1 cylc-flow-8.6.4/tests/i0000777000175000017500000000000015202510242017442 2integration/ustar alastairalastaircylc-flow-8.6.4/tests/conftest.py0000664000175000017500000002146215202510242017153 0ustar alastairalastair# 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 . import logging import re from pathlib import Path from shutil import rmtree from typing import Callable, List, Optional, Tuple import time import pytest from cylc.flow import LOG, flags from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.cfgspec.globalcfg import SPEC from cylc.flow.graphnode import GraphNodeParser from cylc.flow.parsec.config import ParsecConfig from cylc.flow.parsec.validate import cylc_config_validate @pytest.fixture(autouse=True) def before_each(): """Reset global state before every test.""" flags.verbosity = 0 flags.cylc7_back_compat = False LOG.setLevel(logging.NOTSET) # Reset graph node parser singleton: GraphNodeParser.get_inst().clear() @pytest.fixture(scope='module') def mod_monkeypatch(): """A module-scoped version of the monkeypatch fixture.""" from _pytest.monkeypatch import MonkeyPatch mpatch = MonkeyPatch() yield mpatch mpatch.undo() @pytest.fixture def mock_glbl_cfg(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """A Pytest fixture for fiddling global config values. * Hacks the specified `glbl_cfg` object. * Can be called multiple times within a test function. Args: pypath (str): The python-like path to the global configuation object you want to fiddle. E.G. if you want to hack the `glbl_cfg` in `cylc.flow.scheduler` you would provide `cylc.flow.scheduler.glbl_cfg` global_config (str): The globlal configuration as a multi-line string. Example: Change the value of `UTC mode` in the global config as seen from `the scheduler` module. def test_something(mock_glbl_cfg): mock_glbl_cfg( 'cylc.flow.scheduler.glbl_cfg', ''' [scheduler] UTC mode = True ''' ) """ # TODO: modify Parsec so we can use StringIO rather than a temp file. def _mock_glbl_cfg(pypath: str, global_config: str) -> None: global_config_path = tmp_path / 'global.cylc' global_config_path.write_text(global_config) glbl_cfg = ParsecConfig(SPEC, validator=cylc_config_validate) glbl_cfg.loadcfg(global_config_path) def _inner(cached=False): return glbl_cfg monkeypatch.setattr(pypath, _inner) yield _mock_glbl_cfg rmtree(tmp_path) @pytest.fixture def log_filter(caplog: pytest.LogCaptureFixture): """Filter caplog record_tuples (also discarding the log name entry). Args: level: Filter out records if they aren't at this logging level. contains: Filter out records if this string is not in the message. regex: Filter out records if the message doesn't match this regex. exact_match: Filter out records if the message does not exactly match this string. log: A caplog instance. """ def _log_filter( level: Optional[int] = None, contains: Optional[str] = None, regex: Optional[str] = None, exact_match: Optional[str] = None, log: Optional[pytest.LogCaptureFixture] = None ) -> List[Tuple[int, str]]: if log is None: log = caplog return [ (log_level, log_message) for _, log_level, log_message in log.record_tuples if (level is None or level == log_level) and (contains is None or contains in log_message) and (regex is None or re.search(regex, log_message)) and (exact_match is None or exact_match == log_message) ] return _log_filter @pytest.fixture def log_scan(): """Ensure log messages appear in the correct order. TRY TO AVOID DOING THIS! If you are trying to test a sequence of events you are likely better off doing this a different way (e.g. mock the functions you are interested in and test the call arguments/returns later). However, there are some occasions where this might be necessary, e.g. testing a monolithic synchronous function. Args: log: The caplog fixture. items: Iterable of string messages to compare. All are tested by "contains" i.e. "item in string". """ def _log_scan(log, items): records = iter(log.records) record = next(records) for item in items: while item not in record.message: try: record = next(records) except StopIteration: raise Exception(f'Reached end of log looking for: {item}') return _log_scan @pytest.fixture(scope='session') def port_range(): return glbl_cfg().get(['scheduler', 'run hosts', 'ports']) @pytest.fixture def capcall(monkeypatch): """Capture function calls without running the function. Returns a list which is populated with function calls. Args: function_string: The function to replace as it would be specified to monkeypatch.setattr. mock: * If True, the function will be replaced by a "return None". * If False, the original function will be run. * If a Callable is provided, this will be run in place of the original function. Returns: [(args: Tuple, kwargs: Dict), ...] Example: def test_something(capcall): capsys = capcall('sys.exit') sys.exit(1) assert capsys == [(1,), {}] """ def _capcall(function_string: str, mock: bool | Callable = True): calls = [] if mock is True: fcn = lambda *args, **kwargs: None elif mock is False: fcn = import_object_from_string(function_string) else: fcn = mock def _call(*args, **kwargs): calls.append((args, kwargs)) return fcn(*args, **kwargs) monkeypatch.setattr(function_string, _call) return calls return _capcall def import_object_from_string(string): """Import a Python object from a string path. The path may reference a module, function, class, method, whatever. Examples: # import a module >>> import_object_from_string('os') # import a function >>> import_object_from_string('os.path.walk') # import a constant from a namespace package >>> import_object_from_string('cylc.flow.LOG') # import a class >>> import_object_from_string('pathlib.Path') # import a method >>> import_object_from_string('pathlib.Path.exists') """ head = string tail = [] while True: try: # try and import the thing module = __import__(head) except ModuleNotFoundError: # if it's not something we can import, lop the last item off the # end of the string and repeat if '.' in head: head, _tail = head.rsplit('.', 1) tail.append(_tail) else: # we definitely can't import this raise else: # we managed to import something if '(namespace)' in str(module): # with namespace packages you have to pull the module out of # the package yourself for part in head.split('.')[1:]: module = getattr(module, part) break # extract the requested object from the module (if requested) obj = module for part in reversed(tail): obj = getattr(obj, part) return obj @pytest.fixture def set_timezone(monkeypatch): """Fixture to temporarily set a timezone. Will use a very implausible timezone if none is provided. """ def patch(time_zone: str = 'XXX-19:17'): monkeypatch.setenv('TZ', time_zone) time.tzset() try: yield patch finally: # Reset to the original time zone after the test monkeypatch.undo() time.tzset() cylc-flow-8.6.4/tests/README.md0000664000175000017500000000113115202510242016222 0ustar alastairalastair# Tests Cylc-flow has three tiers of tests: | Test Type | Command | Docs | |---|---|---| | Unit Tests | `pytest` | [README](https://github.com/cylc/cylc-flow/blob/master/tests/unit/README.md) | | Integration tests | `pytest tests/integration` | [README](https://github.com/cylc/cylc-flow/blob/master/tests/integration/README.md) | | Functional Tests | `etc/bin/run-functional-tests` | [README](https://github.com/cylc/cylc-flow/blob/master/tests/functional/README.md) | The README files in each directory explain what sort of tests should go into which directory, how to run and debug these tests. cylc-flow-8.6.4/tests/flakyfunctional/0000775000175000017500000000000015202510242020140 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/0000775000175000017500000000000015202510242023112 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/04-dummy-mode-output/0000775000175000017500000000000015202510242026746 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/04-dummy-mode-output/reference.log0000664000175000017500000000020415202510242031403 0ustar alastairalastairInitial point: 20000101T0000Z Final point: 20000101T0000Z 20000101T0000Z/foo -triggered off [] 20000101T0000Z/bar -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/04-dummy-mode-output/flow.cylc0000664000175000017500000000134515202510242030574 0ustar alastairalastair# Live mode: baz never runs as outputs not received. # Dummy and sim modes: baz runs due to automatic completion of custom outputs. [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] initial cycle point = 2000 final cycle point = 2000 [[graph]] P1Y = "foo:meet? & bar:greet? => baz" [runtime] [[root]] script = true [[[simulation]]] default run length = PT0S [[foo]] script = true [[[outputs]]] meet = meet [[bar]] script = true [[[outputs]]] greet = greet [[baz]] cylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/test_header0000777000175000017500000000000015202510242034004 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-get-config/04-dummy-mode-output.t0000775000175000017500000000402315202510242027135 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test for completion of custom outputs in dummy and sim modes. # And no duplication dummy outputs (GitHub #2064) . "$(dirname "$0")/test_header" set_test_number 10 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate --debug "${WORKFLOW_NAME}" # Live mode run: outputs not received, workflow shuts down early without running baz workflow_run_ok "${TEST_NAME_BASE}-run-live" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOG="$(cylc log -m p "$WORKFLOW_NAME")" count_ok '(received)meet' "${LOG}" 0 count_ok '(received)greet' "${LOG}" 0 delete_db # Dummy and sim mode: outputs auto-completed, baz runs workflow_run_ok "${TEST_NAME_BASE}-run-dummy" \ cylc play -m 'dummy' --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOG="$(cylc log -m p "$WORKFLOW_NAME")" count_ok '(received)meet' "${LOG}" 1 count_ok '(received)greet' "${LOG}" 1 delete_db workflow_run_ok "${TEST_NAME_BASE}-run-simulation" \ cylc play -m 'simulation' --reference-test --debug --no-detach "${WORKFLOW_NAME}" LOG="$(cylc log -m p "$WORKFLOW_NAME")" count_ok '(received)meet' "${LOG}" 1 count_ok '(received)greet' "${LOG}" 1 purge exit cylc-flow-8.6.4/tests/flakyfunctional/job-submission/0000775000175000017500000000000015202510242023103 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty/0000775000175000017500000000000015202510242024626 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty/bin/0000775000175000017500000000000015202510242025376 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty/bin/talkingnonsense0000775000175000017500000000026015202510242030524 0ustar alastairalastair#!/usr/bin/env bash sleep 1 cat "${CYLC_REPO_DIR}/COPYING" sleep 1 tac "${CYLC_REPO_DIR}/COPYING" >&2 sleep 1 echo "$@" >>"${CYLC_WORKFLOW_RUN_DIR}/talkingnonsense.out" exit 1 cylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty/reference.log0000664000175000017500000000203615202510242027270 0ustar alastairalastair1/starter -triggered off [] in flow 1 1/nh2 -triggered off ['1/starter'] in flow 1 1/nh3 -triggered off ['1/starter'] in flow 1 1/nh1 -triggered off ['1/starter'] in flow 1 1/nh5 -triggered off ['1/starter'] in flow 1 1/nh0 -triggered off ['1/starter'] in flow 1 1/nh7 -triggered off ['1/starter'] in flow 1 1/nh9 -triggered off ['1/starter'] in flow 1 1/nh4 -triggered off ['1/starter'] in flow 1 1/nh6 -triggered off ['1/starter'] in flow 1 1/nh8 -triggered off ['1/starter'] in flow 1 1/h6 -triggered off ['1/starter'] in flow 1 1/h7 -triggered off ['1/starter'] in flow 1 1/h9 -triggered off ['1/starter'] in flow 1 1/h1 -triggered off ['1/starter'] in flow 1 1/h3 -triggered off ['1/starter'] in flow 1 1/h5 -triggered off ['1/starter'] in flow 1 1/h8 -triggered off ['1/starter'] in flow 1 1/h0 -triggered off ['1/starter'] in flow 1 1/h2 -triggered off ['1/starter'] in flow 1 1/h4 -triggered off ['1/starter'] in flow 1 1/stopper -triggered off ['1/nh0', '1/nh1', '1/nh2', '1/nh3', '1/nh4', '1/nh5', '1/nh6', '1/nh7', '1/nh8', '1/nh9'] in flow 1 cylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty/flow.cylc0000664000175000017500000000274115202510242026455 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT20S [scheduling] [[graph]] # The "starter" task sleeps 5 seconds between start and complete. # NOHOPE family should start first, so we'll have a "cylc jobs-submit" # starting to run the talkingnonsense command (which sleeps and talks # nonsense!) in serial. # While the above "cylc jobs-submit" is still on going, we start # another "cylc jobs-submit" command doing normal job submissions of # HOPEFUL tasks. # The first job submission command should never finish before getting # killed (see global configuration in ".t" file causing the NOHOPE # family to go into submission failure and triggering the stopper task. # The idea is that the NOHOPE job submission nonsense should not block # the HOPEFUL tasks from launching normally. R1 = "starter:start => NOHOPE" R1 = "starter => HOPEFUL" R1 = HOPEFUL:succeed-all R1 = "NOHOPE:submit-fail-all? => stopper" [runtime] [[starter]] script = cylc__job__wait_cylc_message_started; sleep 5 [[HOPEFUL]] script = true [[NOHOPE]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[nh0, nh1, nh2, nh3, nh4, nh5, nh6, nh7, nh8, nh9]] inherit = NOHOPE [[h0, h1, h2, h3, h4, h5, h6, h7, h8, h9]] inherit = HOPEFUL [[stopper]] script = cylc stop "${CYLC_WORKFLOW_ID}" cylc-flow-8.6.4/tests/flakyfunctional/job-submission/05-activity-log/0000775000175000017500000000000015202510242025740 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/job-submission/05-activity-log/reference.log0000664000175000017500000000012415202510242030376 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/t2 -triggered off ['1/t1'] cylc-flow-8.6.4/tests/flakyfunctional/job-submission/05-activity-log/flow.cylc0000664000175000017500000000133415202510242027564 0ustar alastairalastair[scheduler] [[events]] expected task failures = 1/t1 [scheduling] [[graph]] R1 = """t1:start => t2""" [runtime] [[t1]] script = """ cylc__job__wait_cylc_message_started set +e trap '' SIGKILL kill -s SIGKILL $$ sleep 5 # Prevent the script to run to success before it is killed """ [[[events]]] failed handlers = echo [[t2]] script = """ cylc kill "${CYLC_WORKFLOW_ID}//*/t1" sleep 1 cylc poll "${CYLC_WORKFLOW_ID}//*/t1" sleep 1 cylc shutdown "${CYLC_WORKFLOW_ID}" """ [[[job]]] execution time limit = PT1M cylc-flow-8.6.4/tests/flakyfunctional/job-submission/05-activity-log.t0000775000175000017500000000406215202510242026132 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test writing various messages to the job activity log. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 7 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" T1_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/t1/NN/job-activity.log" grep_ok '\[jobs-submit ret_code\] 0' "${T1_ACTIVITY_LOG}" grep_ok '\[jobs-kill ret_code\] 1' "${T1_ACTIVITY_LOG}" grep_ok '\[jobs-kill out\] [^|]*|1/t1/01|1' "${T1_ACTIVITY_LOG}" grep_ok '\[jobs-poll out\] [^|]*|1/t1/01|{"job_runner_name": "background", "job_id": "[^\"]*", "job_runner_exit_polled": 1, "time_submit_exit": "[^\"]*", "time_run": "[^\"]*"}' "${T1_ACTIVITY_LOG}" grep_ok "\[(('event-handler-00', 'failed'), 1) out\] failed .*/05-activity-log 1/t1 job failed" "${T1_ACTIVITY_LOG}" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/job-submission/19-chatty.t0000775000175000017500000000600215202510242025014 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test job submission with a very chatty command. # + Simulate "cylc jobs-submit" getting killed half way through. export REQUIRE_PLATFORM='runner:at' . "$(dirname "$0")/test_header" set_test_number 15 # This test relies on jobs inheriting the scheduler environment: the job # submission command bin/talkingnonsense reads COPYING from $CYLC_REPO_DIR # and writes to $CYLC_WORKFLOW_RUN_DIR. create_test_global_config " [scheduler] process pool timeout = PT10S [platforms] [[$CYLC_TEST_PLATFORM]] job runner command template = talkingnonsense %(job)s clean job submission environment = False " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-workflow-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" --reference-test # Logged killed jobs-submit command cylc cat-log "${WORKFLOW_NAME}" | sed -n ' /\[jobs-submit \(cmd\|ret_code\|out\|err\)\]/,+2{ s/^.*\(\[jobs-submit\)/\1/p }' >'log' contains_ok 'log' <<'__OUT__' [jobs-submit ret_code] -9 [jobs-submit err] killed on timeout (PT10S) __OUT__ # Logged jobs that called talkingnonsense sed -n 's/\(\[jobs-submit out\]\) .*\(|1\/\)/\1 \2/p' 'log' >'log2' N=0 while read -r; do TAIL="${REPLY#"${WORKFLOW_RUN_DIR}"/log/job/}" TASK_JOB="${TAIL%/job}" contains_ok 'log2' <<<"[jobs-submit out] |${TASK_JOB}|1|None" ((N += 1)) done <"${WORKFLOW_RUN_DIR}/talkingnonsense.out" # Logged jobs that did not call talkingnonsense for I in $(eval echo "{$N..9}"); do contains_ok 'log2' <<<"[jobs-submit out] |1/nh${I}/01|1" done # Task pool in database contains the correct states TEST_NAME="${TEST_NAME_BASE}-db-task-pool" DB_FILE="${WORKFLOW_RUN_DIR}/log/db" QUERY="SELECT cycle, name, status FROM task_states WHERE name LIKE 'nh%'" run_ok "$TEST_NAME" sqlite3 "$DB_FILE" "$QUERY" sort "${TEST_NAME}.stdout" > "${TEST_NAME}.stdout.sorted" cmp_ok "${TEST_NAME}.stdout.sorted" << '__OUT__' 1|nh0|submit-failed 1|nh1|submit-failed 1|nh2|submit-failed 1|nh3|submit-failed 1|nh4|submit-failed 1|nh5|submit-failed 1|nh6|submit-failed 1|nh7|submit-failed 1|nh8|submit-failed 1|nh9|submit-failed __OUT__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/job-submission/test_header0000777000175000017500000000000015202510242033775 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/modes/0000775000175000017500000000000015202510242021247 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/modes/03-dummy-env/0000775000175000017500000000000015202510242023410 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/modes/03-dummy-env/reference.log0000664000175000017500000000007315202510242026051 0ustar alastairalastairInitial point: 1 Final point: 1 1/oxygas -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/modes/03-dummy-env/flow.cylc0000664000175000017500000000102015202510242025224 0ustar alastairalastair[scheduling] [[graph]] R1 = oxygas [runtime] [[root]] [[[simulation]]] default run length = PT0S [[oxygas]] pre-script = echo "[MY-PRE-SCRIPT] \${CYLC_TASK_NAME} is ${CYLC_TASK_NAME}" script = """ echo "[MY-SCRIPT] \${CYLC_TASK_NAME} is ${CYLC_TASK_NAME}" """ post-script = echo "[MY-POST-SCRIPT] \${CYLC_TASK_NAME} is ${CYLC_TASK_NAME}" env-script = ELSE=foo [[[environment]]] SOMETHING = "some-modification-$ELSE" cylc-flow-8.6.4/tests/flakyfunctional/modes/03-dummy-env.t0000664000175000017500000000310715202510242023576 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that in dummy mode: # - user environment is disabled. # - env-script is disabled. # - remote host is disabled. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}"\ --mode=dummy # Check that each of pre, main and post script do not leave a trace in the # job out when --mode=dummy. declare -a GREPFOR=('MY-PRE-SCRIPT' 'MY-SCRIPT' 'MY-POST-SCRIPT') for BAD_PHRASE in "${GREPFOR[@]}"; do cp "${WORKFLOW_RUN_DIR}/log/job/1/oxygas/NN/job.out" "${BAD_PHRASE}" grep_fail "${BAD_PHRASE}" "${BAD_PHRASE}" done purge exit cylc-flow-8.6.4/tests/flakyfunctional/modes/test_header0000777000175000017500000000000015202510242032141 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cyclers/0000775000175000017500000000000015202510242021604 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cyclers/19-async_integer.t0000775000175000017500000000467615202510242025072 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test intercycle dependencies. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- if [[ -f "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}-find.out" ]]; then set_test_number 4 else set_test_number 3 fi #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_NAME=${TEST_NAME_BASE}-validate run_ok "${TEST_NAME}" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-graph" graph_workflow "${WORKFLOW_NAME}" "${WORKFLOW_NAME}.graph.plain" cmp_ok "${WORKFLOW_NAME}.graph.plain" \ "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/graph.plain.ref" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- if [[ -f "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}-find.out" ]]; then TEST_NAME="${TEST_NAME_BASE}-find" WORKFLOW_RUN_DIR="${HOME}/cylc-run/${WORKFLOW_NAME}" { (cd "${WORKFLOW_RUN_DIR}" && find 'log/job' -type f) (cd "${WORKFLOW_RUN_DIR}" && find 'work' -type f) } | sort -V >"${TEST_NAME}" cmp_ok "${TEST_NAME}" "${TEST_SOURCE_DIR}/${TEST_NAME_BASE}-find.out" fi #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/cyclers/30-r1_at_icp_or/0000775000175000017500000000000015202510242024365 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cyclers/30-r1_at_icp_or/graph.plain.ref0000664000175000017500000000053015202510242027264 0ustar alastairalastairedge "20130808T0000Z/foo" "20130808T1200Z/baz" edge "20130808T1200Z/bar" "20130808T1200Z/baz" edge "20130808T1200Z/baz" "20130809T1200Z/baz" graph node "20130808T0000Z/foo" "foo\n20130808T0000Z" node "20130808T1200Z/bar" "bar\n20130808T1200Z" node "20130808T1200Z/baz" "baz\n20130808T1200Z" node "20130809T1200Z/baz" "baz\n20130809T1200Z" stop cylc-flow-8.6.4/tests/flakyfunctional/cyclers/30-r1_at_icp_or/reference.log0000664000175000017500000000041415202510242027025 0ustar alastairalastairInitial point: 20130808T0000Z Final point: 20130809T1800Z 20130808T0000Z/foo -triggered off [] 20130808T1200Z/bar -triggered off [] 20130808T1200Z/baz -triggered off ['20130807T1200Z/baz', '20130808T0000Z/foo'] 20130809T1200Z/baz -triggered off ['20130808T1200Z/baz'] cylc-flow-8.6.4/tests/flakyfunctional/cyclers/30-r1_at_icp_or/flow.cylc0000664000175000017500000000052215202510242026207 0ustar alastairalastair[scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 20130808T00 final cycle point = 20130809T18 [[graph]] R1 = "foo" R1/T12 = "foo[^] | bar => baz" T12 = "baz[-P1D] => baz" [runtime] [[root]] script = true [[bar]] script = sleep 5 cylc-flow-8.6.4/tests/flakyfunctional/cyclers/19-async_integer/0000775000175000017500000000000015202510242024665 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cyclers/19-async_integer/graph.plain.ref0000664000175000017500000000011415202510242027562 0ustar alastairalastairedge "1/foo" "1/bar" graph node "1/bar" "bar\n1" node "1/foo" "foo\n1" stop cylc-flow-8.6.4/tests/flakyfunctional/cyclers/19-async_integer/reference.log0000664000175000017500000000012715202510242027326 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/flakyfunctional/cyclers/19-async_integer/flow.cylc0000664000175000017500000000014615202510242026511 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo => bar" [runtime] [[foo, bar]] script = true cylc-flow-8.6.4/tests/flakyfunctional/cyclers/30-r1_at_icp_or.t0000777000175000017500000000000015202510242030013 219-async_integer.tustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cyclers/test_header0000777000175000017500000000000015202510242032476 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/lib0000777000175000017500000000000015202510242023672 2../functional/libustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/shutdown/0000775000175000017500000000000015202510242022013 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/shutdown/00-rm-db.t0000664000175000017500000000363415202510242023424 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------ # Test that removing the DB quickly after a shutdown does not cause it to be # regenerated due to any outstanding connection to the DB. # NOTE: this test is not guaranteed to catch the issue, but is more likely # to do so on slower filesystems. However, faster filesystems are # less likely to have the original issue in the first place. See # https://github.com/cylc/cylc-flow/pull/4046 . "$(dirname "$0")/test_header" set_test_number 5 init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [scheduler] allow implicit tasks = True [scheduling] [[dependencies]] R1 = foo __FLOW_CONFIG__ pri_db="${WORKFLOW_RUN_DIR}/.service/db" pub_db="${WORKFLOW_RUN_DIR}/log/db" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play "${WORKFLOW_NAME}" poll_workflow_running poll_workflow_stopped # This waits for contact file to be removed # Delete the DB without delay rm -f "$pri_db" "$pub_db" # Check if DB exists without delay exists_fail "$pri_db" exists_fail "$pub_db" sleep 10 # Check if DB exists after delay exists_fail "$pri_db" exists_fail "$pub_db" purge exit cylc-flow-8.6.4/tests/flakyfunctional/shutdown/test_header0000777000175000017500000000000015202510242032705 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/README.md0000664000175000017500000000163015202510242021417 0ustar alastairalastair# Flaky Functional Tests This directory contains functional tests that are sensitive to timing, server load, etc. For more information on functional tests, see ../functional/README.md. ## How To Run These Tests ```console $ etc/bin/run-functional-tests tests/k # split the tests into 4 "chunks" and run the first chunk $ CHUNK='1/4' etc/bin/run-functional-tests tests/k ``` ## Why Are There Flaky Tests? A lot of the functional tests are highly timing dependent which can cause them to become flaky, especially on heavily loaded systems or slow file systems. We put especially sensitive functional tests into the `flakyfunctional` directory so that we can easily test them separately with fewer tests running in parallel to give them a chance of passing. ## See Also * ../functional/README.md (which has more details on functional tests) * [cylc/cylc-flow#2894](https://github.com/cylc/cylc-flow/issues/2894). cylc-flow-8.6.4/tests/flakyfunctional/hold-release/0000775000175000017500000000000015202510242022504 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/hold-release/15-hold-after/0000775000175000017500000000000015202510242024754 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/hold-release/15-hold-after/reference.log0000664000175000017500000000061615202510242027420 0ustar alastairalastairInitial point: 20140101T0000Z Final point: 20140104T0000Z 20140101T0000Z/holdafter -triggered off [] 20140101T0000Z/stopper -triggered off [] 20140101T0000Z/foo -triggered off ['20131231T1200Z/foo', '20140101T0000Z/holdafter'] 20140101T1200Z/foo -triggered off ['20140101T0000Z/foo'] 20140101T0000Z/bar -triggered off ['20140101T0000Z/foo'] 20140101T1200Z/bar -triggered off ['20140101T1200Z/foo'] cylc-flow-8.6.4/tests/flakyfunctional/hold-release/15-hold-after/flow.cylc0000664000175000017500000000151715202510242026603 0ustar alastairalastair[meta] title = "cylc hold --after" description = """One task that holds future cycles after a given cycle.""" [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] initial cycle point = 20140101T00 final cycle point = 20140104T00 [[graph]] R1 = """ stopper holdafter => foo """ T00, T12 = foo[-PT12H] => foo => bar [runtime] [[holdafter]] script = cylc hold --after '20140101T12' "${CYLC_WORKFLOW_ID}" [[stopper]] script = """ cylc__job__poll_grep_workflow_log -E '20140101T1200Z/bar/01.* \(received\)succeeded' cylc stop "${CYLC_WORKFLOW_ID}" """ [[[job]]] execution time limit = PT1M [[foo, bar]] script = true cylc-flow-8.6.4/tests/flakyfunctional/hold-release/test_header0000777000175000017500000000000015202510242033376 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/hold-release/15-hold-after.t0000775000175000017500000000320115202510242025140 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc hold --after CYCLE_POINT. # Test cylc play --hold-after CYCLE_POINT. . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" # cylc hold --after=... workflow_run_ok "${TEST_NAME_BASE}-1" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" sqlite3 "${WORKFLOW_RUN_DIR}/log/db" \ "SELECT cycle, name, status FROM task_pool WHERE cycle=='20140102T0000Z' ORDER BY name" \ >'taskpool.out' cmp_ok 'taskpool.out' <<'__OUT__' 20140102T0000Z|foo|waiting __OUT__ delete_db # cylc play --hold-after=... workflow_run_ok "${TEST_NAME_BASE}-2" \ cylc play --hold-after='20140101T1200Z' --reference-test --debug \ --no-detach "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/flakyfunctional/database/0000775000175000017500000000000015202510242021704 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/database/01-broadcast/0000775000175000017500000000000015202510242024064 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/database/01-broadcast/reference.log0000664000175000017500000000016315202510242026525 0ustar alastairalastairInitial point: 1 Final point: 1 1/t1 -triggered off [] 1/recover-t1 -triggered off ['1/t1'] 1/t1 -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/database/01-broadcast/flow.cylc0000664000175000017500000000110215202510242025701 0ustar alastairalastair[scheduling] [[graph]] R1 = """ # previously "t1:submit": flaky? recover-1 could possibly start # executing first. t1:start => recover-t1 """ [runtime] [[t1]] script = test -n "${HELLO}" execution retry delays = PT10M # prevent task failure [[[environment]]] HELLO = [[recover-t1]] script = """ cylc broadcast -p 1 -n t1 -s'[environment]HELLO=Hello' "${CYLC_WORKFLOW_ID}" sleep 1 cylc trigger "${CYLC_WORKFLOW_ID}//1/t1" """ cylc-flow-8.6.4/tests/flakyfunctional/database/00-simple.t0000664000175000017500000000735715202510242023613 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Workflow database content, a basic non-cycling workflow of 3 tasks . "$(dirname "$0")/test_header" if ! command -v 'sqlite3' >'/dev/null'; then skip_all "sqlite3 not installed?" fi set_test_number 21 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --debug --no-detach "${WORKFLOW_NAME}" DB_FILE="${RUN_DIR}/${WORKFLOW_NAME}/log/db" NAME='schema.out' ORIG="${TEST_SOURCE_DIR}/${TEST_NAME_BASE}/${NAME}" SORTED_ORIG="sorted-${NAME}" sqlite3 "${DB_FILE}" ".schema" | env LANG='C' sort >"${NAME}" env LANG='C' sort "${ORIG}" > "${SORTED_ORIG}" cmp_ok "${NAME}" "${SORTED_ORIG}" NAME='select-workflow-params.out' sqlite3 "${DB_FILE}" \ "SELECT key, value FROM workflow_params WHERE key != 'uuid_str' AND key != 'cycle_point_tz' ORDER BY key" \ >"${NAME}" cmp_ok "${NAME}" << __EOF__ UTC_mode|0 cycle_point_format| cylc_version|$(cylc --version) fcp| icp|1 is_paused|0 n_restart|0 run_mode|live startcp| stop_clock_time| stop_task| stopcp| __EOF__ NAME='select-task-events.out' sqlite3 "${DB_FILE}" 'SELECT name, cycle, event, message FROM task_events' \ >"${NAME}" cmp_ok "${NAME}" << __EOF__ foo|1|submitted| foo|1|started| foo|1|succeeded| bar|1|submitted| bar|1|started| bar|1|succeeded| baz|1|submitted| baz|1|started| baz|1|succeeded| __EOF__ NAME='select-task-jobs.out' sqlite3 "${DB_FILE}" \ 'SELECT cycle, name, submit_num, try_num, submit_status, run_status, platform_name, job_runner_name FROM task_jobs ORDER BY name' \ >"${NAME}" LOCALHOST="$(localhost_fqdn)" # FIXME: recent Travis CI failure sed -i "s/localhost/${LOCALHOST}/" "${NAME}" cmp_ok "${NAME}" - <<__SELECT__ 1|bar|1|1|0|0|${LOCALHOST}|background 1|baz|1|1|0|0|${LOCALHOST}|background 1|foo|1|1|0|0|${LOCALHOST}|background __SELECT__ NAME='select-task-jobs-times.out' sqlite3 "${DB_FILE}" \ 'SELECT time_submit,time_submit_exit,time_run,time_run_exit FROM task_jobs' \ >"${NAME}" # We want words not lines here # shellcheck disable=2013 for DATE_TIME_STR in $(sed 's/[|]/ /g' "${NAME}"); do # Parse each string with "date --date=..." without the T run_ok "${NAME}-${DATE_TIME_STR}" \ date --date="${DATE_TIME_STR/T/ }" done # Shut down with empty task pool (ran to completion) NAME=task-pool.out sqlite3 "${DB_FILE}" 'SELECT name, cycle, status FROM task_pool ORDER BY name' \ >"${NAME}" cmp_ok "${NAME}" <'/dev/null' NAME='select-task-states.out' sqlite3 "${DB_FILE}" 'SELECT name, cycle, status FROM task_states ORDER BY name' \ >"${NAME}" cmp_ok "${NAME}" << __EOF__ bar|1|succeeded baz|1|succeeded foo|1|succeeded __EOF__ NAME='select-inheritance.out' sqlite3 "${DB_FILE}" 'SELECT namespace, inheritance FROM inheritance ORDER BY namespace' \ >"${NAME}" cmp_ok "${NAME}" << __EOF__ bar|["bar", "root"] baz|["baz", "root"] foo|["foo", "root"] root|["root"] __EOF__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/database/02-retry.t0000775000175000017500000000350115202510242023457 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Workflow database content, "task_jobs" table after a task retries. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" if ! command -v sqlite3 > /dev/null; then skip 1 "sqlite3 not installed?" purge exit 0 fi DB_FILE="${RUN_DIR}/${WORKFLOW_NAME}/log/db" NAME='select-task-jobs.out' sqlite3 "${DB_FILE}" \ 'SELECT cycle, name, submit_num, try_num, submit_status, run_status, platform_name, job_runner_name FROM task_jobs ORDER BY name' \ >"${NAME}" LOCALHOST="$(localhost_fqdn)" sed -i "s/localhost/${LOCALHOST}/" "${NAME}" cmp_ok "${NAME}" <<__SELECT__ 20200101T0000Z|t1|1|1|0|1|${LOCALHOST}|background 20200101T0000Z|t1|2|2|0|1|${LOCALHOST}|background 20200101T0000Z|t1|3|3|0|0|${LOCALHOST}|background __SELECT__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/database/01-broadcast.t0000775000175000017500000000437615202510242024266 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Workflow database content, broadcast + manual trigger to recover a failure. . "$(dirname "$0")/test_header" set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" DB_FILE="${RUN_DIR}/${WORKFLOW_NAME}/log/db" if ! command -v sqlite3 > /dev/null; then skip 3 "sqlite3 not installed?" purge exit 0 fi NAME='select-broadcast-events.out' sqlite3 "${DB_FILE}" \ 'SELECT change, point, namespace, key, value FROM broadcast_events' >"${NAME}" cmp_ok "${NAME}" <<'__SELECT__' +|1|t1|[environment]HELLO|Hello __SELECT__ NAME='select-broadcast-states.out' sqlite3 "${DB_FILE}" \ 'SELECT point, namespace, key, value FROM broadcast_states' >"${NAME}" cmp_ok "${NAME}" <<'__SELECT__' 1|t1|[environment]HELLO|Hello __SELECT__ NAME='select-task-jobs.out' sqlite3 "${DB_FILE}" \ 'SELECT cycle, name, submit_num, is_manual_submit, submit_status, run_status, platform_name, job_runner_name FROM task_jobs ORDER BY name' \ >"${NAME}" LOCALHOST="$(localhost_fqdn)" # FIXME: recent Travis CI failure sed -i "s/localhost/${LOCALHOST}/" "${NAME}" cmp_ok "${NAME}" <<__SELECT__ 1|recover-t1|1|0|0|0|${LOCALHOST}|background 1|t1|1|0|0|1|${LOCALHOST}|background 1|t1|2|1|0|0|${LOCALHOST}|background __SELECT__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/database/test_header0000777000175000017500000000000015202510242032576 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/database/00-simple/0000775000175000017500000000000015202510242023412 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/database/00-simple/flow.cylc0000664000175000017500000000016215202510242025234 0ustar alastairalastair[scheduling] [[graph]] R1 = "foo => bar => baz" [runtime] [[foo, bar, baz]] script = true cylc-flow-8.6.4/tests/flakyfunctional/database/00-simple/schema.out0000664000175000017500000000432615202510242025410 0ustar alastairalastairCREATE TABLE broadcast_events(time TEXT, change TEXT, point TEXT, namespace TEXT, key TEXT, value TEXT); CREATE TABLE broadcast_states(point TEXT, namespace TEXT, key TEXT, value TEXT, PRIMARY KEY(point, namespace, key)); CREATE TABLE inheritance(namespace TEXT, inheritance TEXT, PRIMARY KEY(namespace)); CREATE TABLE workflow_params(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE workflow_template_vars(key TEXT, value TEXT, PRIMARY KEY(key)); CREATE TABLE task_action_timers(cycle TEXT, name TEXT, ctx_key TEXT, ctx TEXT, delays TEXT, num INTEGER, delay TEXT, timeout TEXT, PRIMARY KEY(cycle, name, ctx_key)); CREATE TABLE task_events(name TEXT, cycle TEXT, time TEXT, submit_num INTEGER, event TEXT, message TEXT); CREATE TABLE task_jobs(cycle TEXT, name TEXT, submit_num INTEGER, flow_nums TEXT, is_manual_submit INTEGER, try_num INTEGER, time_submit TEXT, time_submit_exit TEXT, submit_status INTEGER, time_run TEXT, time_run_exit TEXT, run_signal TEXT, run_status INTEGER, platform_name TEXT, job_runner_name TEXT, job_id TEXT, PRIMARY KEY(cycle, name, submit_num)); CREATE TABLE task_late_flags(cycle TEXT, name TEXT, value INTEGER, PRIMARY KEY(cycle, name)); CREATE TABLE task_outputs(cycle TEXT, name TEXT, flow_nums TEXT, outputs TEXT, PRIMARY KEY(cycle, name, flow_nums)); CREATE TABLE task_pool(cycle TEXT, name TEXT, flow_nums TEXT, status TEXT, is_held INTEGER, PRIMARY KEY(cycle, name, flow_nums)); CREATE TABLE task_prerequisites(cycle TEXT, name TEXT, flow_nums TEXT, prereq_name TEXT, prereq_cycle TEXT, prereq_output TEXT, satisfied TEXT, PRIMARY KEY(cycle, name, flow_nums, prereq_name, prereq_cycle, prereq_output)); CREATE TABLE task_states(name TEXT, cycle TEXT, flow_nums TEXT, time_created TEXT, time_updated TEXT, submit_num INTEGER, status TEXT, flow_wait INTEGER, is_manual_submit INTEGER, PRIMARY KEY(name, cycle, flow_nums)); CREATE TABLE task_timeout_timers(cycle TEXT, name TEXT, timeout REAL, PRIMARY KEY(cycle, name)); CREATE TABLE tasks_to_hold(name TEXT, cycle TEXT); CREATE TABLE workflow_flows(flow_num INTEGER, start_time TEXT, description TEXT, PRIMARY KEY(flow_num)); CREATE TABLE xtriggers(signature TEXT, results TEXT, PRIMARY KEY(signature)); CREATE TABLE absolute_outputs(cycle TEXT, name TEXT, output TEXT); cylc-flow-8.6.4/tests/flakyfunctional/database/02-retry/0000775000175000017500000000000015202510242023270 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/database/02-retry/reference.log0000664000175000017500000000024615202510242025733 0ustar alastairalastairInitial point: 20200101T0000Z Final point: 20200101T0000Z 20200101T0000Z/t1 -triggered off [] 20200101T0000Z/t1 -triggered off [] 20200101T0000Z/t1 -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/database/02-retry/flow.cylc0000664000175000017500000000041215202510242025110 0ustar alastairalastair[scheduler] UTC mode=True [scheduling] initial cycle point=2020 final cycle point=2020 [[graph]] P1Y = t1 [runtime] [[t1]] script=test "${CYLC_TASK_SUBMIT_NUMBER}" -gt 2 [[[job]]] execution retry delays=2*PT0S cylc-flow-8.6.4/tests/flakyfunctional/special/0000775000175000017500000000000015202510242021560 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/06-clock-triggered-iso.t0000664000175000017500000000536515202510242026036 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clock triggering is working . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "START='$(date '+%Y%m%dT%H%z')'" \ -s "HOUR='$(date '+%H')'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-run-now" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(date '+%Y%m%dT%H%z')'" \ -s "HOUR='$(date '+%H')'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- delete_db TZSTR="$(date '+%z')" NOW="$(date '+%Y%m%dT%H')" run_ok "${TEST_NAME_BASE}-run-past" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='-10')${TZSTR}'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='-10' --print-hour)'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT1M"' #------------------------------------------------------------------------------- delete_db NOW="$(date '+%Y%m%dT%H')" run_fail "${TEST_NAME_BASE}-run-later" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='10')${TZSTR}'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='10' --print-hour)'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/special/06-clock-triggered-iso0000777000175000017500000000000015202510242030767 204-clock-triggeredustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/08-clock-triggered-0.t0000664000175000017500000000530315202510242025375 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clock triggering is working, with no offset argument # https://github.com/cylc/cylc-flow/issues/1417 . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" -s "START='$(date '+%Y%m%dT%H%z')'" \ -s "HOUR='$(date '+%H')'" -s 'UTC_MODE="False"' -s 'TIMEOUT="PT0.2M"' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-run-now" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(date '+%Y%m%dT%H%z')'" \ -s "HOUR='$(date '+%H')'" -s 'UTC_MODE="False"' -s 'TIMEOUT="PT0.2M"' #------------------------------------------------------------------------------- delete_db NOW="$(date '+%Y%m%dT%H')" START="$(cylc cycle-point "${NOW}" --offset-hour='-10')$(date '+%z')" HOUR="$(cylc cycle-point "${NOW}" --offset-hour='-10' --print-hour)" run_ok "${TEST_NAME_BASE}-run-past" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" -s "START='${START}'" \ -s "HOUR='${HOUR}'" -s 'UTC_MODE="False"' -s 'TIMEOUT="PT1M"' #------------------------------------------------------------------------------- delete_db NOW="$(date '+%Y%m%dT%H')" START="$(cylc cycle-point "${NOW}" --offset-hour='10')$(date '+%z')" HOUR="$(cylc cycle-point "${NOW}" --offset-hour='10' --print-hour)" run_fail "${TEST_NAME_BASE}-run-later" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" -s START="${START}" \ -s "HOUR='${HOUR}'" -s 'UTC_MODE="False"' -s 'TIMEOUT="PT0.2M"' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/special/05-clock-triggered-utc0000777000175000017500000000000015202510242030767 204-clock-triggeredustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/04-clock-triggered.t0000664000175000017500000000531315202510242025235 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clock triggering is working . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "START='$(date '+%Y%m%dT%H')'" \ -s "HOUR='$(date '+%H')'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-run-now" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(date '+%Y%m%dT%H')'" \ -s "HOUR='$(date '+%H')'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- delete_db NOW="$(date '+%Y%m%dT%H')" run_ok "${TEST_NAME_BASE}-run-past" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='-10')'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='-10' --print-hour)'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT1M"' #------------------------------------------------------------------------------- delete_db NOW="$(date '+%Y%m%dT%H')" run_fail "${TEST_NAME_BASE}-run-later" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='10')'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='10' --print-hour)'" \ -s "CPTZ='$(date '+%z')'" \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/special/test_header0000777000175000017500000000000015202510242032452 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/08-clock-triggered-00000777000175000017500000000000015202510242030336 204-clock-triggeredustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/05-clock-triggered-utc.t0000664000175000017500000000532515202510242026032 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test clock triggering is working . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 4 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" \ -s "START='$(date -u '+%Y%m%dT%H00')Z'" \ -s "HOUR='$(date -u '+%H')'" \ -s 'UTC_MODE="True"' \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-run-now" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(date -u '+%Y%m%dT%H00')Z'" \ -s "HOUR='$(date -u '+%H')'" \ -s 'UTC_MODE="True"' \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT1M"' #------------------------------------------------------------------------------- delete_db NOW="$(date -u '+%Y%m%dT%H00')Z" run_ok "${TEST_NAME_BASE}-run-past" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='-10')'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='-10' --print-hour)'" \ -s 'UTC_MODE="True"' \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- delete_db NOW="$(date -u '+%Y%m%dT%H00')Z" run_fail "${TEST_NAME_BASE}-run-later" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" \ -s "START='$(cylc cycle-point "${NOW}" --offset-hour='10')'" \ -s "HOUR='$(cylc cycle-point "${NOW}" --offset-hour='10' --print-hour)'" \ -s 'UTC_MODE="True"' \ -s 'OFFSET="PT0S"' \ -s 'TIMEOUT="PT12S"' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/special/04-clock-triggered/0000775000175000017500000000000015202510242025046 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/special/04-clock-triggered/flow.cylc0000664000175000017500000000101715202510242026670 0ustar alastairalastair#!Jinja2 [scheduler] {% if CPTZ is defined %} cycle point time zone = {{CPTZ}} {% else %} cycle point time zone = '+0000' {% endif %} [[events]] abort on inactivity timeout = True inactivity timeout = {{TIMEOUT}} [scheduling] initial cycle point = {{START}} final cycle point = {{START}} [[special tasks]] clock-trigger = clock{% if OFFSET is defined %}({{OFFSET}}){% endif %} [[graph]] T{{HOUR}} = "clock" [runtime] [[clock]] script = true cylc-flow-8.6.4/tests/flakyfunctional/restart/0000775000175000017500000000000015202510242021624 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/bin/0000775000175000017500000000000015202510242022374 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/bin/ctb-select-task-states0000777000175000017500000000000015202510242040250 2../../../functional/restart/bin/ctb-select-task-statesustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/lib0000777000175000017500000000000015202510242027255 2../../functional/restart/libustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/39-auto-restart-no-suitable-host.t0000664000175000017500000000441015202510242030066 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" set_test_number 3 BASE_GLOBAL_CONFIG=" [scheduler] [[main loop]] plugins = health check, auto restart [[[auto restart]]] interval = PT5S [[events]] abort on inactivity timeout = True abort on stall timeout = True inactivity timeout = PT2M stall timeout = PT2M " #------------------------------------------------------------------------------- # test that workflows will not attempt to auto stop-restart if there is no # available host to restart on init_workflow "${TEST_NAME_BASE}" <<< ' [scheduler] UTC mode = True allow implicit tasks = True [scheduling] initial cycle point = 2000 [[graph]] P1D = foo ' create_test_global_config '' " ${BASE_GLOBAL_CONFIG} [scheduler] [[run hosts]] available = localhost " cylc play "${WORKFLOW_NAME}" --debug poll_workflow_running create_test_global_config '' " ${BASE_GLOBAL_CONFIG} [scheduler] [[run hosts]] available = localhost condemned = $(localhost_fqdn) " FILE=$(cylc cat-log "${WORKFLOW_NAME}" -m p |xargs readlink -f) log_scan "${TEST_NAME_BASE}-no-auto-restart" "${FILE}" 20 1 \ 'The Cylc workflow host will soon become un-available' \ 'Workflow cannot automatically restart: No alternative host' \ 'Workflow cannot automatically restart: No alternative host' \ cylc stop --kill --max-polls=10 --interval=2 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/flakyfunctional/restart/21-task-elapsed/0000775000175000017500000000000015202510242024421 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/21-task-elapsed/reference.log0000664000175000017500000000021715202510242027062 0ustar alastairalastairInitial point: 2016 Final point: 2020 2018/t1 -triggered off ['2017/t1'] 2019/t1 -triggered off ['2018/t1'] 2020/t1 -triggered off ['2019/t1'] cylc-flow-8.6.4/tests/flakyfunctional/restart/21-task-elapsed/flow.cylc0000664000175000017500000000071415202510242026246 0ustar alastairalastair#!jinja2 [scheduler] UTC mode=True cycle point format = %Y [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M [scheduling] initial cycle point = 2016 final cycle point = 2031 [[graph]] P1Y=t1 & t2 [runtime] [[t1, t2]] script = sleep $((RANDOM % 2 + 6)) [[[job]]] execution time limit = PT25S cylc-flow-8.6.4/tests/flakyfunctional/restart/14-multicycle/0000775000175000017500000000000015202510242024220 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/14-multicycle/bin/0000775000175000017500000000000015202510242024770 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/14-multicycle/bin/ctb-select-task-states0000775000175000017500000000254015202510242031205 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- set -eu CYLC_WORKFLOW_RUN_DIR="$1" CYLC_TASK_NAME="${2:-}" sqlite3 "${CYLC_WORKFLOW_RUN_DIR}/log/db" " SELECT task_states.name, task_states.cycle, task_states.submit_num, task_jobs.try_num, task_states.status FROM task_states LEFT OUTER JOIN task_jobs ON task_states.name == task_jobs.name AND task_states.cycle == task_jobs.cycle AND task_states.submit_num == task_jobs.submit_num WHERE task_states.name != '${CYLC_TASK_NAME}' ORDER BY task_states.name, task_states.cycle ;" cylc-flow-8.6.4/tests/flakyfunctional/restart/14-multicycle/flow.cylc0000664000175000017500000000271115202510242026044 0ustar alastairalastair#!jinja2 {%- set TEST_DIR = environ['TEST_DIR'] %} [scheduler] UTC mode = True [[events]] abort on stall timeout = True stall timeout = PT3M [scheduling] initial cycle point = 20130923T00 final cycle point = 20130926T00 [[graph]] PT12H = """ foo[-PT12H] => foo => bar bar[-P1D] => bar """ R1/20130925T0000Z = """ bar[-P1D] & bar[-PT12H] & foo[-PT12H] => shutdown => output_states output_states => foo => bar """ [runtime] [[foo,bar]] script = """ sleep 1 """ [[[meta]]] description = "Placeholder tasks for dependencies" [[OUTPUT]] script = """ sleep 5 ctb-select-task-states \ "${CYLC_WORKFLOW_RUN_DIR}" "${CYLC_TASK_NAME}" \ > "${CYLC_WORKFLOW_RUN_DIR}/$OUTPUT_SUFFIX-db" """ [[shutdown]] inherit = OUTPUT post-script = """ cylc shutdown $CYLC_WORKFLOW_ID sleep 5 """ [[[meta]]] description = "Force a shutdown of the workflow" [[[environment]]] OUTPUT_SUFFIX=pre-restart [[output_states]] inherit = OUTPUT pre-script = """ sleep 5 """ [[[meta]]] description = "Wait for the restart to complete, then output states" [[[environment]]] OUTPUT_SUFFIX=post-restart cylc-flow-8.6.4/tests/flakyfunctional/restart/test_header0000777000175000017500000000000015202510242032516 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/restart/14-multicycle.t0000775000175000017500000000657715202510242024427 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test restarting a workflow with multi-cycle tasks and interdependencies. if [[ -z "${TEST_DIR:-}" ]]; then . "$(dirname "$0")/test_header" fi #------------------------------------------------------------------------------- set_test_number 6 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" if ! command -v 'sqlite3' > /dev/null; then skip 5 'sqlite3 not installed?' purge exit 0 fi workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-restart" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- # The waiting tasks below have two parents and are spawned by the earlier # intercycle dependencies. cmp_ok "${WORKFLOW_RUN_DIR}/pre-restart-db" <<'__DB_DUMP__' bar|20130923T0000Z|1|1|succeeded bar|20130923T1200Z|1|1|succeeded bar|20130924T0000Z|1|1|succeeded bar|20130924T1200Z|1|1|succeeded bar|20130925T0000Z|0||waiting bar|20130925T1200Z|0||waiting foo|20130923T0000Z|1|1|succeeded foo|20130923T1200Z|1|1|succeeded foo|20130924T0000Z|1|1|succeeded foo|20130924T1200Z|1|1|succeeded foo|20130925T0000Z|0||waiting __DB_DUMP__ contains_ok "${WORKFLOW_RUN_DIR}/post-restart-db" <<'__DB_DUMP__' bar|20130923T0000Z|1|1|succeeded bar|20130923T1200Z|1|1|succeeded bar|20130924T0000Z|1|1|succeeded bar|20130924T1200Z|1|1|succeeded foo|20130923T0000Z|1|1|succeeded foo|20130923T1200Z|1|1|succeeded foo|20130924T0000Z|1|1|succeeded foo|20130924T1200Z|1|1|succeeded shutdown|20130925T0000Z|1|1|succeeded __DB_DUMP__ "${TEST_SOURCE_DIR}/bin/ctb-select-task-states" "${WORKFLOW_RUN_DIR}" \ > "${TEST_DIR}/db" cmp_ok "${TEST_DIR}/db" <<'__DB_DUMP__' bar|20130923T0000Z|1|1|succeeded bar|20130923T1200Z|1|1|succeeded bar|20130924T0000Z|1|1|succeeded bar|20130924T1200Z|1|1|succeeded bar|20130925T0000Z|1|1|succeeded bar|20130925T1200Z|1|1|succeeded bar|20130926T0000Z|1|1|succeeded foo|20130923T0000Z|1|1|succeeded foo|20130923T1200Z|1|1|succeeded foo|20130924T0000Z|1|1|succeeded foo|20130924T1200Z|1|1|succeeded foo|20130925T0000Z|1|1|succeeded foo|20130925T1200Z|1|1|succeeded foo|20130926T0000Z|1|1|succeeded output_states|20130925T0000Z|1|1|succeeded shutdown|20130925T0000Z|1|1|succeeded __DB_DUMP__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/restart/40-auto-restart-force-stop.t0000664000175000017500000000436015202510242026746 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" set_test_number 4 BASE_GLOBAL_CONFIG=" [scheduler] [[main loop]] plugins = health check, auto restart [[[auto restart]]] interval = PT5S [[events]] abort on inactivity timeout = True abort on stall timeout = True inactivity timeout = PT2M stall timeout = PT2M " #------------------------------------------------------------------------------- # test the force shutdown option (auto stop, no restart) in condemned hosts init_workflow "${TEST_NAME_BASE}" <<< ' [scheduler] allow implicit tasks = True [scheduling] [[graph]] R1 = foo ' create_test_global_config '' " ${BASE_GLOBAL_CONFIG} [scheduler] [[run hosts]] available = localhost " cylc play "${WORKFLOW_NAME}" --pause poll_workflow_running create_test_global_config '' " ${BASE_GLOBAL_CONFIG} [scheduler] [[run hosts]] available = localhost condemned = $(localhost_fqdn)! " FILE=$(cylc cat-log "${WORKFLOW_NAME}" -m p |xargs readlink -f) log_scan "${TEST_NAME_BASE}-no-auto-restart" "${FILE}" 20 1 \ 'The Cylc workflow host will soon become un-available' \ 'This workflow will be shutdown as the workflow host is' \ 'When another workflow host becomes available the workflow can' \ 'Workflow shutting down - REQUEST(NOW)' cylc stop --kill --max-polls=10 --interval=2 "${WORKFLOW_NAME}" 2>'/dev/null' purge exit cylc-flow-8.6.4/tests/flakyfunctional/restart/21-task-elapsed.t0000775000175000017500000000541615202510242024617 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- . "$(dirname "$0")/test_header" set_test_number 8 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" test_dump() { local TEST_NAME="$1" run_ok "${TEST_NAME}" python3 - "$@" <<'__PYTHON__' import ast import sys data = ast.literal_eval(open(sys.argv[1]).read()) keys = list( f"{task['cyclePoint']}/{task['name']}" for task in data['taskProxies'] ) if keys != ["2031/t1", "2031/t2"]: sys.exit(keys) for datum in data['tasks']: assert isinstance(datum['meanElapsedTime'], float) __PYTHON__ } cd "${WORKFLOW_RUN_DIR}" || exit 1 run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" RUND="${RUN_DIR}/${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play "${WORKFLOW_NAME}" --debug --no-detach --stopcp=2020 workflow_run_ok "${TEST_NAME_BASE}-restart-1" \ cylc play "${WORKFLOW_NAME}" --stopcp=2028 --debug --no-detach sed -n '/LOADING task run times/,+2{s/^.* DEBUG - //;s/[0-9]\(,\|$\)/%d\1/g;p}' \ "${RUND}/log/scheduler/log" >'restart-1.out' contains_ok "restart-1.out" <<'__OUT__' LOADING task run times + t2: %d,%d,%d,%d,%d + t1: %d,%d,%d,%d,%d __OUT__ workflow_run_ok "${TEST_NAME_BASE}-restart-2" \ cylc play "${WORKFLOW_NAME}" --stopcp=2030 --debug --no-detach sed -n '/LOADING task run times/,+2{s/^.* DEBUG - //;s/[0-9]\(,\|$\)/%d\1/g;p}' \ "${RUND}/log/scheduler/log" >'restart-2.out' contains_ok 'restart-2.out' <<'__OUT__' LOADING task run times + t2: %d,%d,%d,%d,%d,%d,%d,%d,%d,%d + t1: %d,%d,%d,%d,%d,%d,%d,%d,%d,%d __OUT__ workflow_run_ok "${TEST_NAME_BASE}-restart-3" \ cylc play "${WORKFLOW_NAME}" --hold-after=1900 # allow the task pool to settle before requesting a dump cylc workflow-state "${WORKFLOW_NAME}" \ --task=t1 \ --point=2031 \ --status=running \ --interval=1 \ --max-polls=10 1>'/dev/null' 2>&1 cylc dump -l -r "${WORKFLOW_NAME}" >'cylc-dump.out' test_dump 'cylc-dump.out' cylc stop --max-polls=10 --interval=2 "${WORKFLOW_NAME}" purge exit cylc-flow-8.6.4/tests/flakyfunctional/restart/46-stop-clock-time.t0000664000175000017500000000570715202510242025263 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test restart with stop clock time . "$(dirname "$0")/test_header" dumpdbtables() { sqlite3 "${WORKFLOW_RUN_DIR}/log/db" \ "SELECT * FROM workflow_params WHERE key=='stop_clock_time';" \ >'stopclocktime.out' } set_test_number 6 # Event should look like this: # Start workflow # At 1/t1, set stop clock time to 60 seconds ahead # At 1/t2, stop workflow # Restart # Workflow runs to stop clock time, reset stop clock time init_workflow "${TEST_NAME_BASE}" <<'__FLOW_CONFIG__' [task parameters] i = 1..10 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = P2M [scheduling] [[graph]] R1 = t => t [runtime] [[t]] script = sleep 10 [[t]] script = """ CLOCKTIME="$(($(date +%s) + 60))" echo "${CLOCKTIME}" >"${CYLC_WORKFLOW_RUN_DIR}/clocktime" cylc stop -w "$(date --date="@${CLOCKTIME}" +%FT%T%:z)" "${CYLC_WORKFLOW_ID}" """ [[t]] script = cylc stop "${CYLC_WORKFLOW_ID}" __FLOW_CONFIG__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" cylc play "${WORKFLOW_NAME}" --no-detach read -r CLOCKTIME <"${WORKFLOW_RUN_DIR}/clocktime" dumpdbtables cmp_ok 'stopclocktime.out' <<<"stop_clock_time|${CLOCKTIME}" workflow_run_ok "${TEST_NAME_BASE}-restart-1" \ cylc play "${WORKFLOW_NAME}" --no-detach dumpdbtables cmp_ok 'stopclocktime.out' <<<"stop_clock_time|" cut -d ' ' -f 4- "${WORKFLOW_RUN_DIR}/log/scheduler/log" >'log.edited' if [[ "$(date +%:z)" == '+00:00' ]]; then CLOCKTIMESTR="$(date --date="@${CLOCKTIME}" +%FT%TZ)" else CLOCKTIMESTR="$(date --date="@${CLOCKTIME}" +%FT%T%:z)" fi contains_ok 'log.edited' <<__LOG__ + stop clock time = ${CLOCKTIME} (${CLOCKTIMESTR}) Wall clock stop time reached: ${CLOCKTIMESTR} __LOG__ for i in {01..10}; do ST_FILE="${WORKFLOW_RUN_DIR}/log/job/1/t_i${i}/01/job.status" if [[ -e "${ST_FILE}" ]]; then JOB_ID="$(awk -F= '$1 == "CYLC_JOB_ID" {print $2}' "${ST_FILE}")" poll_pid_done "${JOB_ID}" fi done purge exit cylc-flow-8.6.4/tests/flakyfunctional/xtriggers/0000775000175000017500000000000015202510242022156 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/xtriggers/00-wall_clock/0000775000175000017500000000000015202510242024505 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/xtriggers/00-wall_clock/flow.cylc0000664000175000017500000000101215202510242026322 0ustar alastairalastair#!Jinja2 # Test wall_clock xtrigger: workflow will run to completion and exit if the # clock trigger is not satisfied, else abort on inactivity. [scheduler] # Default to time zone Z [[events]] abort on inactivity timeout = True inactivity timeout = PT15S [scheduling] initial cycle point = {{START}} final cycle point = {{START}} [[xtriggers]] clock = wall_clock(offset={{OFFSET}}) [[graph]] T{{HOUR}} = "@clock => foo" [runtime] [[foo]] script = true cylc-flow-8.6.4/tests/flakyfunctional/xtriggers/01-workflow_state.t0000664000175000017500000001074415202510242025641 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------ # Test workflow-state xtriggers with workflow depends on an upstream workflow # that stops once cycle short, so it should abort with waiting tasks. . "$(dirname "$0")/test_header" set_test_number 8 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" # Register and validate the upstream workflow. WORKFLOW_NAME_UPSTREAM="${WORKFLOW_NAME}-upstream" cylc install "${TEST_DIR}/${WORKFLOW_NAME}/upstream" --workflow-name="${WORKFLOW_NAME_UPSTREAM}" --no-run-name run_ok "${TEST_NAME_BASE}-validate-up" cylc validate --debug "${WORKFLOW_NAME_UPSTREAM}" # Validate the downstream test workflow. run_ok "${TEST_NAME_BASE}-validate" \ cylc val --debug --set="UPSTREAM='${WORKFLOW_NAME_UPSTREAM}'" "${WORKFLOW_NAME}" # Run the upstream workflow and detach (not a test). cylc play "${WORKFLOW_NAME_UPSTREAM}" # Run the test workflow - it should fail after inactivity ... TEST_NAME="${TEST_NAME_BASE}-run-fail" workflow_run_fail "${TEST_NAME}" \ cylc play --set="UPSTREAM='${WORKFLOW_NAME_UPSTREAM}'" --no-detach "${WORKFLOW_NAME}" WORKFLOW_LOG="$(cylc cat-log -m 'p' "${WORKFLOW_NAME}")" grep_ok 'WARNING - inactivity timer timed out after PT20S' "${WORKFLOW_LOG}" # ... with 2016/foo succeeded and 2016/FAM waiting. cylc workflow-state --old-format "${WORKFLOW_NAME}//2016" >'workflow_state.out' contains_ok 'workflow_state.out' << __END__ foo, 2016, succeeded f3, 2016, waiting f1, 2016, waiting f2, 2016, waiting __END__ # Check broadcast of xtrigger outputs to dependent tasks. JOB_LOG="$(cylc cat-log -f 'j' -m 'p' "${WORKFLOW_NAME}//2015/f1")" contains_ok "${JOB_LOG}" << __END__ upstream_workflow="${WORKFLOW_NAME_UPSTREAM}" upstream_task="foo" upstream_point="2015" upstream_trigger="data_ready" __END__ # Check broadcast of xtrigger outputs is recorded: 1) in the workflow log... # # Lines are those which should appear after a ' INFO - Broadcast # set' ('+') and later '... INFO - Broadcast cancelled:' ('-') line, where we # use as a test case an arbitrary task where such setting & cancellation occurs: contains_ok "${WORKFLOW_LOG}" << __LOG_BROADCASTS__ ${LOG_INDENT}+ [2014/f1] [environment]upstream_workflow=${WORKFLOW_NAME_UPSTREAM} ${LOG_INDENT}+ [2014/f1] [environment]upstream_task=foo ${LOG_INDENT}+ [2014/f1] [environment]upstream_point=2014 ${LOG_INDENT}+ [2014/f1] [environment]upstream_trigger=data_ready ${LOG_INDENT}- [2014/f1] [environment]upstream_workflow=${WORKFLOW_NAME_UPSTREAM} ${LOG_INDENT}- [2014/f1] [environment]upstream_task=foo ${LOG_INDENT}- [2014/f1] [environment]upstream_point=2014 ${LOG_INDENT}- [2014/f1] [environment]upstream_trigger=data_ready __LOG_BROADCASTS__ # ... and 2) in the DB. TEST_NAME="${TEST_NAME_BASE}-check-broadcast-in-db" if ! command -v 'sqlite3' >'/dev/null'; then skip 1 "sqlite3 not installed?" fi DB_FILE="${RUN_DIR}/${WORKFLOW_NAME}/log/db" NAME='db-broadcast-states.out' sqlite3 "${DB_FILE}" \ 'SELECT change, point, namespace, key, value FROM broadcast_events ORDER BY time, change, point, namespace, key' >"${NAME}" contains_ok "${NAME}" << __DB_BROADCASTS__ +|2014|f1|[environment]upstream_workflow|${WORKFLOW_NAME_UPSTREAM} +|2014|f1|[environment]upstream_task|foo +|2014|f1|[environment]upstream_point|2014 +|2014|f1|[environment]upstream_trigger|data_ready -|2014|f1|[environment]upstream_workflow|${WORKFLOW_NAME_UPSTREAM} -|2014|f1|[environment]upstream_task|foo -|2014|f1|[environment]upstream_point|2014 -|2014|f1|[environment]upstream_trigger|data_ready __DB_BROADCASTS__ purge # Clean up the upstream workflow, just in case (expect error here, but exit 0): cylc stop --now "${WORKFLOW_NAME_UPSTREAM}" --max-polls=20 --interval=2 \ >'/dev/null' 2>&1 purge "${WORKFLOW_NAME_UPSTREAM}" cylc-flow-8.6.4/tests/flakyfunctional/xtriggers/00-wall_clock.t0000664000175000017500000000425115202510242024674 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test clock xtriggers . "$(dirname "$0")/test_header" # shellcheck disable=SC2317 disable=SC2329 run_workflow() { cylc play --no-detach --debug "$1" \ -s "START='$2'" -s "HOUR='$3'" -s "OFFSET='$4'" } set_test_number 5 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" NOW="$(date -u '+%Y%m%dT%H')" # Initial cycle point is the hour just passed. START="$NOW" HOUR="$(date -u +%H)" OFFSET="PT0S" # Validate and run with "now" clock trigger (satisfied). run_ok "${TEST_NAME_BASE}-val" cylc validate "${WORKFLOW_NAME}" \ -s "START='${START}'" -s "HOUR='${HOUR}'" -s "OFFSET='${OFFSET}'" TEST_NAME="${TEST_NAME_BASE}-now" run_ok "${TEST_NAME}" run_workflow "${WORKFLOW_NAME}" "${START}" "${HOUR}" "${OFFSET}" # Run with "past" clock trigger (satisfied). START="$(cylc cycle-point "${NOW}" --offset-hour='-10')" HOUR="$(cylc cycle-point "${START}" --print-hour)" OFFSET='PT0S' delete_db TEST_NAME="${TEST_NAME_BASE}-past" run_ok "${TEST_NAME}" run_workflow "${WORKFLOW_NAME}" "${START}" "${HOUR}" "${OFFSET}" # Run with "future" clock trigger (not satisfied - stall and abort). START="$(cylc cycle-point "${NOW}" --offset-hour=10)" HOUR="$(cylc cycle-point "${START}" --print-hour)" delete_db TEST_NAME="${TEST_NAME_BASE}-future" run_fail "${TEST_NAME}" run_workflow "${WORKFLOW_NAME}" "${START}" "${HOUR}" "${OFFSET}" LOG="$(cylc cat-log -m p "${WORKFLOW_NAME}")" grep_ok "inactivity timer timed out" "${LOG}" purge exit cylc-flow-8.6.4/tests/flakyfunctional/xtriggers/test_header0000777000175000017500000000000015202510242033050 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/xtriggers/01-workflow_state/0000775000175000017500000000000015202510242025446 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/xtriggers/01-workflow_state/upstream/0000775000175000017500000000000015202510242027306 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/xtriggers/01-workflow_state/upstream/flow.cylc0000664000175000017500000000050015202510242031124 0ustar alastairalastair[scheduler] cycle point format = %Y [scheduling] initial cycle point = 2010 final cycle point = 2015 [[graph]] P1Y = "foo:data_ready => bar" [runtime] [[root]] script = true [[bar]] [[foo]] script = cylc message "data ready" [[[outputs]]] data_ready = "data ready" cylc-flow-8.6.4/tests/flakyfunctional/xtriggers/01-workflow_state/flow.cylc0000664000175000017500000000105115202510242027266 0ustar alastairalastair#!Jinja2 [scheduler] cycle point format = %Y [[events]] inactivity timeout = PT20S abort on inactivity timeout = True [scheduling] initial cycle point = 2011 final cycle point = 2016 [[xtriggers]] upstream = workflow_state("{{UPSTREAM}}//%(point)s/foo:data_ready", is_trigger=True):PT1S [[graph]] P1Y = """ foo @upstream => FAM:succeed-all => blam """ [runtime] [[root]] script = true [[foo, blam]] [[FAM]] [[f1,f2,f3]] inherit = FAM cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/0000775000175000017500000000000015202510242022036 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/03-poll-all/0000775000175000017500000000000015202510242023772 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/03-poll-all/reference.log0000664000175000017500000000117215202510242026434 0ustar alastairalastairInitial point: 20141207T0000Z Final point: 20141208T0000Z 20141207T0000Z/run_kill -triggered off ['20141206T0000Z/run_kill'] 20141207T0000Z/submit_hold -triggered off ['20141207T0000Z/run_kill'] 20141207T0000Z/poll_check_kill -triggered off ['20141207T0000Z/submit_hold'] 20141208T0000Z/run_kill -triggered off ['20141207T0000Z/run_kill'] 20141207T0000Z/poll_now -triggered off ['20141207T0000Z/poll_check_kill'] 20141208T0000Z/submit_hold -triggered off ['20141208T0000Z/run_kill'] 20141208T0000Z/poll_check_kill -triggered off ['20141208T0000Z/submit_hold'] 20141208T0000Z/poll_now -triggered off ['20141208T0000Z/poll_check_kill'] cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/03-poll-all/flow.cylc0000664000175000017500000000401215202510242025612 0ustar alastairalastair[meta] title = "Test workflow for task state change on poll result." description = """ Task run_kill fails silently - it will be stuck in 'running' unless polled. Task run_kill goes to Task submit_hold. Task poll_check_kill then polls all to find if any tasks have failed, allowing run_kill to suicide via a :fail trigger. Task submit_hold is an idle task which is killed after Task poll_check_kill succeeds by Task poll_now. Task poll_now then polls all to find if any tasks, allowing submit_hold to suicide via a :submit-fail trigger, and the workflow to shut down successfully. """ [scheduler] UTC mode = True [[events]] abort on inactivity timeout = True inactivity timeout = PT2M expected task failures = 20141207T0000Z/run_kill, \ 20141208T0000Z/run_kill, \ 20141207T0000Z/submit_hold, \ 20141208T0000Z/submit_hold [scheduling] initial cycle point = 20141207T0000Z final cycle point = 20141208T0000Z [[graph]] T00 = """ run_kill[-P1D]:fail? => run_kill? run_kill:start => submit_hold? run_kill:fail? => !run_kill submit_hold:submit? => poll_check_kill => poll_now submit_hold:submit-fail? => !submit_hold """ [runtime] [[run_kill]] init-script = cylc__job__disable_fail_signals ERR EXIT script = exit 1 [[poll_check_kill]] script = """ cylc poll "${CYLC_WORKFLOW_ID}//*" cylc__job__poll_grep_workflow_log \ "${CYLC_TASK_CYCLE_POINT}/submit_hold/01:preparing.* => submitted" st_file="${CYLC_WORKFLOW_RUN_DIR}/log/job/${CYLC_TASK_CYCLE_POINT}/submit_hold/NN/job.status" pkill -g "$(awk -F= '$1 == "CYLC_JOB_ID" {print $2}' "${st_file}")" """ [[poll_now]] script = cylc poll "${CYLC_WORKFLOW_ID}//*" [[submit_hold]] init-script = sleep 120 cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/16-execution-time-limit/0000775000175000017500000000000015202510242026335 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/16-execution-time-limit/reference.log0000664000175000017500000000012715202510242030776 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/bar -triggered off ['1/foo'] cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/16-execution-time-limit/flow.cylc0000664000175000017500000000057315202510242030165 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S expected task failures = 1/foo [scheduling] [[graph]] R1 = """ foo:fail => bar """ [runtime] [[foo]] platform = {{ environ['CYLC_TEST_PLATFORM'] }} script = sleep 20 execution time limit = PT10S [[bar]] cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/03-poll-all.t0000775000175000017500000000173415202510242024167 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that when polling all a failed task sets the task state correctly . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/test_header0000777000175000017500000000000015202510242032730 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-poll/16-execution-time-limit.t0000775000175000017500000000312715202510242026530 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test execution time limit polling. export REQUIRE_PLATFORM='loc:* comms:poll runner:background' . "$(dirname "$0")/test_header" set_test_number 5 create_test_global_config '' " [platforms] [[$CYLC_TEST_PLATFORM]] submission polling intervals = PT2S execution polling intervals = PT1M execution time limit polling intervals = PT5S " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test -v --no-detach "${WORKFLOW_NAME}" --timestamp LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log" log_scan "${TEST_NAME_BASE}-log" "${LOG}" 1 0 \ "\[1/foo/01:submitted\] => running" \ "\[1/foo/01:running\] poll now, (next in PT5S" \ "\[1/foo/01:running\] (polled)failed/XCPU" purge cylc-flow-8.6.4/tests/flakyfunctional/events/0000775000175000017500000000000015202510242021444 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/41-task-event-template-deprecated.t0000664000175000017500000000445615202510242030054 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Test deprecated batch_sys_job_id & batch_sys_name event handler template vars # - they should still work but give a validation warning . "$(dirname "$0")/test_header" set_test_number 7 init_workflow "${TEST_NAME_BASE}" << __FLOW__ [scheduling] [[graph]] R1 = foo [runtime] [[foo]] [[[events]]] started handlers = \ echo "job_id = %(batch_sys_job_id)s; job_runner_name = %(batch_sys_name)s; workflow = %(suite)s; workflow_uuid = %(suite_uuid)s" __FLOW__ run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" grep_ok 'WARNING - The event handler template variable "%(batch_sys_job_id)s" is deprecated - use "%(job_id)s" instead' \ "${TEST_NAME_BASE}-validate.stderr" -F grep_ok 'WARNING - The event handler template variable "%(batch_sys_name)s" is deprecated - use "%(job_runner_name)s" instead' \ "${TEST_NAME_BASE}-validate.stderr" -F grep_ok 'WARNING - The event handler template variable "%(suite)s" is deprecated - use "%(workflow)s" instead' \ "${TEST_NAME_BASE}-validate.stderr" -F grep_ok 'WARNING - The event handler template variable "%(suite_uuid)s" is deprecated - use "%(uuid)s" instead' \ "${TEST_NAME_BASE}-validate.stderr" -F workflow_run_ok "${TEST_NAME_BASE}-run" cylc play --no-detach "${WORKFLOW_NAME}" FOO_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job-activity.log" grep_ok "\[(('event-handler-00', 'started'), 1) out\] job_id = [0-9]\+; job_runner_name = background; workflow = ${WORKFLOW_NAME}; workflow_uuid = [a-f0-9\-]\+" "$FOO_ACTIVITY_LOG" purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/05-timeout-ref-dummy/0000775000175000017500000000000015202510242025257 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/05-timeout-ref-dummy/flow.cylc0000664000175000017500000000050215202510242027077 0ustar alastairalastair# Time out with an unhandled failure. [scheduler] [[events]] abort on stall timeout = True stall timeout = PT0S [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = "false" [[[simulation]]] fail cycle points = 1 default run length = PT0S cylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all/0000775000175000017500000000000015202510242026335 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all/bin/0000775000175000017500000000000015202510242027105 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all/bin/checkargs0000775000175000017500000000257615202510242030777 0ustar alastairalastair#!/usr/bin/env python3 # Compare actual and expected event handler command lines. import os import sys from subprocess import Popen, PIPE args = dict([arg.split('=', 1) for arg in sys.argv[1:]]) workflow = os.environ['CYLC_WORKFLOW_ID'] proc = Popen(['cylc', 'cat-log', '-m', 'p', '-f', 'a', workflow, '//1/foo'], stdout=PIPE, stdin=open(os.devnull)) alog = proc.communicate()[0].decode().strip() proc.wait() for line in open(alog): if 'STDOUT' in line: submit_time, _, job_id = line.split(' ') break del args['start_time'] # must exist, but value unreliable desired_args = { 'workflow_title': 'a test workflow', 'job_id': job_id.strip(), 'point': '1', 'URL': 'http://cheesy.peas', 'title': 'a task called foo', 'fish': 'trout', 'submit_num': '1', 'try_num': '1', 'job_runner_name': 'background', 'id': '1/foo', 'finish_time': 'None', 'workflow_size': 'large', 'workflow': workflow, 'message': 'cheesy peas', 'platform_name': 'localhost', 'event': 'custom', 'submit_time': submit_time, 'name': 'foo' } try: assert args == desired_args except AssertionError: msg = "" for key, value in desired_args.items(): if args[key] != value: msg += f"\nkey, args[key], value are: {key, args[key], value}" raise AssertionError(msg) print('OK: command line checks out') cylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all/reference.log0000664000175000017500000000007015202510242030773 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all/flow.cylc0000664000175000017500000000205315202510242030160 0ustar alastairalastair# Test all the standard event handler command line template args. # When testing this workflow outside the test framework you may find # it useful to delete the workflow from your `cylc-run` directory # between tries. [meta] title = a test workflow size = large [scheduling] [[graph]] R1 = "foo" [runtime] [[foo]] script = """ cylc__job__wait_cylc_message_started cylc message -p CUSTOM "cheesy peas" """ [[[events]]] custom handlers = checkargs workflow=%(workflow)s job_id=%(job_id)s event=%(event)s point=%(point)s name=%(name)s try_num=%(try_num)s submit_num=%(submit_num)s id=%(id)s job_runner_name=%(job_runner_name)s message=%(message)s fish=%(fish)s title=%(title)s URL=%(URL)s workflow_title=%(workflow_title)s workflow_size=%(workflow_size)s submit_time=%(submit_time)s start_time=%(start_time)s finish_time=%(finish_time)s platform_name=%(platform_name)s [[[meta]]] title = "a task called foo" URL = http://cheesy.peas fish = trout cylc-flow-8.6.4/tests/flakyfunctional/events/40-stall-despite-clock-trig.t0000775000175000017500000000236015202510242026664 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test stall does not wait for unrelated clock trigger. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_fail "${TEST_NAME}" cylc play --debug --no-detach "${WORKFLOW_NAME}" grep_ok "Workflow stalled" "${TEST_NAME}.stderr" purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/39-task-event-template-all.t0000775000175000017500000000236315202510242026531 0ustar alastairalastair#!/usr/bin/env bash # 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 . # Compare actual and expected event handler command args. . "$(dirname "$0")/test_header" set_test_number 3 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach --reference-test "${WORKFLOW_NAME}" FOO_ACTIVITY_LOG="${WORKFLOW_RUN_DIR}/log/job/1/foo/NN/job-activity.log" grep_ok 'OK: command line checks out' "${FOO_ACTIVITY_LOG}" purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/01-task.t0000775000175000017500000000365615202510242023026 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the task events workflow. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" create_test_global_config ' [platforms] [[test platform]] hosts = NOHOST.NODOMAIN ' #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate \ --set=WORKFLOW_LOG_DIR=\""${WORKFLOW_RUN_DIR}/log/scheduler"\" \ "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach \ --set=WORKFLOW_LOG_DIR=\""${WORKFLOW_RUN_DIR}/log/scheduler"\" \ "${WORKFLOW_NAME}" sort -u 'events.log' >'expected.events.log' sed 's/ (after .*)$//' "${WORKFLOW_RUN_DIR}/log/scheduler/events.log" | sort -u \ >'actual.events.log' cmp_ok 'actual.events.log' 'expected.events.log' #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/test_header0000777000175000017500000000000015202510242032336 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/05-timeout-ref-dummy.t0000775000175000017500000000337615202510242025460 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Validate and run the workflow reference test dummy timeout workflow . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 3 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" RUN_MODE="$(basename "$0" | sed "s/.*-ref-\(.*\).t/\1/g")" workflow_run_fail "${TEST_NAME}" \ cylc play --mode="${RUN_MODE}" --debug --no-detach "${WORKFLOW_NAME}" grep_ok "WARNING - stall timer timed out after P0Y" "${TEST_NAME}.stderr" #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/40-stall-despite-clock-trig/0000775000175000017500000000000015202510242026473 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/40-stall-despite-clock-trig/flow.cylc0000664000175000017500000000144015202510242030315 0ustar alastairalastair# Stall due to unhandled failure of t2 # TODO: I think this test can be removed. Since SoD it only tests that a workflow # can stall due to unhandled failed tasks, which is tested elsewhere. It was # probably meant to test that stall was not affected by the clock trigger on # waiting t1 in the next cycle under SoS. [scheduler] UTC mode = True cycle point format = %Y%m%d [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT5M [scheduling] initial cycle point = now [[special tasks]] clock-trigger = t1(P0D) [[graph]] P1D=t3[-P1D] => t1 => t2 => t3 [runtime] [[t1]] script = true [[t2]] script = false [[t3]] script = true cylc-flow-8.6.4/tests/flakyfunctional/events/44-timeout/0000775000175000017500000000000015202510242023357 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/44-timeout/bin/0000775000175000017500000000000015202510242024127 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/44-timeout/bin/sleeper.sh0000775000175000017500000000003715202510242026125 0ustar alastairalastair#!/usr/bin/env bash sleep 300 cylc-flow-8.6.4/tests/flakyfunctional/events/44-timeout/flow.cylc0000664000175000017500000000046015202510242025202 0ustar alastairalastair[scheduler] [[events]] abort on stall timeout = True stall timeout = PT20S [scheduling] [[graph]] R1 = "foo => stopper" [runtime] [[foo]] [[[events]]] started handlers = sleeper.sh %(id)s [[stopper]] script = cylc stop "${CYLC_WORKFLOW_ID}" cylc-flow-8.6.4/tests/flakyfunctional/events/44-timeout.t0000775000175000017500000000350515202510242023552 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test that timed out event handlers get killed and recorded as failed. . "$(dirname "$0")/test_header" set_test_number 4 create_test_global_config "" " [scheduler] process pool timeout = PT10S " install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --debug --no-detach "${WORKFLOW_NAME}" sed -e 's/^.* \([EW]\)/\1/' "${WORKFLOW_RUN_DIR}/log/scheduler/log" >'log' contains_ok 'log' <<__END__ ERROR - [(('event-handler-00', 'started'), 1) cmd] sleeper.sh 1/foo ${LOG_INDENT}[(('event-handler-00', 'started'), 1) ret_code] -9 ${LOG_INDENT}[(('event-handler-00', 'started'), 1) err] killed on timeout (PT10S) WARNING - 1/foo/01 handler:event-handler-00 for task event:started failed __END__ cylc workflow-state --old-format "${WORKFLOW_NAME}" >'workflow-state.log' contains_ok 'workflow-state.log' << __END__ stopper, 1, succeeded foo, 1, succeeded __END__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/events/01-task/0000775000175000017500000000000015202510242022624 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/01-task/bin/0000775000175000017500000000000015202510242023374 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/events/01-task/bin/handler.sh0000775000175000017500000000021215202510242025343 0ustar alastairalastair#!/usr/bin/env bash EVENT="$1" #WORKFLOW="$2" TASK="$3" MSG="$4" printf "%-20s %-8s %s\n" "${EVENT}" "${TASK}" "${MSG}" >> "${EVNTLOG}" cylc-flow-8.6.4/tests/flakyfunctional/events/01-task/events.log0000664000175000017500000000115315202510242024633 0ustar alastairalastairEVENT TASK MESSAGE critical 1/foo failed/ERR execution timeout 1/foo execution timeout after PT3S failed 1/baz job failed retry 1/foo job failed, retrying in PT3S started 1/baz job started submission failed 1/bar job submission failed submission retry 1/bar job submission failed, retrying in PT3S submission timeout 1/baz submission timeout after PT3S submitted 1/baz job submitted succeeded 1/foo job succeeded warning 1/foo this is a user-defined warning message cylc-flow-8.6.4/tests/flakyfunctional/events/01-task/reference.log0000664000175000017500000000041315202510242025263 0ustar alastairalastairInitial point: 1 Final point: 1 1/prep -triggered off [] 1/foo -triggered off ['1/prep'] 1/bar -triggered off ['1/prep'] 1/baz -triggered off ['1/prep'] 1/foo -triggered off ['1/prep'] 1/bar -triggered off ['1/prep'] 1/done -triggered off ['1/bar', '1/baz', '1/foo'] cylc-flow-8.6.4/tests/flakyfunctional/events/01-task/flow.cylc0000664000175000017500000000464115202510242024454 0ustar alastairalastair#!jinja2 # simple generic handler in the workflow bin dir: {% set EVNTLOG = "$CYLC_WORKFLOW_LOG_DIR/events.log" %} {% set HANDLER = "EVNTLOG={0} handler.sh".format(EVNTLOG) %} [meta] title = "test all event handlers" [scheduler] allow implicit tasks = True [[events]] abort on stall timeout = True stall timeout = PT0S abort on inactivity timeout = True inactivity timeout = PT3M expected task failures = 1/bar, 1/baz [scheduling] [[graph]] R1 = """ prep => foo & bar & baz? bar:submit-fail? & baz:fail? & foo => done done => !bar & !baz """ [runtime] [[root]] script = "true" # fast [[prep]] script = printf "%-20s %-8s %s\n" EVENT TASK MESSAGE > {{ EVNTLOG }} [[foo]] # timeout, retry, warning, succeeded script = """ test "${CYLC_TASK_TRY_NUMBER}" -gt 1 while ! grep -q 'execution timeout *1/foo' "${CYLC_WORKFLOW_LOG_DIR}/events.log" do sleep 1 done cylc message -p WARNING 'this is a user-defined warning message' """ [[[job]]] execution retry delays = PT3S [[[events]]] succeeded handlers = {{ HANDLER }} warning handlers = {{ HANDLER }} critical handlers = {{ HANDLER }} retry handlers = {{ HANDLER }} execution timeout = PT3S execution timeout handlers = {{ HANDLER }} [[bar]] # submission retry and submission failed platform = test platform [[[events]]] submission failed handlers = {{ HANDLER }} submission retry handlers = {{ HANDLER }} [[[job]]] submission retry delays = PT3S [[baz]] # submitted, submission timeout, started, failed # Delay in init-script to cause submission timeout. # (Note CYLC_WORKFLOW_LOG_DIR is not defined at this point!) init-script = """ while ! grep -q 'submission timeout .*1/baz' "${CYLC_WORKFLOW_LOG_DIR}/events.log" do sleep 1 done """ script = false [[[events]]] submitted handlers = {{ HANDLER }} started handlers = {{ HANDLER }} failed handlers = {{ HANDLER }} submission timeout = PT3S submission timeout handlers = {{ HANDLER }} cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/0000775000175000017500000000000015202510242024213 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/00-background.t0000775000175000017500000000267615202510242026752 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution time limit, background/at job JOB_RUNNER="${0##*\/??-}" export REQUIRE_PLATFORM="runner:${JOB_RUNNER%%.t}" . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" \ cylc validate "${WORKFLOW_NAME}" workflow_run_fail "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach --abort-if-any-task-fails "${WORKFLOW_NAME}" LOGD="${RUN_DIR}/${WORKFLOW_NAME}/log/job/1/foo" grep_ok '# Execution time limit: 5.0' "${LOGD}/01/job" grep_ok 'CYLC_JOB_EXIT=XCPU' "${LOGD}/01/job.status" purge exit cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/04-poll.t0000775000175000017500000000175015202510242025575 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test execution time limit polling. . "$(dirname "$0")/test_header" set_test_number 2 export REFTEST_OPTS="--abort-if-any-task-fails" reftest exit cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/01-at.t0000777000175000017500000000000015202510242027736 200-background.tustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/04-poll/0000775000175000017500000000000015202510242025402 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/04-poll/reference.log0000664000175000017500000000012015202510242030034 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] 1/foo -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/04-poll/flow.cylc0000664000175000017500000000121215202510242027221 0ustar alastairalastair#!jinja2 [scheduler] [[events]] abort on inactivity timeout = True inactivity timeout = PT2M [scheduling] [[graph]] R1 = foo [runtime] [[foo]] init-script = cylc__job__disable_fail_signals ERR EXIT script = """ cylc__job__wait_cylc_message_started if [[ "${CYLC_TASK_SUBMIT_NUMBER}" == '1' ]]; then # Will be killed after PT5S sleep 40 else # Fake success cat >>"$0.status" <<__STATUS__ CYLC_JOB_EXIT=SUCCEEDED CYLC_JOB_EXIT_TIME=$(date -u '+%FT%H:%M:%SZ') __STATUS__ fi exit 1 """ [[[job]]] execution time limit = PT10S execution retry delays = PT0S cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/test_header0000777000175000017500000000000015202510242035105 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/01-at0000777000175000017500000000000015202510242027232 200-backgroundustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/00-background/0000775000175000017500000000000015202510242026547 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/00-background/reference.log0000664000175000017500000000007015202510242031205 0ustar alastairalastairInitial point: 1 Final point: 1 1/foo -triggered off [] cylc-flow-8.6.4/tests/flakyfunctional/execution-time-limit/00-background/flow.cylc0000664000175000017500000000064515202510242030377 0ustar alastairalastair#!Jinja2 [scheduler] [[events]] abort on inactivity timeout = True abort on stall timeout = True stall timeout = PT0S inactivity timeout = PT2M expected task failures = 1/foo [scheduling] [[graph]] R1 = foo [runtime] [[foo]] script = sleep 10 platform = {{ environ['CYLC_TEST_PLATFORM'] }} [[[job]]] execution time limit = PT5S cylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/0000775000175000017500000000000015202510242023223 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/00-satellite.t0000664000175000017500000000176215202510242025621 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Run the integer cycling satellite example workflow. # Non-specific, tests a bunch of stuff. . "$(dirname "$0")/test_header" set_test_number 2 reftest exit cylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/00-satellite/0000775000175000017500000000000015202510242025426 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/00-satellite/reference.log0000664000175000017500000000112615202510242030067 0ustar alastairalastairInitial point: 1 Final point: 3 1/prep -triggered off [] 1/get_data -triggered off ['0/get_data', '1/prep'] 1/satsim -triggered off ['1/prep'] 2/get_data -triggered off ['1/get_data'] 1/proc1 -triggered off ['1/get_data'] 1/proc2 -triggered off ['1/proc1'] 3/get_data -triggered off ['2/get_data'] 2/proc1 -triggered off ['2/get_data'] 1/products -triggered off ['1/proc2'] 3/proc1 -triggered off ['3/get_data'] 2/proc2 -triggered off ['2/proc1'] 3/proc2 -triggered off ['3/proc1'] 2/products -triggered off ['2/proc2'] 3/products -triggered off ['3/proc2'] 3/collate -triggered off ['3/products'] cylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/00-satellite/flow.cylc0000664000175000017500000000674515202510242027265 0ustar alastairalastair#!Jinja2 [meta] title = Test workflow based on the satellite data processing example description = """ Each successive integer cycle retrieves and processes the next arbitrarily timed and arbitrarily labelled dataset, in parallel with previous cycles if the data comes in quickly. """ # you can monitor output processing with: # $ watch -n 1 \ # "find ~/cylc-run//share; find ~/cylc-run//work" {% set N_DATASETS = 3 %} # define shared directories (could use runtime namespaces for this) {% set DATA_IN_DIR = "$CYLC_WORKFLOW_SHARE_DIR/incoming" %} {% set PRODUCT_DIR = "$CYLC_WORKFLOW_SHARE_DIR/products" %} [scheduling] cycling mode = integer initial cycle point = 1 final cycle point = {{N_DATASETS}} runahead limit = P2 [[graph]] R1 = prep => satsim & get_data P1 = """ # Processing chain for each dataset get_data => proc1 => proc2 => products # As one dataset is retrieved, start waiting on another. get_data[-P1] => get_data """ R1//{{N_DATASETS}} = products => collate [runtime] [[prep]] script = rm -rf $CYLC_WORKFLOW_SHARE_DIR $CYLC_WORKFLOW_WORK_DIR [[[meta]]] title = clean the workflow output directories [[satsim]] pre-script = mkdir -p {{DATA_IN_DIR}} script = """ COUNT=0 while true; do (( COUNT == {{N_DATASETS}} )) && break sleep $(( 1 + RANDOM % 5 )) touch {{DATA_IN_DIR}}/dataset-$(date +%s).raw (( COUNT += 1 )) done """ [[[meta]]] title = simulate a satellite data feed description = """ Generates {{N_DATASETS}} arbitrarily labelled datasets after random durations. """ [[WORKDIR]] # Define a common cycle-point-specific work-directory for all # processing tasks so that they all work on the same dataset. work sub-directory = proc-$CYLC_TASK_CYCLE_POINT #pre-script = sleep 10 [[get_data]] inherit = WORKDIR script = """ while ! DATASET=$(ls {{DATA_IN_DIR}}/dataset-*.raw 2>/dev/null \ | head -n 1) do sleep 1 done mv "$DATASET" "$PWD" """ [[[meta]]] title = grab one new dataset, waiting if necessary [[proc1]] inherit = WORKDIR script = """ DATASET=$(ls dataset-*.raw) mv $DATASET ${DATASET%raw}proc1 """ [[[meta]]] title = convert .raw dataset to .proc1 form [[proc2]] inherit = WORKDIR script = """ DATASET=$(ls dataset-*.proc1) mv $DATASET ${DATASET%proc1}proc2 """ [[[meta]]] title = convert .proc1 dataset to .proc2 form [[products]] inherit = WORKDIR pre-script = mkdir -p {{PRODUCT_DIR}} script = """ DATASET=$( ls dataset-*.proc2 ) mv $DATASET {{PRODUCT_DIR}}/${DATASET%proc2}prod """ [[[meta]]] title = generate products from .proc2 processed dataset [[collate]] # Note you might want to use "cylc workflow-state" to check that # _all_ product tasks have finished before collating results. script = ls {{PRODUCT_DIR}} [[[meta]]] title = collate all products from the workflow run cylc-flow-8.6.4/tests/flakyfunctional/integer-cycling/test_header0000777000175000017500000000000015202510242034115 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-show/0000775000175000017500000000000015202510242022050 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-show/00-simple.t0000664000175000017500000001216315202510242023746 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc show for a basic task. . "$(dirname "$0")/test_header" #------------------------------------------------------------------------------- set_test_number 8 #------------------------------------------------------------------------------- install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" #------------------------------------------------------------------------------- TEST_SHOW_OUTPUT_PATH="${PWD}/${TEST_NAME_BASE}-show" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-validate" run_ok "${TEST_NAME}" cylc validate \ --set="TEST_OUTPUT_PATH='${TEST_SHOW_OUTPUT_PATH}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-run" workflow_run_ok "${TEST_NAME}" cylc play --reference-test --debug --no-detach \ --set="TEST_OUTPUT_PATH='${TEST_SHOW_OUTPUT_PATH}'" "${WORKFLOW_NAME}" #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-show" cmp_ok "${TEST_NAME}-workflow" <<'__SHOW_OUTPUT__' title: a test workflow description: the quick brown fox custom: custard URL: (not given) __SHOW_OUTPUT__ cmp_ok "${TEST_NAME}-task" <<'__SHOW_OUTPUT__' title: a task description: jumped over the lazy dog baz: pub URL: (not given) __SHOW_OUTPUT__ cmp_ok "${TEST_NAME}-taskinstance" <<'__SHOW_OUTPUT__' title: a task description: jumped over the lazy dog baz: pub URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 20141106T0900Z/bar succeeded outputs: ('⨯': not completed) ⨯ 20141106T0900Z/foo expired ✓ 20141106T0900Z/foo submitted ⨯ 20141106T0900Z/foo submit-failed ✓ 20141106T0900Z/foo started ⨯ 20141106T0900Z/foo succeeded ⨯ 20141106T0900Z/foo failed output completion: incomplete ┆ ( ✓ ┆ started ⨯ ┆ and succeeded ┆ ) __SHOW_OUTPUT__ #------------------------------------------------------------------------------- TEST_NAME="${TEST_NAME_BASE}-show-json" cmp_json "${TEST_NAME}-workflow" "${TEST_NAME}-workflow" <<'__SHOW_OUTPUT__' { "title": "a test workflow", "description": "the quick brown fox", "URL": "", "custom": "custard" } __SHOW_OUTPUT__ cmp_json "${TEST_NAME}-task" "${TEST_NAME}-task" <<'__SHOW_OUTPUT__' { "foo": { "title": "a task", "description": "jumped over the lazy dog", "URL": "", "baz": "pub" } } __SHOW_OUTPUT__ cmp_json "${TEST_NAME}-taskinstance" "${TEST_NAME}-taskinstance" \ <<__SHOW_OUTPUT__ { "20141106T0900Z/foo": { "name": "foo", "id": "~${USER}/${WORKFLOW_NAME}//20141106T0900Z/foo", "cyclePoint": "20141106T0900Z", "state": "running", "isHeld": false, "isQueued": false, "isRunahead": false, "flowNums": "[1]", "task": { "meta": { "title": "a task", "description": "jumped over the lazy dog", "URL": "", "userDefined": { "baz": "pub" } } }, "runtime": { "completion": "(started and succeeded)", "runMode": "Live" }, "prerequisites": [ { "expression": "0", "conditions": [ { "exprAlias": "0", "taskId": "20141106T0900Z/bar", "reqState": "succeeded", "message": "satisfied naturally", "satisfied": true } ], "satisfied": true } ], "outputs": [ {"label": "expired", "message": "expired", "satisfied": false}, {"label": "submitted", "message": "submitted", "satisfied": true}, {"label": "submit-failed", "message": "submit-failed", "satisfied": false}, {"label": "started", "message": "started", "satisfied": true}, {"label": "succeeded", "message": "succeeded", "satisfied": false}, {"label": "failed", "message": "failed", "satisfied": false} ], "externalTriggers": [], "xtriggers": [] } } __SHOW_OUTPUT__ #------------------------------------------------------------------------------- purge exit cylc-flow-8.6.4/tests/flakyfunctional/cylc-show/04-multi/0000775000175000017500000000000015202510242023423 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-show/04-multi/reference.log0000664000175000017500000000037015202510242026064 0ustar alastairalastairInitial point: 2016 Final point: 2018 2016/t1 -triggered off ['2015/t1'] 2016/t2 -triggered off ['2015/t1'] 2017/t1 -triggered off ['2016/t1'] 2017/t2 -triggered off ['2016/t1'] 2018/t1 -triggered off ['2017/t1'] 2018/t2 -triggered off ['2017/t1'] cylc-flow-8.6.4/tests/flakyfunctional/cylc-show/04-multi/flow.cylc0000664000175000017500000000213315202510242025245 0ustar alastairalastair#!jinja2 [scheduler] cycle point format = %Y UTC mode = True [scheduling] initial cycle point = 2016 final cycle point = 2018 [[graph]] P1Y = t1[-P1Y]:start => t1 & t2 [runtime] [[t1]] script = """ # Final task runs the show. The other wait after starting. if [[ "${CYLC_TASK_CYCLE_POINT}" == '2018' ]]; then # Ensure workflow knows about current task started cylc__job__wait_cylc_message_started sleep 5 cylc show "${CYLC_WORKFLOW_ID}//*/t1" \ >"${CYLC_WORKFLOW_RUN_DIR}/show.txt" cylc show --task-def=t1 --task-def=t2 "${CYLC_WORKFLOW_ID}" \ >"${CYLC_WORKFLOW_RUN_DIR}/show2.txt" else while [[ ! -s "${CYLC_WORKFLOW_RUN_DIR}/show.txt" ]]; do sleep 1 done fi """ execution time limit = PT1M [[t2]] [[[meta]]] title = beer description = better than water URL = beer.com abv = 12% cylc-flow-8.6.4/tests/flakyfunctional/cylc-show/04-multi.t0000664000175000017500000000531715202510242023616 0ustar alastairalastair#!/usr/bin/env bash # 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 . #------------------------------------------------------------------------------- # Test cylc show multiple tasks . "$(dirname "$0")/test_header" set_test_number 4 install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}" run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}" workflow_run_ok "${TEST_NAME_BASE}-run" \ cylc play --reference-test --debug --no-detach "${WORKFLOW_NAME}" RUND="${RUN_DIR}/${WORKFLOW_NAME}" contains_ok "${RUND}/show.txt" <<'__TXT__' Task ID: 2016/t1 title: (not given) description: (not given) URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 2015/t1 started outputs: ('⨯': not completed) ⨯ 2016/t1 expired ✓ 2016/t1 submitted ⨯ 2016/t1 submit-failed ✓ 2016/t1 started ⨯ 2016/t1 succeeded ⨯ 2016/t1 failed output completion: incomplete ┆ ( ✓ ┆ started ⨯ ┆ and succeeded ┆ ) Task ID: 2017/t1 title: (not given) description: (not given) URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 2016/t1 started outputs: ('⨯': not completed) ⨯ 2017/t1 expired ✓ 2017/t1 submitted ⨯ 2017/t1 submit-failed ✓ 2017/t1 started ⨯ 2017/t1 succeeded ⨯ 2017/t1 failed output completion: incomplete ┆ ( ✓ ┆ started ⨯ ┆ and succeeded ┆ ) Task ID: 2018/t1 title: (not given) description: (not given) URL: (not given) state: running prerequisites: ('⨯': not satisfied) ✓ 2017/t1 started outputs: ('⨯': not completed) ⨯ 2018/t1 expired ✓ 2018/t1 submitted ⨯ 2018/t1 submit-failed ✓ 2018/t1 started ⨯ 2018/t1 succeeded ⨯ 2018/t1 failed output completion: incomplete ┆ ( ✓ ┆ started ⨯ ┆ and succeeded ┆ ) __TXT__ contains_ok "${RUND}/show2.txt" <<'__TXT__' TASK NAME: t1 title: (not given) description: (not given) URL: (not given) TASK NAME: t2 title: beer description: better than water abv: 12% URL: beer.com __TXT__ purge exit cylc-flow-8.6.4/tests/flakyfunctional/cylc-show/test_header0000777000175000017500000000000015202510242032742 2../../functional/lib/bash/test_headerustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-show/00-simple/0000775000175000017500000000000015202510242023556 5ustar alastairalastaircylc-flow-8.6.4/tests/flakyfunctional/cylc-show/00-simple/reference.log0000664000175000017500000000136115202510242026220 0ustar alastairalastair20141106T0900Z/bar -triggered off [] 20141106T0900Z/foo -triggered off ['20141106T0900Z/bar'] 20141106T0900Z/show-task-json -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/show-workflow-json -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/show-taskinstance-json -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/show-taskinstance -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/show-task -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/show-workflow -triggered off ['20141106T0900Z/foo'] 20141106T0900Z/end -triggered off ['20141106T0900Z/show-task', '20141106T0900Z/show-task-json', '20141106T0900Z/show-taskinstance', '20141106T0900Z/show-taskinstance-json', '20141106T0900Z/show-workflow', '20141106T0900Z/show-workflow-json'] cylc-flow-8.6.4/tests/flakyfunctional/cylc-show/00-simple/flow.cylc0000664000175000017500000000340215202510242025400 0ustar alastairalastair#!jinja2 [meta] title = a test workflow description = the quick brown fox custom = custard [scheduler] UTC mode = True [scheduling] initial cycle point = 20141106T09 final cycle point = 20141106T09 [[graph]] PT1H = """ bar => foo foo:start => SHOW? SHOW:finish-all => end """ [runtime] [[foo]] script = """ touch 'foot' while [[ -e 'foot' ]]; do sleep 1 done """ [[[meta]]] title = a task description = jumped over the lazy dog baz = pub [[bar]] script = true [[end]] script = rm -f '../foo/foot' [[SHOW]] [[show-workflow]] inherit = SHOW script = cylc show "$CYLC_WORKFLOW_ID" >>{{ TEST_OUTPUT_PATH }}-workflow [[show-task]] inherit = SHOW script = cylc show "$CYLC_WORKFLOW_ID" --task-def foo >>{{ TEST_OUTPUT_PATH }}-task [[show-taskinstance]] inherit = SHOW script = """ cylc show "$CYLC_WORKFLOW_ID//20141106T0900Z/foo" \ >>{{ TEST_OUTPUT_PATH }}-taskinstance """ [[show-workflow-json]] inherit = SHOW script = """ cylc show --json "$CYLC_WORKFLOW_ID" \ >>{{ TEST_OUTPUT_PATH }}-json-workflow """ [[show-task-json]] inherit = SHOW script = """ cylc show --json "$CYLC_WORKFLOW_ID" --task-def foo \ >>{{ TEST_OUTPUT_PATH }}-json-task """ [[show-taskinstance-json]] inherit = SHOW script = """ cylc show --json "$CYLC_WORKFLOW_ID//20141106T0900Z/foo" \ >>{{ TEST_OUTPUT_PATH }}-json-taskinstance """ cylc-flow-8.6.4/.wci.yml0000664000175000017500000000346415202510242015177 0ustar alastairalastair# name: # will use GitHub repository name if not provided name: Cylc # icon: # will use GitHub organization icon if not provided # headline: # will use GitHub repository description if not provided headline: A general purpose workflow engine with a particular gift for cycling. # description: # will use GitHub repository description if not provided description: Cylc is a decentralised, distributed, DAG/DCG workflow scheduler. # language: # will attempt to fetch GitHub repository main language if not provided language: Python # release: # will attempt to fetch latest release from GitHub repository if not provided # version: # date: # format: YYYY-MM-DD # url: # documentation: # nothing will be used if not provided # general: # installation: # tutorial: documentation: general: https://cylc.github.io/cylc-doc/stable/html/index.html installation: https://cylc.github.io/cylc-doc/stable/html/installation.html tutorial: https://cylc.github.io/cylc-doc/stable/html/tutorial/index.html # social: # nothing will be used if not provided # twitter: # youtube: # execution_environment: # nothing will be used if not provided # interfaces: # list of interfaces # - # - # resource_managers: # list of supported resource managers # - # - resource_managers: # list of supported resource managers - PBS - Slurm - Moab - LSF - atd - background # transfer_protocols: # list of supported transfer protocols # - # - cylc-flow-8.6.4/.codacy.yml0000664000175000017500000000010515202510242015644 0ustar alastairalastairexclude_paths: - 'etc/**' - 'tests/**' - 'cylc/flow/**_pb2.py' cylc-flow-8.6.4/pyproject.toml0000664000175000017500000000241715202510242016525 0ustar alastairalastair[tool.towncrier] directory = "changes.d" name = "Cylc" package = "cylc.flow" filename = "CHANGES.md" template = "changes.d/changelog-template.jinja" underlines = ["", "", ""] title_format = "## __cylc-{version} (Released {project_date})__" issue_format = "[#{issue}](https://github.com/cylc/cylc-flow/pull/{issue})" ignore = ["changelog-template.jinja"] # These changelog sections will be shown in the defined order: [[tool.towncrier.type]] directory = "break" # NB this is just the filename not directory e.g. 123.break.md name = "⚠ Breaking Changes" showcontent = true [[tool.towncrier.type]] directory = "feat" name = "🚀 Enhancements" showcontent = true [[tool.towncrier.type]] directory = "fix" name = "🔧 Fixes" showcontent = true # Not mandated to use these tools, but if you do: [tool.ruff] line-length = 79 target-version = "py312" [tool.ruff.format] quote-style = "preserve" [tool.black] line-length = 79 target-version = ['py312'] skip-string-normalization = true [tool.isort] profile = "black" line_length = 79 force_grid_wrap = 2 lines_after_imports = 2 combine_as_imports = true force_sort_within_sections = true [tool.ruff.lint.isort] # force-grid-wrap = 2 # astral-sh/ruff#2601 lines-after-imports = 2 combine-as-imports = true force-sort-within-sections = true cylc-flow-8.6.4/SECURITY.md0000664000175000017500000000275215202510242015404 0ustar alastairalastair# Security Policies and Procedures This document outlines security procedures and general policies for the Cylc project. * [Reporting a Bug](#reporting-a-bug) * [Disclosure Policy](#disclosure-policy) * [Comments on this Policy](#comments-on-this-policy) ## Reporting a Bug The Cylc maintainers take security bugs seriously. Thank you for improving the security of Cylc. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. Please report security bugs by sending an email to the lead Cylc maintainers, [Hilary Oliver](mailto:hilary.oliver@niwa.co.nz) and [Oliver Sanders](mailto:oliver.sanders@metoffice.gov.uk). If a fix is needed, progress will be recorded on Cylc repository Issue page on GitHub, and resulting new releases will be announced on the Cylc [Discourse forum](https://cylc.discourse.group/). Report security bugs in third-party modules to the person or team maintaining the module. ## Disclosure Policy When the Cylc maintainers receive a security bug report, they will assign it to a primary handler. This person will coordinate the fix and release process as follows: * Confirm the problem and determine the affected versions. * Audit code to find any potential similar problems. * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. ## Comments on this Policy If you have suggestions on how this process could be improved please submit a pull request. cylc-flow-8.6.4/conda-environment.yml0000664000175000017500000000123115202510242017753 0ustar alastairalastairname: cylc-dev channels: - conda-forge dependencies: - ansimarkup >=1.0.0 - colorama >=0.4,<1.0 - graphql-core >=3.2,<3.3 - graphene >=3.4.0,<3.5 - graphviz # for static graphing # Note: can't pin jinja2 any higher than this until we give up on Cylc 7 back-compat - jinja2 >=3.0,<3.1 - metomi-isodatetime >=1!3.0.0, <1!3.2.0 - packaging # Constrain protobuf version for compatible Scheduler-UIS comms across hosts - protobuf >=4.24.4,<4.25.0 - psutil >=5.6.0 - python - pyzmq >=22 - urwid >=2.2,<4,!=2.6.2,!=2.6.3 # optional dependencies #- pandas >=1.0,<2 #- pympler #- matplotlib-base #- sqlparse #- h5py #- requests cylc-flow-8.6.4/doc/0000775000175000017500000000000015202510242014352 5ustar alastairalastaircylc-flow-8.6.4/doc/README.md0000664000175000017500000000010615202510242015626 0ustar alastairalastairThe Cylc User Guide has been moved to the `cylc/cylc-doc` repository. cylc-flow-8.6.4/changes.d/0000775000175000017500000000000015202510242015437 5ustar alastairalastaircylc-flow-8.6.4/changes.d/changelog-template.jinja0000664000175000017500000000046115202510242022215 0ustar alastairalastair{% if sections[""] %} {% for category, val in definitions.items() if category in sections[""] %} ### {{ definitions[category]['name'] }} {% for text, pulls in sections[""][category].items() %} {{ pulls|join(', ') }} - {{ text }} {% endfor %} {% endfor %} {% else %} No significant changes. {% endif %} cylc-flow-8.6.4/cylc/0000775000175000017500000000000015202510242014537 5ustar alastairalastaircylc-flow-8.6.4/cylc/flow/0000775000175000017500000000000015202510242015506 5ustar alastairalastaircylc-flow-8.6.4/cylc/flow/task_pool.py0000664000175000017500000027752315202510242020073 0ustar alastairalastair# 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 . """Wrangle task proxies to manage the workflow.""" from collections import Counter from contextlib import suppress import json import logging from textwrap import indent from typing import ( TYPE_CHECKING, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple, Type, Union, ) from cylc.flow import LOG from cylc.flow.cycling.loader import ( get_point, standardise_point_string, ) from cylc.flow.exceptions import ( PlatformLookupError, PointParsingError, WorkflowConfigError, ) import cylc.flow.flags from cylc.flow.flow_mgr import ( FLOW_NONE, repr_flow_nums, ) from cylc.flow.id import ( TaskTokens, Tokens, quick_relative_id, ) from cylc.flow.id_match import id_match from cylc.flow.platforms import get_platform from cylc.flow.prerequisite import PrereqTuple from cylc.flow.run_modes import RunMode from cylc.flow.run_modes.skip import process_outputs as get_skip_mode_outputs from cylc.flow.task_action_timer import ( TaskActionTimer, TimerFlags, ) from cylc.flow.task_events_mgr import ( CustomTaskEventHandlerContext, EventKey, TaskEventMailContext, TaskJobLogsRetrieveContext, ) from cylc.flow.task_id import TaskID from cylc.flow.task_outputs import ( TASK_OUTPUT_EXPIRED, TASK_OUTPUT_FAILED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.task_proxy import TaskProxy from cylc.flow.task_queues.independent import IndepQueueManager from cylc.flow.task_state import ( TASK_STATUS_EXPIRED, TASK_STATUS_FAILED, TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_SUBMITTED, TASK_STATUS_SUCCEEDED, TASK_STATUS_WAITING, TASK_STATUSES_ACTIVE, TASK_STATUSES_FINAL, status_geq, ) from cylc.flow.task_trigger import TaskTrigger from cylc.flow.util import deserialise_set from cylc.flow.workflow_status import StopMode from cylc.flow.scripts.set import XTRIGGER_PREREQ_PREFIX if TYPE_CHECKING: from cylc.flow.config import WorkflowConfig from cylc.flow.cycling import ( IntervalBase, PointBase, ) from cylc.flow.data_store_mgr import DataStoreMgr from cylc.flow.flow_mgr import ( FlowMgr, FlowNums, ) from cylc.flow.prerequisite import SatisfiedState from cylc.flow.task_events_mgr import TaskEventsManager from cylc.flow.taskdef import TaskDef from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager from cylc.flow.xtrigger_mgr import XtriggerManager Pool = Dict['PointBase', Dict[str, TaskProxy]] def _get_xtrig_prereqs( prereqs: 'Iterable[str]' ) -> 'Dict[str, bool]': """Extract xtriggers from user prerequisite input. Weed out any task prerequisites. Command validation has handled output suffixes and defaults. Args: prereqs: prerequisites and xtriggers in string form: /: xtrigger/[:succeeded] xtrigger/all[:succeeded] Returns: { or "all": satisfied} Examples: >>> _get_xtrig_prereqs(["1/foo:started"]) {} >>> _get_xtrig_prereqs({"1/foo:started", "xtrigger/x1:succeeded"}) {'x1': True} (No need to test "all" - it just looks like an xtrigger label.) """ _xtrigs = {} for prereq in prereqs: pre = Tokens(prereq, relative=True) if pre['cycle'] != XTRIGGER_PREREQ_PREFIX: # weed out task prerequisites continue # requested state to set: _xtrigs[pre['task']] = (pre['task_sel'] == TASK_OUTPUT_SUCCEEDED) return _xtrigs class TaskPool: """Task pool of a workflow.""" ERR_TMPL_NO_TASKID_MATCH = "No matching tasks found: {0}" ERR_PREFIX_TASK_NOT_ON_SEQUENCE = "Invalid cycle point for task: {0}, {1}" SUICIDE_MSG = "suicide trigger" REMOVED_BY_PREREQ = "prerequisite task(s) removed" def __init__( self, tokens: 'Tokens', config: 'WorkflowConfig', workflow_db_mgr: 'WorkflowDatabaseManager', task_events_mgr: 'TaskEventsManager', xtrigger_mgr: 'XtriggerManager', data_store_mgr: 'DataStoreMgr', flow_mgr: 'FlowMgr' ) -> None: self.tokens = tokens self.config: 'WorkflowConfig' = config self.stop_point = config.stop_point or config.final_point self.workflow_db_mgr: 'WorkflowDatabaseManager' = workflow_db_mgr self.task_events_mgr: 'TaskEventsManager' = task_events_mgr self.task_events_mgr.spawn_func = self.spawn_on_output self.xtrigger_mgr: 'XtriggerManager' = xtrigger_mgr self.xtrigger_mgr.add_xtriggers(self.config.xtrigger_collator) self.data_store_mgr: 'DataStoreMgr' = data_store_mgr self.flow_mgr: 'FlowMgr' = flow_mgr self.max_future_offset: Optional['IntervalBase'] = None self._prev_runahead_base_point: Optional['PointBase'] = None self._prev_runahead_sequence_points: Optional[Set['PointBase']] = None self.runahead_limit_point: Optional['PointBase'] = None # Tasks in the active window of the workflow. self.active_tasks: Pool = {} self._active_tasks_list: List[TaskProxy] = [] self.active_tasks_changed = False self.tasks_removed = False self.hold_point: Optional['PointBase'] = None self.abs_outputs_done: Set[Tuple[str, str, str]] = set() self.stop_task_id: Optional[str] = None self.stop_task_finished = False self.abort_task_failed = False self.expected_failed_tasks = self.config.get_expected_failed_tasks() self.task_name_list = self.config.get_task_name_list() self.task_queue_mgr = IndepQueueManager( self.config.cfg['scheduling']['queues'], self.task_name_list, self.config.runtime['descendants'] ) self.tasks_to_hold: set[tuple[str, 'PointBase']] = set() self.tasks_to_trigger_now: set['TaskProxy'] = set() self.pre_start_tasks_to_trigger: set[tuple[str, 'PointBase']] = set() def set_stop_task(self, task_id): """Set stop after a task.""" tokens = Tokens(task_id, relative=True) name = tokens['task'] if name in self.config.taskdefs: task_id = TaskID.get_standardised_taskid(task_id) LOG.info("Setting stop task: " + task_id) self.stop_task_id = task_id self.stop_task_finished = False self.workflow_db_mgr.put_workflow_stop_task(task_id) else: LOG.warning("Requested stop task name does not exist: %s" % name) def stop_task_done(self): """Return True if stop task has succeeded.""" if self.stop_task_id is not None and self.stop_task_finished: LOG.info("Stop task %s finished" % self.stop_task_id) self.stop_task_id = None self.stop_task_finished = False self.workflow_db_mgr.put_workflow_stop_task(None) return True return False def _swap_out(self, itask): """Swap old task for new, during reload.""" if itask.identity in self.active_tasks.get(itask.point, set()): self.active_tasks[itask.point][itask.identity] = itask self.active_tasks_changed = True def load_from_point(self): """Load the task pool for the workflow start point. Add every parentless task out to the runahead limit. """ flow_num = self.flow_mgr.get_flow( meta=f"original flow from {self.config.start_point}") self.compute_runahead() for name in self.task_name_list: tdef = self.config.get_taskdef(name) point = tdef.first_point(self.config.start_point) self.spawn_to_rh_limit(tdef, point, {flow_num}) def db_add_new_flow_rows(self, itask: TaskProxy) -> None: """Add new rows to DB task tables that record flow_nums. Call when a new task is spawned or a flow merge occurs. """ # Add row to task_states table. self.workflow_db_mgr.put_insert_task_states(itask) # Add row to task_outputs table: self.workflow_db_mgr.put_insert_task_outputs(itask) def add_to_pool(self, itask) -> None: """Add a task to the pool.""" self.active_tasks.setdefault(itask.point, {}) if itask.identity in self.active_tasks[itask.point]: # If logged, something has gone wrong. LOG.debug(f"{itask.identity} not added to n=0: already exists") return None self.active_tasks[itask.point][itask.identity] = itask self.active_tasks_changed = True LOG.debug(f"[{itask}] added to the n=0 window") self.create_data_store_elements(itask) if itask.tdef.max_future_prereq_offset is not None: # (Must do this once added to the pool). self.set_max_future_offset() def create_data_store_elements(self, itask): """Create the node window elements about given task proxy.""" # Register pool node reference self.data_store_mgr.add_pool_node(itask.tdef.name, itask.point) # Create new data-store n-distance graph window about this task self.data_store_mgr.increment_graph_window( itask.tokens, itask.point, is_manual_submit=itask.is_manual_submit, itask=itask ) self.data_store_mgr.delta_task_state(itask) def release_runahead_tasks(self): """Release tasks below the runahead limit. Return True if any tasks are released, else False. Call when RH limit changes. """ if not self.active_tasks or not self.runahead_limit_point: # (At start-up task pool might not exist yet) return False released = False # An intermediate list is needed here: auto-spawning of parentless # tasks can cause the task pool to change size during iteration. release_me = [ itask for point, itask_id_map in self.active_tasks.items() for itask in itask_id_map.values() if point <= self.runahead_limit_point if itask.state.is_runahead ] for itask in release_me: self.rh_release_and_queue(itask) if itask.flow_nums and not itask.is_xtrigger_sequential: self.spawn_to_rh_limit( itask.tdef, itask.tdef.next_point(itask.point), itask.flow_nums ) released = True return released def compute_runahead(self, force=False) -> bool: """Compute the runahead limit; return True if it changed. To be called if: * The runahead base point might have changed: - a task completed expected outputs, or expired - (Cylc7 back compat: a task succeeded or failed) * The max future offset might have changed. * The runahead limit config or task pool might have changed (reload). This is a collective task pool computation. Call it once at the end of a group operation such as removal of multiple tasks (not after every individual task operation). Start from earliest point with unfinished tasks. Partially satisfied and incomplete tasks count too because they still need to run. The limit itself is limited by workflow stop point, if there is one, and adjusted upward on the fly if tasks with future offsets appear. With force=True we recompute the limit even if the base point has not changed (needed if max_future_offset changed, or on reload). """ limit = self.config.runahead_limit # e.g. P2 or P2D count_cycles = False with suppress(TypeError): # Count cycles (integer cycling, and optional for datetime too). ilimit = int(limit) # type: ignore count_cycles = True base_point: Optional['PointBase'] = None # First get the runahead base point. if not self.active_tasks: # Find the earliest sequence point beyond the workflow start point. base_point = min( ( point for point in { seq.get_first_point(self.config.start_point) for seq in self.config.sequences } if point is not None ), default=None, ) else: # Find the earliest point with incomplete tasks. for point, itasks in sorted(self.get_tasks_by_point().items()): # All n=0 tasks are incomplete by definition, but Cylc 7 # ignores failed ones (it does not ignore submit-failed!). if ( cylc.flow.flags.cylc7_back_compat and all( itask.state(TASK_STATUS_FAILED) for itask in itasks ) ): continue base_point = point break if base_point is None: return False LOG.debug(f"Runahead: base point {base_point}") if self._prev_runahead_base_point is None: self._prev_runahead_base_point = base_point if ( not force and self.runahead_limit_point is not None and ( base_point == self._prev_runahead_base_point or self.runahead_limit_point == self.stop_point ) ): # No need to recompute the list of points if the base point did not # change or the runahead limit is already at stop point. return False # Now generate all possible cycle points from the base point and stop # at the runahead limit point. Note both cycle count and time interval # limits involve all possible cycles, not just active cycles. sequence_points: Set['PointBase'] = set() if ( not force and self._prev_runahead_sequence_points and base_point == self._prev_runahead_base_point ): # Cache for speed. sequence_points = self._prev_runahead_sequence_points else: # Recompute possible points. for sequence in self.config.sequences: seq_point = sequence.get_first_point(base_point) count = 1 while seq_point is not None: if count_cycles: # P0 allows only the base cycle point to run. if count > 1 + ilimit: # this point may be beyond the runahead limit break else: # PT0H allows only the base cycle point to run. if seq_point > base_point + limit: # this point can not be beyond the runahead limit break count += 1 sequence_points.add(seq_point) seq_point = sequence.get_next_point(seq_point) self._prev_runahead_sequence_points = sequence_points self._prev_runahead_base_point = base_point if not sequence_points: limit_point = base_point elif count_cycles: # (len(list) may be less than ilimit due to sequence end) limit_point = sorted(sequence_points)[:ilimit + 1][-1] else: limit_point = max(sequence_points) # Adjust for future offset and stop point. pre_adj_limit = limit_point if self.max_future_offset is not None: limit_point += self.max_future_offset LOG.debug( "Runahead (future trigger adjust):" f" {pre_adj_limit} -> {limit_point}" ) if self.stop_point and limit_point > self.stop_point: limit_point = self.stop_point LOG.debug( "Runahead (stop point adjust):" f" {pre_adj_limit} -> {limit_point} (stop point)" ) LOG.debug(f"Runahead limit: {limit_point}") self.runahead_limit_point = limit_point return True def update_flow_mgr(self): flow_nums_seen = set() for itask in self.get_tasks(): flow_nums_seen.update(itask.flow_nums) self.flow_mgr.load_from_db(flow_nums_seen) def load_abs_outputs_for_restart(self, row_idx, row): cycle, name, output = row self.abs_outputs_done.add((cycle, name, output)) def check_task_output( self, cycle: str, task: str, output_msg: str, flow_nums: 'FlowNums', ) -> 'SatisfiedState': """Returns truthy if the specified output is satisfied in the DB. Args: cycle: Cycle point of the task whose output is being checked. task: Name of the task whose output is being checked. output_msg: The output message to check for. flow_nums: Flow numbers of the task whose output is being checked. If this is empty it means 'none'; will return False. """ if not flow_nums: return False for task_outputs, task_flow_nums in ( self.workflow_db_mgr.pri_dao.select_task_outputs(task, cycle) ).items(): # loop through matching tasks # (if task_flow_nums is empty, it means the 'none' flow) if flow_nums.intersection(task_flow_nums): # BACK COMPAT: In Cylc >8.0.0,<8.3.0, only the task # messages were stored in the DB as a list. # from: 8.0.0 # to: 8.3.0 outputs: Union[ Dict[str, str], List[str] ] = json.loads(task_outputs) messages = ( outputs.values() if isinstance(outputs, dict) else outputs ) return ( 'satisfied from database' if output_msg in messages else False ) else: # no matching entries return False def load_db_task_pool_for_restart(self, row_idx, row): """Load tasks from DB task pool/states/jobs tables. Output completion status is loaded from the DB, and tasks recorded as submitted or running are polled to confirm their true status. Tasks are added to queues again on release from runahead pool. Returns: Names of platform if attempting to look up that platform has led to a PlatformNotFoundError. """ if row_idx == 0: LOG.info("LOADING task proxies") # Create a task proxy corresponding to this DB entry. (cycle, name, flow_nums, flow_wait, is_manual_submit, is_late, status, is_held, submit_num, _, platform_name, time_submit, time_run, timeout, outputs_str) = row try: itask = TaskProxy( self.tokens, self.config.get_taskdef(name), get_point(cycle), deserialise_set(flow_nums), status=status, is_held=is_held, submit_num=submit_num, is_late=bool(is_late), flow_wait=bool(flow_wait), is_manual_submit=bool(is_manual_submit), sequential_xtrigger_labels=( self.xtrigger_mgr.xtriggers.sequential_xtrigger_labels ), ) except WorkflowConfigError: LOG.exception( f'ignoring task {name} from the workflow run database\n' '(its task definition has probably been deleted).') except Exception: LOG.exception(f'could not load task {name}') else: if status in ( TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING, TASK_STATUS_FAILED, TASK_STATUS_SUCCEEDED ): # update the task proxy with platform # If we get a failure from the platform selection function # set task status to submit-failed. try: itask.platform = get_platform(platform_name) except PlatformLookupError: return platform_name if time_submit: itask.set_summary_time('submitted', time_submit) if time_run: itask.set_summary_time('started', time_run) if timeout is not None: itask.timeout = timeout elif status == TASK_STATUS_PREPARING: # put back to be readied again. status = TASK_STATUS_WAITING # Re-prepare same submit. itask.submit_num -= 1 # Running or finished task can have completed custom outputs. if itask.state( TASK_STATUS_RUNNING, TASK_STATUS_FAILED, TASK_STATUS_SUCCEEDED ): for message in json.loads(outputs_str): itask.state.outputs.set_message_complete(message) self.data_store_mgr.delta_task_output(itask, message) if platform_name and status != TASK_STATUS_WAITING: itask.summary['platforms_used'][ int(submit_num)] = platform_name LOG.info( f"+ {cycle}/{name} {status}{' (held)' if is_held else ''}") # Update prerequisite satisfaction status from DB sat = {} for prereq_name, prereq_cycle, prereq_output_msg, satisfied in ( self.workflow_db_mgr.pri_dao.select_task_prerequisites( cycle, name, flow_nums, ) ): # Prereq satisfaction as recorded in the DB. sat[ (prereq_cycle, prereq_name, prereq_output_msg) ] = satisfied if satisfied != '0' else False for itask_prereq in itask.state.prerequisites: for key in itask_prereq: if key in sat: itask_prereq[key] = sat[key] else: # This prereq is not in the DB: new dependencies # added to an already-spawned task before restart. # Look through task outputs to see if is has been # satisfied prereq_cycle, prereq_task, prereq_output_msg = key itask_prereq[key] = ( self.check_task_output( prereq_cycle, prereq_task, prereq_output_msg, itask.flow_nums, ) ) for xtrigger_label in itask.state.xtriggers: if ("xtrigger", xtrigger_label, TASK_OUTPUT_SUCCEEDED) in sat: itask.state.xtriggers[xtrigger_label] = True if itask.state_reset(status, is_runahead=True): self.data_store_mgr.delta_task_state(itask) self.add_to_pool(itask) # All tasks load as runahead-limited, but finished and manually # triggered tasks (incl. --start-task's) can be released now. if ( itask.state( TASK_STATUS_FAILED, TASK_STATUS_SUCCEEDED, TASK_STATUS_EXPIRED ) or itask.is_manual_submit ): self.rh_release_and_queue(itask) def load_db_task_action_timers(self, row_idx: int, row: Iterable) -> None: """Load a task action timer, e.g. event handlers, retry states.""" if row_idx == 0: LOG.debug("LOADING task action timers") (cycle, name, ctx_key_raw, ctx_raw, delays_raw, num, delay, timeout) = row tokens = Tokens( cycle=cycle, task=name, ) id_ = tokens.relative_id try: # Extract type namedtuple variables from JSON strings ctx_key = json.loads(str(ctx_key_raw)) ctx_data = json.loads(str(ctx_raw)) known_cls: Type[NamedTuple] for known_cls in ( CustomTaskEventHandlerContext, TaskEventMailContext, TaskJobLogsRetrieveContext ): if ctx_data and ctx_data[0] == known_cls.__name__: ctx_args: list = ctx_data[1] if len(ctx_args) > len(known_cls._fields): # BACK COMPAT: no-longer used ctx_type arg # from: Cylc 7 # to: 8.3.0 ctx_args.pop(1) ctx: tuple = known_cls(*ctx_args) break else: # no break ctx = ctx_data if ctx is not None: ctx = tuple(ctx) delays = json.loads(str(delays_raw)) except ValueError: LOG.exception( "%(id)s: skip action timer %(ctx_key)s" % {"id": id_, "ctx_key": ctx_key_raw}) return LOG.debug("+ %s/%s %s" % (cycle, name, ctx_key)) if ctx_key == "poll_timer": itask = self._get_task_by_id(id_) if itask is None: return itask.poll_timer = TaskActionTimer( ctx, delays, num, delay, timeout) elif ctx_key[0] == "try_timers": itask = self._get_task_by_id(id_) if itask is None: return if 'retrying' in ctx_key[1]: if 'submit' in ctx_key[1]: submit = True ctx_key[1] = TimerFlags.SUBMISSION_RETRY else: submit = False ctx_key[1] = TimerFlags.EXECUTION_RETRY if timeout: LOG.debug( f' (upgrading retrying state for {itask.identity})') self.task_events_mgr._retry_task( itask, float(timeout), submit_retry=submit ) itask.try_timers[ctx_key[1]] = TaskActionTimer( ctx, delays, num, delay, timeout) elif ctx: (handler, event), submit_num = ctx_key self.task_events_mgr.add_event_timer( EventKey( handler, event, # NOTE: the event "message" is not preserved in the DB so # we use the event as a placeholder event, tokens.duplicate(job=submit_num), ), TaskActionTimer( ctx, delays, num, delay, timeout ) ) else: LOG.exception( "%(id)s: skip action timer %(ctx_key)s" % {"id": id_, "ctx_key": ctx_key_raw}) return def load_db_tasks_to_hold(self): """Update the tasks_to_hold set with the tasks stored in the database.""" self.tasks_to_hold.update( (name, get_point(cycle)) for name, cycle in self.workflow_db_mgr.pri_dao.select_tasks_to_hold() ) def rh_release_and_queue(self, itask) -> None: """Release a task from runahead limiting, and queue it if ready. Check the task against the RH limit before calling this method (in forced triggering we need to release even if beyond the limit). """ if itask.state_reset(is_runahead=False): self.data_store_mgr.delta_task_state(itask) if itask.is_ready_to_run(): # (otherwise waiting on xtriggers etc.) self.queue_task(itask) def get_or_spawn_task( self, point: 'PointBase', tdef: 'TaskDef', flow_nums: 'FlowNums', flow_wait: bool = False ) -> 'Tuple[Optional[TaskProxy], bool, bool]': """Return new or existing task point/name with merged flow_nums. Returns: tuple - (itask, is_in_pool, is_xtrig_sequential) itask: The requested task proxy, or None if task does not exist or cannot spawn. is_in_pool: Was the task found in a pool. is_xtrig_sequential: Is the next task occurrence spawned on xtrigger satisfaction, or do all occurrence spawn out to the runahead limit. It does not add a spawned task proxy to the pool. """ ntask = self.get_task(point, tdef.name) is_in_pool = False is_xtrig_sequential = False if ntask is None: # ntask does not exist: spawn it in the flow. ntask = self.spawn_task( tdef.name, point, flow_nums, flow_wait=flow_wait ) # if the task was found set xtrigger checking type. # otherwise find the xtrigger type if it can't spawn # for whatever reason. if ntask is not None: is_xtrig_sequential = ntask.is_xtrigger_sequential elif any( xtrig_label in ( self.xtrigger_mgr.xtriggers.sequential_xtrigger_labels) for sequence, xtrig_labels in tdef.xtrig_labels.items() for xtrig_label in xtrig_labels if sequence.is_valid(point) ): is_xtrig_sequential = True else: # ntask already exists (n=0): merge flows. is_in_pool = True self.merge_flows(ntask, flow_nums) is_xtrig_sequential = ntask.is_xtrigger_sequential # ntask may still be None return ntask, is_in_pool, is_xtrig_sequential def spawn_to_rh_limit( self, tdef: 'TaskDef', point: Optional['PointBase'], flow_nums: 'FlowNums', ) -> None: """Spawn parentless task instances from point to runahead limit. Sequentially checked xtriggers will spawn the next occurrence of their corresponding tasks. These tasks will keep spawning until they depend on any unsatisfied xtrigger of the same sequential behavior, are no longer parentless, and/or hit the runahead limit. """ if ( not flow_nums # Force-triggered no-flow task or point is None # Reached end of sequence? or point < self.config.start_point # Warm start ): return if self.runahead_limit_point is None: self.compute_runahead() if self.runahead_limit_point is None: return is_xtrig_sequential = False while point is not None and (point <= self.runahead_limit_point): if tdef.is_parentless(point, cutoff=self.config.start_point): ntask, is_in_pool, is_xtrig_sequential = ( self.get_or_spawn_task(point, tdef, flow_nums) ) if ntask is not None: if not is_in_pool: self.add_to_pool(ntask) self.rh_release_and_queue(ntask) if is_xtrig_sequential: break point = tdef.next_point(point) # Once more for the runahead-limited task (don't release it). if not is_xtrig_sequential: self.spawn_if_parentless(tdef, point, flow_nums) def spawn_if_parentless(self, tdef, point, flow_nums): """Spawn a task if parentless, regardless of runahead limit.""" if ( flow_nums and point is not None and tdef.is_parentless(point, cutoff=self.config.start_point) ): ntask, is_in_pool, _ = self.get_or_spawn_task( point, tdef, flow_nums ) if ntask is not None and not is_in_pool: self.add_to_pool(ntask) def remove(self, itask: 'TaskProxy', reason: Optional[str] = None) -> None: """Remove a task from the pool.""" # the held state is no longer relevant -> remove it self.release_held_active_task(itask) # xtriggers are no longer relevant -> remove them self.xtrigger_mgr.force_satisfy_all(itask, log=False) if itask.state.is_runahead and itask.flow_nums: # If removing a parentless runahead-limited task # auto-spawn its next instance first. self.spawn_if_parentless( itask.tdef, itask.tdef.next_point(itask.point), itask.flow_nums ) msg = f"removed from the n=0 window: {reason or 'completed'}" # Mark as transient in case itask is still processed in other contexts. itask.transient = True if itask.is_xtrigger_sequential: self.xtrigger_mgr.sequential_has_spawned_next.discard( itask.identity ) try: del self.active_tasks[itask.point][itask.identity] except KeyError: pass else: self.tasks_to_trigger_now.discard(itask) self.pre_start_tasks_to_trigger.discard( (itask.tdef.name, itask.point) ) self.tasks_removed = True self.active_tasks_changed = True if not self.active_tasks[itask.point]: del self.active_tasks[itask.point] self.task_queue_mgr.remove_task(itask) if itask.tdef.max_future_prereq_offset is not None: self.set_max_future_offset() # Notify the data-store manager of their removal # (the manager uses window boundary tracking for pruning). self.data_store_mgr.remove_pool_node(itask.tdef.name, itask.point) # Event-driven final update of task_states table. # TODO: same for datastore (still updated by scheduler loop) self.workflow_db_mgr.put_update_task_state(itask) level = logging.DEBUG if itask.state( TASK_STATUS_PREPARING, TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING, ): level = logging.WARNING msg += " - active job orphaned" elif reason == self.REMOVED_BY_PREREQ: level = logging.INFO LOG.log(level, f"[{itask}] {msg}") # ensure this task is written to the DB before moving on # https://github.com/cylc/cylc-flow/issues/6315 self.workflow_db_mgr.process_queued_ops() del itask # removing this task could nudge the runahead limit forward if self.compute_runahead(): self.release_runahead_tasks() def get_tasks(self) -> List[TaskProxy]: """Return a list of task proxies in the task pool.""" # Cached list only for use internally in this method. if self.active_tasks_changed: self.active_tasks_changed = False self._active_tasks_list = [ itask for itask_id_map in self.active_tasks.values() for itask in itask_id_map.values() ] return self._active_tasks_list def get_task_ids(self) -> Set[str]: """Return a list of task IDs in the task pool.""" return {itask.identity for itask in self.get_tasks()} def get_tasks_by_point(self) -> 'Dict[PointBase, List[TaskProxy]]': """Return a map of task proxies by cycle point.""" return { point: list(itask_id_map.values()) for point, itask_id_map in self.active_tasks.items() } def get_task(self, point: 'PointBase', name: str) -> Optional[TaskProxy]: """Retrieve a task from the pool.""" rel_id = f'{point}/{name}' tasks = self.active_tasks.get(point) if tasks: return tasks.get(rel_id) return None def _get_task_by_id(self, id_: str) -> Optional[TaskProxy]: """Return pool task by ID if it exists, or None.""" for itask_ids in self.active_tasks.values(): if id_ in itask_ids: return itask_ids[id_] return None def get_itasks(self, ids: 'Iterable[Tokens]') -> List[TaskProxy]: """Return a list of itasks matching the IDs provided. Args: ids: The exact IDs to match (no globs, families, etc supported). Returns: A list of an active tasks matching these IDs. """ return [ itasks[id_] for itasks in self.active_tasks.values() for id_ in itasks.keys() & {id_.relative_id for id_ in ids} ] def queue_task(self, itask: TaskProxy) -> None: """Queue a task that is ready to run. If it is already queued, do nothing. """ if itask.state_reset(is_queued=True): self.data_store_mgr.delta_task_state(itask) self.task_queue_mgr.push_task(itask) def unqueue_task(self, itask: TaskProxy) -> None: """Un-queue a task that is no longer ready to run. If it is not queued, do nothing. """ if itask.state_reset(is_queued=False): self.data_store_mgr.delta_task_state(itask) self.task_queue_mgr.remove_task(itask) def count_active_tasks(self): """Count active tasks and identify pre-prep tasks.""" # count active tasks by name # {task_name: number_of_active_instances, ...} active_task_counter = Counter() # tasks which have entered the submission pipeline but have not yet # entered the PREPARING state pre_prep_tasks = [] for itask in self.get_tasks(): # populate active_task_counter and pre_prep_tasks together to # avoid iterating the task pool twice if itask.waiting_on_job_prep: # a task which has entered the submission pipeline # for the purposes of queue limiting this should be treated # the same as an active task active_task_counter.update([itask.tdef.name]) pre_prep_tasks.append(itask) elif itask.state( TASK_STATUS_PREPARING, TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING, ): # an active task active_task_counter.update([itask.tdef.name]) return active_task_counter, pre_prep_tasks def release_queued_tasks(self) -> set['TaskProxy']: """Return list of queue-released tasks awaiting job prep. Note: Tasks can hang about for a while between being released and entering the PREPARING state for various reasons. This method returns tasks which are awaiting job prep irrespective of whether they have been previously returned. """ active_task_counter, pre_prep_tasks = self.count_active_tasks() # release queued tasks released = self.task_queue_mgr.release_tasks(active_task_counter) for itask in released: itask.state_reset(is_queued=False) self.data_store_mgr.delta_task_state(itask) itask.waiting_on_job_prep = True if cylc.flow.flags.cylc7_back_compat: # Cylc 7 Back Compat: spawn downstream to cause Cylc 7 style # stalls - with unsatisfied waiting tasks - even with single # prerequisites (which result in incomplete tasks in Cylc 8). # We do it here (rather than at runhead release) to avoid # pre-spawning out to the runahead limit. self.spawn_on_all_outputs(itask) # Note: released and pre_prep_tasks can overlap return set(released + pre_prep_tasks) def get_min_point(self): """Return the minimum cycle point currently in the pool.""" cycles = list(self.active_tasks) minc = None if cycles: minc = min(cycles) return minc def set_max_future_offset(self): """Calculate the latest required future trigger offset.""" orig = self.max_future_offset max_offset = None for itask in self.get_tasks(): if ( itask.tdef.max_future_prereq_offset is not None and ( max_offset is None or itask.tdef.max_future_prereq_offset > max_offset ) ): max_offset = itask.tdef.max_future_prereq_offset self.max_future_offset = max_offset if max_offset != orig and self.compute_runahead(force=True): self.release_runahead_tasks() def reload(self, config: 'WorkflowConfig') -> None: self.config = config # store the updated config self.xtrigger_mgr.add_xtriggers( self.config.xtrigger_collator, reload=True) self._reload_taskdefs() def _reload_taskdefs(self) -> None: """Reload the definitions of task proxies in the pool. Orphaned tasks (whose definitions were removed from the workflow): - remove if not active yet - if active, leave them but prevent them from spawning children on subsequent outputs Otherwise: replace task definitions but copy over existing outputs etc. self.config should already be updated for the reload. """ self.stop_point = self.config.stop_point or self.config.final_point # find any old tasks that have been removed from the workflow old_task_name_list = self.task_name_list self.task_name_list = self.config.get_task_name_list() orphans = [ task for task in old_task_name_list if task not in self.task_name_list ] # adjust the new workflow config to handle the orphans self.config.adopt_orphans(orphans) LOG.info("Reloading task definitions.") tasks = self.get_tasks() # Log tasks orphaned by a reload but not currently in the task pool. for name in orphans: if name not in (itask.tdef.name for itask in tasks): LOG.info("Removed task: '%s'", name) # Store lists of tasks which were active before reload. warn_tasks: List[str] = [] _warn_tasks: List[str] = [] for itask in tasks: if itask.tdef.name in orphans: if ( itask.state(TASK_STATUS_WAITING) or itask.state.is_held or itask.state.is_queued ): # Remove orphaned task if it hasn't started running yet. self.remove(itask, 'task definition removed') else: # Keep active orphaned task, but stop it from spawning. itask.graph_children = {} LOG.info( f"[{itask}] will not spawn children " "- task definition removed" ) else: new_task = TaskProxy( self.tokens, self.config.get_taskdef(itask.tdef.name), itask.point, itask.flow_nums, itask.state.status, sequential_xtrigger_labels=( self.xtrigger_mgr.xtriggers.sequential_xtrigger_labels ), ) itask.copy_to_reload_successor( new_task, self.check_task_output, ) self._swap_out(new_task) self.data_store_mgr.delta_task_prerequisite(new_task) LOG.info(f"[{itask}] reloaded task definition") if itask.state(*TASK_STATUSES_ACTIVE): warn_tasks.append(str(itask)) elif itask.state(TASK_STATUS_PREPARING): # Job file might have been written at this point? _warn_tasks.append(str(itask)) for may, tasks in (('', warn_tasks), ('may be', _warn_tasks)): if tasks: _tasks = "\n * ".join(tasks) LOG.info( f"Tasks {may} active with pre-reload settings:\n{_tasks}") # Reassign live tasks to the internal queue del self.task_queue_mgr self.task_queue_mgr = IndepQueueManager( self.config.cfg['scheduling']['queues'], self.task_name_list, self.config.runtime['descendants'] ) if self.compute_runahead(): self.release_runahead_tasks() # Now queue all tasks that are ready to run for itask in self.get_tasks(): # Recreate data store elements from task pool. self.create_data_store_elements(itask) if itask.state.is_queued: # Already queued continue if itask.is_ready_to_run() and not itask.state.is_runahead: self.queue_task(itask) def set_stop_point(self, stop_point: 'PointBase') -> bool: """Set the workflow stop cycle point. And reset the runahead limit if less than the stop point. """ if self.stop_point == stop_point: LOG.info(f"Stop point unchanged: {stop_point}") return False LOG.info(f"Setting stop point: {stop_point}") self.stop_point = stop_point if ( self.runahead_limit_point is not None and self.runahead_limit_point > stop_point ): self.runahead_limit_point = stop_point # Now handle existing waiting tasks (e.g. xtriggered). for itask in self.get_tasks(): if ( itask.point > stop_point and itask.state(TASK_STATUS_WAITING) and itask.state_reset(is_runahead=True) ): self.data_store_mgr.delta_task_state(itask) return True def can_stop(self, stop_mode): """Return True if workflow can stop. A task is considered active if: * It is in the active state and not marked with a kill failure. * It has pending event handlers. """ if stop_mode is None: return False if stop_mode == StopMode.REQUEST_NOW_NOW: return True if self.task_events_mgr._event_timers: return False return not any( ( stop_mode in [StopMode.REQUEST_CLEAN, StopMode.REQUEST_KILL] and itask.state(*TASK_STATUSES_ACTIVE) and not itask.state.kill_failed ) # preparing tasks get reset to waiting on restart for itask in self.get_tasks() ) def warn_stop_orphans(self) -> None: """Log (warning) orphaned tasks on workflow stop.""" orphans = [] orphans_kill_failed = [] for itask in self.get_tasks(): if itask.state(*TASK_STATUSES_ACTIVE): if itask.state.kill_failed: orphans_kill_failed.append(itask) else: orphans.append(itask) for orphanlist, extra_text in ( (orphans_kill_failed, ' (kill failed)'), (orphans, '') ): if orphanlist: LOG.warning( f"Orphaned tasks{extra_text}:\n" + "\n".join( f"* {itask.identity} ({itask.state.status})" for itask in orphanlist) ) for id_key in self.task_events_mgr._event_timers: LOG.warning( f"{id_key.tokens.relative_id}:" " incomplete task event handler" f" {(id_key.handler, id_key.event)}" ) def log_incomplete_tasks(self) -> bool: """Log finished but incomplete tasks; return True if there any.""" incomplete = [] for itask in self.get_tasks(): if not itask.state(*TASK_STATUSES_FINAL): continue if not itask.state.outputs.is_complete(): incomplete.append( ( itask.identity, itask.state.outputs.format_completion_status( ansimarkup=1 ), ) ) if incomplete: LOG.error( "Incomplete tasks:\n" + "\n".join( f"* {id_} did not complete the required outputs:" f"\n{indent(outputs, ' ')}" for id_, outputs in incomplete ) ) return True return False def log_unsatisfied_prereqs(self) -> bool: """Log unsatisfied prerequisites in the pool. Return True if any, ignoring: - prerequisites beyond the stop point - dependence on tasks beyond the stop point (can be caused by future triggers) """ unsat: Dict[str, List[str]] = {} for itask in self.get_tasks(): task_point = itask.point if self.stop_point and task_point > self.stop_point: continue for pr in itask.state.get_unsatisfied_prerequisites(): if self.stop_point and get_point(pr.point) > self.stop_point: continue if itask.identity not in unsat: unsat[itask.identity] = [] unsat[itask.identity].append( f"{pr.get_id()}:" f"{self.config.get_taskdef(pr.task).get_output(pr.output)}" ) if unsat: LOG.warning( "Partially satisfied prerequisites:\n" + "\n".join( f" * {id_} is waiting on {sorted(others)}" for id_, others in unsat.items() ) ) return True return False def is_stalled(self) -> bool: """Return whether the workflow is stalled. Is stalled if not paused and contains only: - incomplete tasks - partially satisfied prerequisites (below stop point) - runahead-limited tasks (held back by the above) """ if any( itask.state( *TASK_STATUSES_ACTIVE, TASK_STATUS_PREPARING ) or ( itask.state(TASK_STATUS_WAITING) and not itask.state.is_runahead # (avoid waiting pre-spawned absolute-triggered tasks:) and itask.prereqs_are_satisfied() ) for itask in self.get_tasks() ): return False incomplete = self.log_incomplete_tasks() unsatisfied = self.log_unsatisfied_prereqs() return (incomplete or unsatisfied) def hold_active_task(self, itask: TaskProxy) -> None: if itask.state_reset(is_held=True): self.data_store_mgr.delta_task_state(itask) self.tasks_to_hold.add((itask.tdef.name, itask.point)) self.workflow_db_mgr.put_tasks_to_hold(self.tasks_to_hold) def release_held_active_task(self, itask: TaskProxy) -> None: if itask.state_reset(is_held=False): self.data_store_mgr.delta_task_state(itask) if (not itask.state.is_runahead) and itask.is_ready_to_run(): self.queue_task(itask) self.tasks_to_hold.discard((itask.tdef.name, itask.point)) self.workflow_db_mgr.put_tasks_to_hold(self.tasks_to_hold) def set_hold_point(self, point: 'PointBase') -> None: """Set the point after which all tasks must be held.""" self.hold_point = point for itask in self.get_tasks(): if itask.point > point: self.hold_active_task(itask) self.workflow_db_mgr.put_workflow_hold_cycle_point(point) def hold_tasks(self, items: Set[TaskTokens]) -> int: """Hold tasks with IDs matching the specified items.""" matched, unmatched = self.id_match(items) for id_ in matched: itask = self._get_task_by_id(id_.relative_id) if itask: # hold active task self.hold_active_task(itask) else: # hold inactive task icycle = get_point(id_['cycle']) self.data_store_mgr.delta_task_held(id_['task'], icycle, True) self.tasks_to_hold.add((id_['task'], icycle)) self.workflow_db_mgr.put_tasks_to_hold(self.tasks_to_hold) LOG.debug(f"Tasks to hold: {self.tasks_to_hold}") return len(unmatched) def release_held_tasks(self, items: Set[TaskTokens]) -> int: """Release held tasks with IDs matching any specified items.""" matched, unmatched = id_match( self.config, { # only match held tasks TaskTokens(cycle=str(cycle), task=task) for task, cycle in self.tasks_to_hold }, items, # only match tasks within the held task list only_match_pool=True, ) for id_ in matched: itask = self._get_task_by_id(id_.relative_id) if itask: # release active task self.release_held_active_task(itask) else: # release inactive task self.data_store_mgr.delta_task_held( id_['task'], get_point(id_['cycle']), False ) self.tasks_to_hold.discard( (id_['task'], get_point(id_['cycle'])) ) self.workflow_db_mgr.put_tasks_to_hold(self.tasks_to_hold) LOG.debug(f"Tasks to hold: {self.tasks_to_hold}") return len(unmatched) def release_hold_point(self) -> None: """Unset the workflow hold point and release all held active tasks.""" self.hold_point = None for itask in self.get_tasks(): self.release_held_active_task(itask) self.tasks_to_hold.clear() self.workflow_db_mgr.put_tasks_to_hold(self.tasks_to_hold) self.workflow_db_mgr.put_workflow_hold_cycle_point(None) def check_abort_on_task_fails(self): """Check whether workflow should abort on task failure. Return True if a task failed and `--abort-if-any-task-fails` was given. """ return self.abort_task_failed def spawn_on_output(self, itask: TaskProxy, output: str) -> None: """Spawn child-tasks of given output, into the pool. Remove the parent task from the pool if complete. Called by task event manager on receiving output messages, and after forced setting of task outputs (in this case the parent task could be transient, i.e. not in the pool). Also set the abort-on-task-failed flag if necessary. If not flowing on, update existing children but don't spawn new ones (unless manually forced to spawn with no flow number). If an absolute output is completed update the store of completed abs outputs, and update the prerequisites of every instance of the child in the pool. (The self.spawn method uses the store of completed abs outputs to satisfy any tasks with absolute prerequisites). Args: output: output to spawn on. """ if ( output == TASK_OUTPUT_FAILED and self.expected_failed_tasks is not None and itask.identity not in self.expected_failed_tasks ): self.abort_task_failed = True children = [] if itask.flow_nums: with suppress(KeyError): children = itask.graph_children[output] if itask.flow_wait and children: LOG.warning( f"[{itask}] not spawning on {output}: flow wait requested") self.remove_if_complete(itask, output) return if status_geq(itask.state.status, TASK_STATUS_PREPARING): # task has begun submission -> clear all xtriggers self.xtrigger_mgr.force_satisfy_all(itask, log=False) suicide = [] for c_name, c_point, is_abs in children: if is_abs: self.abs_outputs_done.add( (str(itask.point), itask.tdef.name, output)) self.workflow_db_mgr.put_insert_abs_output( str(itask.point), itask.tdef.name, output) self.workflow_db_mgr.process_queued_ops() c_task = self._get_task_by_id(quick_relative_id(c_point, c_name)) in_pool = c_task is not None if c_task is not None and c_task != itask: # (Avoid self-suicide: A => !A) self.merge_flows(c_task, itask.flow_nums) elif c_task is None and itask.flow_nums: # If child is not in the pool already, and parent belongs to a # flow (so it can spawn children), and parent is not waiting # for an upcoming flow merge before spawning ... then spawn it. c_task = self.spawn_task(c_name, c_point, itask.flow_nums) tasks: List[TaskProxy] if c_task is not None: # Have child task, update its prerequisites. if is_abs: matched, _unmatched = self.id_match( {TaskTokens(cycle='*', task=c_name)} ) tasks = self.get_itasks(matched) if c_task not in tasks: tasks.append(c_task) else: tasks = [c_task] for t in tasks: t.satisfy_me( [itask.tokens.duplicate(task_sel=output)], mode=itask.run_mode ) self.data_store_mgr.delta_task_prerequisite(t) if not in_pool: self.add_to_pool(t) if ( self.runahead_limit_point is not None and t.point <= self.runahead_limit_point ): self.rh_release_and_queue(t) # Event-driven suicide. if ( t.state.suicide_prerequisites and t.state.suicide_prerequisites_all_satisfied() ): suicide.append(t) for c_task in suicide: if self.config.experimental.expire_triggers: self.task_queue_mgr.remove_task(c_task) self.task_events_mgr.process_message( c_task, logging.WARNING, TASK_OUTPUT_EXPIRED ) else: self.remove(c_task, self.__class__.SUICIDE_MSG) if suicide: # Update DB now in case of very quick respawn attempt. # See https://github.com/cylc/cylc-flow/issues/6066 self.workflow_db_mgr.process_queued_ops() self.remove_if_complete(itask, output) def remove_if_complete( self, itask: TaskProxy, output: Optional[str] = None ) -> bool: """Remove a finished task if required outputs are complete. Return True if removed else False. Cylc 8: - if complete: - remove task and recompute runahead - else (incomplete): - retain Cylc 7 back compat: - if succeeded: - remove task and recompute runahead else (failed): - retain and recompute runahead (C7 failed tasks don't count toward runahead limit) """ if not itask.state(*TASK_STATUSES_FINAL): # can't be complete return False if itask.identity == self.stop_task_id: self.stop_task_finished = True if cylc.flow.flags.cylc7_back_compat: ret = False if not itask.state(TASK_STATUS_FAILED, TASK_OUTPUT_SUBMIT_FAILED): self.remove(itask) ret = True # Recompute runahead either way; failed tasks don't count in C7. if self.compute_runahead(): self.release_runahead_tasks() return ret if not itask.state.outputs.is_complete(): # Keep incomplete tasks in the pool. if output in TASK_STATUSES_FINAL: # Log based on the output, not the state, to avoid warnings # due to use of "cylc set" to set internal outputs on an # already-finished task. LOG.warning( f"[{itask}] did not complete the required outputs:\n" + itask.state.outputs.format_completion_status( ansimarkup=1 ) ) return False self.remove(itask) if self.compute_runahead(): self.release_runahead_tasks() return True def spawn_on_all_outputs( self, itask: TaskProxy, completed_only: bool = False ) -> None: """Spawn on all (or all completed) task outputs. If completed_only is False: Used in Cylc 7 Back Compat mode for pre-spawning waiting tasks. Do not set the associated prerequisites of spawned children satisfied. If completed_only is True: Used to retroactively spawn on already-completed outputs when a flow merges into a force-triggered no-flow task. In this case, do set the associated prerequisites of spawned children to satisfied. """ if not itask.flow_nums: return for _trigger, message, is_completed in itask.state.outputs: if completed_only and not is_completed: continue try: children = itask.graph_children[message] except KeyError: continue for c_name, c_point, _ in children: c_taskid = Tokens( cycle=str(c_point), task=c_name, ).relative_id c_task = self._get_task_by_id(c_taskid) if c_task is not None: # already spawned continue c_task = self.spawn_task(c_name, c_point, itask.flow_nums) if c_task is None: # not spawnable continue if completed_only: c_task.satisfy_me( [itask.tokens.duplicate(task_sel=message)], mode=itask.run_mode ) self.data_store_mgr.delta_task_prerequisite(c_task) self.add_to_pool(c_task) if ( self.runahead_limit_point is not None and c_task.point <= self.runahead_limit_point ): self.rh_release_and_queue(c_task) def can_be_spawned(self, name: str, point: 'PointBase') -> bool: """Return True if a point/name is within graph bounds.""" if name not in self.config.taskdefs: LOG.debug('No task definition %s', name) return False # Don't spawn outside of graph limits. # TODO: is it possible for initial_point to not be defined?? # (see also the similar check + log message in scheduler.py) if self.config.initial_point and point < self.config.initial_point: # Attempted manual trigger prior to FCP # or future triggers like foo[+P1] => bar, with foo at ICP. LOG.debug( 'Not spawning %s/%s: before initial cycle point', point, name) return False if self.config.final_point and point > self.config.final_point: # Only happens on manual trigger beyond FCP LOG.debug( 'Not spawning %s/%s: beyond final cycle point', point, name) return False # Is it on-sequence and within recurrence bounds. if not self.config.get_taskdef(name).is_valid_point(point): LOG.warning( self.ERR_PREFIX_TASK_NOT_ON_SEQUENCE.format( name, point ) ) return False return True def _get_task_history( self, name: str, point: 'PointBase', flow_nums: 'FlowNums' ) -> tuple[int, str | None, bool]: """Get submit_num, status, flow_wait for point/name in flow_nums. Args: name: task name point: task cycle point flow_nums: task flow numbers Returns: (submit_num, status, flow_wait) If no matching history, status will be None """ submit_num: int = 0 status: Optional[str] = None flow_wait = False info = self.workflow_db_mgr.pri_dao.select_prev_instances( name, str(point) ) with suppress(ValueError): submit_num = max(s[0] for s in info) for _snum, f_wait, old_fnums, old_status in info: if set.intersection(flow_nums, old_fnums): # matching flows status = old_status flow_wait = f_wait if status in TASK_STATUSES_FINAL: # task finished break # Else continue: there may be multiple entries with flow # overlap due to merges (they'll have have same snum and # f_wait); keep going to find the finished one, if any. return submit_num, status, flow_wait def _load_historical_outputs(self, itask: 'TaskProxy') -> None: """Load a task's historical outputs from the DB. NOTE this creates a task_states/task_outputs DB entry if not present. """ info = self.workflow_db_mgr.pri_dao.select_task_outputs( itask.tdef.name, str(itask.point)) if not info: # task never ran before self.db_add_new_flow_rows(itask) else: flow_seen = False for outputs_str, fnums in info.items(): # (if fnums is empty, it means the 'none' flow) if itask.flow_nums.intersection(fnums): # DB row has overlap with itask's flows flow_seen = True # BACK COMPAT: In Cylc >8.0.0,<8.3.0, only the task # messages were stored in the DB as a list. # from: 8.0.0 # to: 8.3.0 outputs: Union[ Dict[str, str], List[str] ] = json.loads(outputs_str) if isinstance(outputs, dict): # {trigger: message} - match triggers, not messages. # DB may record forced completion rather than message. for trigger in outputs.keys(): itask.state.outputs.set_trigger_complete(trigger) else: # [message] - always the full task message for msg in outputs: itask.state.outputs.set_message_complete(msg) if not flow_seen: # itask never ran before in its assigned flows self.db_add_new_flow_rows(itask) def spawn_task( self, name: str, point: 'PointBase', flow_nums: 'FlowNums', flow_wait: bool = False, ) -> TaskProxy | None: """Return a new task proxy for the given flow if possible. We need to hit the DB for: - submit number - task status - flow-wait - completed outputs (e.g. via "cylc set") If history records a final task status (for this flow): - if not flow wait, don't spawn (return None) - if flow wait, don't spawn (return None) but do spawn children - if outputs are incomplete, don't auto-rerun it (return None) Otherwise, spawn the task and load any completed outputs. """ submit_num, prev_status, prev_flow_wait = ( self._get_task_history(name, point, flow_nums) ) if ( not prev_status and point < self.config.start_point and flow_nums.issuperset({1}) # Warm start - treat pre-startcp tasks as already run in flow=1, # unless manually triggered: and (name, point) not in self.pre_start_tasks_to_trigger ): return None # Create the task proxy with any completed outputs loaded. itask = self._load_db_task_proxy( point, self.config.get_taskdef(name), flow_nums, status=prev_status or TASK_STATUS_WAITING, submit_num=submit_num, flow_wait=flow_wait, ) if itask is None: return None if ( prev_status is not None and not itask.state.outputs.get_completed_outputs() and not self.config.experimental.expire_triggers ): # If itask has any history but no completed outputs, it must have # been removed by suicide trigger (not by `cylc remove` which # erases task history). # # This was a bodge to prevent suicided tasks from respawning via # other dependencies, given that suicide leaves no DB record. # The bodge fails if any outputs were completed before suicide. # # The reimplementation of suicide triggers as expire triggers # renders this bodge obsolete. TODO: remove this code block on # migrating expire triggers from "experimental" to standard. LOG.info(f"Not respawning {point}/{name} - task was removed") return None if prev_status in TASK_STATUSES_FINAL: # Task finished previously. if itask.is_complete(): msg = "and completed" itask.transient = True else: # revive as incomplete. msg = "incomplete" if LOG.level <= logging.DEBUG: # avoid unnecessary compute when we are not in debug mode id_ = itask.tokens.duplicate( task_sel=prev_status ).relative_id_with_selectors LOG.debug( f"[{id_}] already finished {msg}" f" {repr_flow_nums(flow_nums, full=True)})" ) if prev_flow_wait: self._spawn_after_flow_wait(itask) if itask.transient: return None if not itask.transient: if (name, point) in self.tasks_to_hold: LOG.info(f"[{itask}] holding (as requested earlier)") self.hold_active_task(itask) elif self.hold_point and itask.point > self.hold_point: # Hold if beyond the workflow hold point LOG.info( f"[{itask}] holding (beyond workflow " f"hold point: {self.hold_point})" ) self.hold_active_task(itask) # Don't add to pool if it depends on a task beyond the stop point. # "foo; foo[+P1] & bar => baz" # Here, in the final cycle bar wants to spawn baz, but that would # stall because baz also depends on foo after the final point. if self.stop_point and itask.point <= self.stop_point: for pct in itask.state.prerequisites_get_target_points(): if pct > self.stop_point: LOG.warning( f"[{itask}] not spawned: a prerequisite is beyond" f" the workflow stop point ({self.stop_point})" ) return None # Satisfy any absolute triggers. if ( itask.tdef.has_abs_triggers and itask.state.prerequisites_are_not_all_satisfied() ): itask.satisfy_me([ Tokens(cycle=cycle, task=task, task_sel=output) for cycle, task, output in self.abs_outputs_done ]) if prev_status is None: # only add new flow rows if this task has not run before # see https://github.com/cylc/cylc-flow/pull/6821 self.db_add_new_flow_rows(itask) return itask def _spawn_after_flow_wait(self, itask: TaskProxy) -> None: LOG.info(f"[{itask}] spawning outputs after flow-wait") self.spawn_on_all_outputs(itask, completed_only=True) # update flow wait status in the DB itask.flow_wait = False # itask.flow_nums = orig_fnums self.workflow_db_mgr.put_update_task_flow_wait(itask) return None def _load_db_task_proxy( self, point: 'PointBase', taskdef: 'TaskDef', flow_nums: 'FlowNums', status: str = TASK_STATUS_WAITING, flow_wait: bool = False, transient: bool = False, is_manual_submit: bool = False, submit_num: int = 0, ) -> 'TaskProxy | None': """Spawn a task, update outputs from DB. NOTE this creates a task_states/task_outputs DB entry if not present. """ if not self.can_be_spawned(taskdef.name, point): return None itask = TaskProxy( self.tokens, taskdef, point, flow_nums, status=status, flow_wait=flow_wait, submit_num=submit_num, transient=transient, is_manual_submit=is_manual_submit, sequential_xtrigger_labels=( self.xtrigger_mgr.xtriggers.sequential_xtrigger_labels ), ) # Update it with outputs that were already completed. self._load_historical_outputs(itask) return itask def _standardise_prereqs( self, prereqs: 'Iterable[str]' ) -> 'Set[PrereqTuple]': """Extract task prerequisites from user input and standardise. Weed out any xtrigger prerequisites. Command validation has handled output suffixes and defaults. Args: prereqs: prerequisites and xtriggers in string form: /: xtrigger/[:succeeded] xtrigger/all[:succeeded] Returns: {prerequisite-tokens} """ _prereqs = set() for prereq in prereqs: pre = Tokens(prereq, relative=True) if pre['cycle'] == XTRIGGER_PREREQ_PREFIX: # weed out xtrigger prerequisites continue output = TaskTrigger.standardise_name( pre['task_sel'] or TASK_OUTPUT_SUCCEEDED) # Convert outputs to task messages. try: msg = self.config.get_taskdef( str(pre['task']) ).outputs[output][0] cycle = standardise_point_string(pre['cycle']) except KeyError: LOG.warning( f"Output {pre.relative_id_with_selectors} not found") continue except WorkflowConfigError as exc: LOG.warning( f'Invalid prerequisite task name:\n{exc.args[0]}') except PointParsingError as exc: LOG.warning( f'Invalid prerequisite cycle point:\n{exc.args[0]}') else: _prereqs.add(PrereqTuple(str(cycle), str(pre['task']), msg)) return _prereqs def _standardise_outputs( self, point: 'PointBase', tdef: 'TaskDef', outputs: Iterable[str] ) -> List[str]: """Convert task output triggers to task messages.""" _outputs = [] for out in outputs: # convert "succeed" to "succeeded" etc. output = TaskTrigger.standardise_name(out) try: msg = tdef.outputs[output][0] except KeyError: LOG.warning( f"Output {point}/{tdef.name}:{output} not found") continue _outputs.append(msg) return _outputs def set_prereqs_and_outputs( self, items: 'Set[TaskTokens]', outputs: List[str], prereqs: List[str], flow: List[str], flow_wait: bool = False, flow_descr: Optional[str] = None, ): """Complete outputs and satisfy prerequisites, via "cylc set" command. Default to completing all required outputs. On setting prerequisites: - spawn the task into n=0. - prerequisite validity is checked via the taskdef prior to spawning so we can back out it if no valid prerequisites are given On setting outputs: - update task outputs in the DB - (implied outputs are handled by the event manager) - spawn children of the outputs, with those prerequisites satisfied Transient task proxies are used to spawn the children of outputs. Even if the parent was previously spawned in this flow its children might not have been. ("Transient" just means not intended for the task pool, just a convient way to use TaskProxy methods). A forced output cannot cause a state change to submitted or running, but it can complete a task so that it doesn't need to run. Args: items: Parsed task ID patterns. prereqs: Prerequisites to satisfy. outputs: Outputs to complete. flow: Flow numbers for spawned or merged tasks. flow_wait: Wait for flows to catch up before continuing. flow_descr: Description of new flow. """ matched, _unmatched = self.id_match(set(items)) no_op = True # Clean and separate requested prerequisite and xtrigger specs. if prereqs != ['all']: set_all = False clean_pre = self._standardise_prereqs(prereqs) clean_xtr = _get_xtrig_prereqs(prereqs) else: set_all = True clean_pre = set() clean_xtr = {} if prereqs and not (set_all or clean_pre or clean_xtr): # Nothing to do! return # Get integer flow numbers from CLI inputs. flow_nums = self.flow_mgr.cli_to_flow_nums(flow, flow_descr) # Here, empty flow_nums means either no-flow or all active flows. if flow != [FLOW_NONE] and not flow_nums: flow_nums = self._get_active_flow_nums() # Set active tasks. warnings_flow_none = [] for id_ in matched: itask = self._get_task_by_id(id_.relative_id) if itask: # set active task if flow == [FLOW_NONE] and itask.flow_nums: # Exclude --flow=none for active tasks. warnings_flow_none.append( f"{itask.identity}: " f"{repr_flow_nums(itask.flow_nums, full=True)}" ) continue if prereqs: valid_prereqs = self._get_valid_prereqs( clean_pre, itask.tdef, itask.point) valid_xtrigs = self._get_valid_xtrigs( clean_xtr, itask.tdef, itask.point) if not (set_all or valid_prereqs or valid_xtrigs): continue self.merge_flows(itask, flow_nums) self._set_prereqs_itask( itask, valid_prereqs, valid_xtrigs, set_all) no_op = False else: # Outputs (may be empty list) # Spawn as if seq xtrig of parentless task was satisfied, # with associated task producing these outputs. self.merge_flows(itask, flow_nums) self.check_spawn_psx_task(itask) self._set_outputs_itask(itask, outputs) no_op = False else: # set inactive task tdef = self.config.taskdefs[id_['task']] icycle = get_point(id_['cycle']) if prereqs: valid_prereqs = self._get_valid_prereqs( clean_pre, tdef, icycle) valid_xtrigs = self._get_valid_xtrigs( clean_xtr, tdef, icycle) if not (set_all or valid_prereqs or valid_xtrigs): continue self._set_prereqs_tdef( icycle, tdef, valid_prereqs, valid_xtrigs, flow_nums, flow_wait, set_all) no_op = False else: # Outputs (may be empty list) trans = self._load_db_task_proxy( icycle, tdef, flow_nums, flow_wait=flow_wait, transient=True ) if trans and self._set_outputs_itask(trans, outputs): no_op = False if warnings_flow_none: msg = '\n * '.join(warnings_flow_none) LOG.warning(f"Already active - ignoring no-flow set: \n * {msg}") if not no_op: # for "cylc play --start-tasks" compute runahead after spawning self.compute_runahead() self.release_runahead_tasks() def _get_valid_prereqs( self, prereqs: Set[PrereqTuple], tdef: 'TaskDef', point: 'PointBase' ) -> 'Set[PrereqTuple]': """Weed out prerequisites not valid for this task. And convert outputs to messages, for satisfying task prerequisites. """ # Valid prerequisites as tokens (outputs as task messages). valid_pre = { PrereqTuple(key.point, key.task, key.output) for pre in tdef.get_prereqs(point) for key in pre.keys() } # standardise and weed out xtrigger prerequisites invalid = prereqs - valid_pre if invalid: task = quick_relative_id(point, tdef.name) for prereq in invalid: # But log bad ones with triggers, not messages. trg = self.config.get_taskdef( prereq.task ).get_output(prereq.output) LOG.warning( f'{task} does not depend on' f' "{quick_relative_id(prereq.point, prereq.task, trg)}"' ) return valid_pre & prereqs def _get_valid_xtrigs( self, xtrigs: Dict[str, bool], tdef: 'TaskDef', point: 'PointBase' ) -> 'Dict[str, bool]': """Weed out xtriggers not valid for this task.""" valid_x_labels = tdef.get_xtrigs(point) # And allow any dynamically xtriggers, such as retries. itask = self.get_task(point, tdef.name) if itask is not None: valid_x_labels.update(itask.state.xtriggers.keys()) invalid = set(xtrigs.keys()) - valid_x_labels if invalid: task = quick_relative_id(point, tdef.name) for xtrig in invalid: if xtrig != "all": LOG.warning( f'{task} does not depend on xtrigger "{xtrig}"') return { k: v for k, v in xtrigs.items() if k in valid_x_labels or k == "all" } def _set_outputs_itask( self, itask: 'TaskProxy', outputs: Iterable[str], ) -> bool: """Manually set requested outputs on a task proxy and spawn children. If no outputs were specified and the task has no required outputs to set, set the "success pathway" outputs in the same way that skip mode does. Designated flows should already be merged to the task proxy. Returns: True if any outputs were set, else False. """ no_op = True outputs = set(outputs) if not outputs: outputs = set( # Set required outputs by default itask.state.outputs.iter_required_messages() ) or ( # Set success pathway outputs get_skip_mode_outputs(itask) ) else: # --out=skip sets all the outputs that skip mode would. skips: Set[str] = set() if RunMode.SKIP.value in outputs: # Check for broadcasts to task: outputs.remove(RunMode.SKIP.value) bc_mgr = self.task_events_mgr.broadcast_mgr rtconfig = bc_mgr.get_updated_rtconfig(itask) skips = get_skip_mode_outputs(itask, rtconfig) itask.run_mode = RunMode.SKIP outputs = set( self._standardise_outputs(itask.point, itask.tdef, outputs) ).union(skips) for output in sorted(outputs, key=itask.state.outputs.output_sort_key): if itask.state.outputs.is_message_complete(output): LOG.info(f"output {itask.identity}:{output} completed already") continue self.task_events_mgr.process_message( itask, logging.INFO, output, forced=True ) no_op = False if not itask.state(TASK_STATUS_WAITING): # Can't be runahead limited or queued. itask.state_reset(is_runahead=False, is_queued=False) self.task_queue_mgr.remove_task(itask) if no_op: return False self.data_store_mgr.delta_task_state(itask) self.data_store_mgr.delta_task_outputs(itask) self.workflow_db_mgr.put_update_task_state(itask) self.workflow_db_mgr.put_update_task_outputs(itask) self.workflow_db_mgr.process_queued_ops() return True def _set_prereqs_itask( self, itask: 'TaskProxy', prereqs: 'Iterable[PrereqTuple]', xtrigs: 'Dict[str, bool]', set_all: bool ) -> None: """Set prerequisites on a task proxy. Designated flows should already be merged to the task proxy. """ # task prerequisites itask.force_satisfy(prereqs, set_all) # xtriggers, including "all" self.xtrigger_mgr.force_satisfy(itask, xtrigs) if ( itask.state.is_runahead and self.runahead_limit_point is not None and itask.point <= self.runahead_limit_point ): self.spawn_to_rh_limit( itask.tdef, itask.tdef.next_point(itask.point), itask.flow_nums ) def _set_prereqs_tdef( self, point: 'PointBase', taskdef: 'TaskDef', prereqs: 'Iterable[PrereqTuple]', xtrigs: 'Dict[str, bool]', flow_nums: 'FlowNums', flow_wait: bool, set_all: bool ) -> Optional[TaskProxy]: """Spawn an inactive task and set prerequisites on it.""" itask = self.spawn_task( taskdef.name, point, flow_nums, flow_wait=flow_wait ) if itask is None: return None self.db_add_new_flow_rows(itask) self._set_prereqs_itask(itask, prereqs, xtrigs, set_all) self.add_to_pool(itask) return itask def _get_active_flow_nums(self) -> 'FlowNums': """Return all active flow numbers. If there are no active flows (e.g. on restarting a completed workflow) return the most recent active flows. Or, if there are no flows in the workflow history (e.g. after `cylc remove`), return flow=1. """ return ( set().union(*(itask.flow_nums for itask in self.get_tasks())) or self.workflow_db_mgr.pri_dao.select_latest_flow_nums() or {1} ) def queue_or_trigger(self, itask: 'TaskProxy'): """Handle state, queues, and runahead for a manually triggered task. Triggering a non-queued task: - queue it, if the queue is full - run it, if the queue is not full Triggering a queued task: - run it, regardless of the queue limit If ready, add itask to the tasks_to_trigger_now list. Assumes the task is in the pool. Note manual trigger now works by satisfying prerequisites so this method should only be called for fully satisfied tasks. """ itask.is_manual_submit = True itask.reset_try_timers() self.data_store_mgr.delta_task_prerequisite(itask) if itask.state_reset(TASK_STATUS_WAITING): # (could also be unhandled failed) self.data_store_mgr.delta_task_state(itask) if itask.state_reset(is_runahead=False): # Can force trigger runahead-limited tasks. self.data_store_mgr.delta_task_state(itask) self.spawn_to_rh_limit( itask.tdef, itask.tdef.next_point(itask.point), itask.flow_nums ) if not itask.state.is_queued: # queue it if limiting active, _ = self.count_active_tasks() if self.task_queue_mgr.push_task_if_limited(itask, active): itask.state_reset(is_queued=True) self.data_store_mgr.delta_task_state(itask) elif self.task_queue_mgr.remove_task(itask): # else release it from the queue to run now itask.state_reset(is_queued=False) self.data_store_mgr.delta_task_state(itask) if not itask.state.is_queued: # If not queued now, record the task as ready to run. itask.waiting_on_job_prep = True self.tasks_to_trigger_now.add(itask) # Task may be set running before xtrigger is satisfied, # if so check/spawn if xtrigger sequential. self.check_spawn_psx_task(itask) def check_spawn_psx_task(self, itask: 'TaskProxy') -> None: """Check and spawn parentless sequential xtriggered task (psx).""" # Will spawn out to RH limit or next parentless clock trigger # or non-parentless. if ( itask.is_xtrigger_sequential and ( itask.identity not in self.xtrigger_mgr.sequential_has_spawned_next ) ): self.xtrigger_mgr.sequential_has_spawned_next.add( itask.identity ) self.spawn_to_rh_limit( itask.tdef, itask.tdef.next_point(itask.point), itask.flow_nums ) def clock_expire_tasks(self): """Expire any tasks past their clock-expiry time.""" for itask in self.get_tasks(): if ( # force triggered tasks can not clock-expire # see proposal point 10: # https://cylc.github.io/cylc-admin/proposal-optional-output-extension.html#proposal not itask.is_manual_submit # only waiting tasks can clock-expire # see https://github.com/cylc/cylc-flow/issues/6025 # (note retrying tasks will be in the waiting state) and itask.state(TASK_STATUS_WAITING) # check if this task is clock expired and itask.clock_expire() ): self.task_queue_mgr.remove_task(itask) self.task_events_mgr.process_message( itask, logging.WARNING, TASK_OUTPUT_EXPIRED, ) def task_succeeded(self, id_): """Return True if task with id_ is in the succeeded state.""" return any( ( itask.identity == id_ and itask.state(TASK_STATUS_SUCCEEDED) ) for itask in self.get_tasks() ) def stop_flow(self, flow_num): """Stop a given flow from spawning any further. Remove the flow number from every task in the pool, and remove any task with no remaining flow numbers if it is not already active. """ for itask in self.get_tasks(): try: itask.flow_nums.remove(flow_num) except KeyError: continue else: if ( not itask.state( *TASK_STATUSES_ACTIVE, TASK_STATUS_PREPARING) and not itask.flow_nums ): # Don't spawn successor if the task is parentless. self.remove(itask, "flow stopped") if self.compute_runahead(): self.release_runahead_tasks() def log_task_pool(self, log_lvl=logging.DEBUG): """Log content of task pool, for debugging.""" LOG.log( log_lvl, "\n".join( f"* {itask}" for itask in self.get_tasks() ) ) def id_match( self, ids: Set[TaskTokens], only_match_pool: bool = False, ) -> Tuple[Set[TaskTokens], Set[TaskTokens]]: """Match IDs against active tasks in the pool.""" active_task_ids: Set[TaskTokens] = { TaskTokens( cycle=itask.tokens['cycle'], task=itask.tokens['task'], task_sel=itask.state.status, ) for itasks in self.active_tasks.values() for itask in itasks.values() } return id_match( self.config, active_task_ids, ids, only_match_pool=only_match_pool, ) def merge_flows(self, itask: TaskProxy, flow_nums: 'FlowNums') -> None: """Merge flow_nums into itask.flow_nums, for existing itask. This is required when we try to spawn a task instance that already exists in the pool (i.e., with the same name and cycle point). This also performs required spawning / state changing for edge cases. """ if not flow_nums or (flow_nums == itask.flow_nums): # Don't do anything if: # 1. merging from a no-flow task, or # 2. same flow (no merge needed); can arise # downstream of an AND trigger (if "A & B => C" # and A spawns C first, B will find C is already in the pool), # and via suicide triggers ("A =>!A": A tries to spawn itself). return merge_with_no_flow = not itask.flow_nums itask.merge_flows(flow_nums) self.data_store_mgr.delta_task_flow_nums(itask) # Merged tasks get a new row in the db task_states table. self.db_add_new_flow_rows(itask) if ( itask.state(*TASK_STATUSES_FINAL) and not itask.state.outputs.is_complete() ): # Re-queue incomplete task to run again in the merged flow. LOG.info(f"[{itask}] incomplete task absorbed by new flow.") itask.state_reset(TASK_STATUS_WAITING) self.queue_task(itask) self.data_store_mgr.delta_task_state(itask) elif merge_with_no_flow or itask.flow_wait: # 2. Retro-spawn on completed outputs and continue as merged flow. LOG.info(f"[{itask}] spawning on pre-merge outputs") itask.flow_wait = False self.spawn_on_all_outputs(itask, completed_only=True) self.spawn_to_rh_limit( itask.tdef, itask.next_point(), itask.flow_nums) cylc-flow-8.6.4/cylc/flow/id_cli.py0000664000175000017500000004600115202510242017304 0ustar alastairalastair# 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 . import asyncio import fnmatch from pathlib import Path import re from typing import Optional, Dict, List, Tuple, Any from metomi.isodatetime.parsers import TimePointParser from metomi.isodatetime.exceptions import ISO8601SyntaxError from cylc.flow import LOG from cylc.flow.exceptions import ( InputError, ) from cylc.flow.hostuserutil import get_user from cylc.flow.id import ( Tokens, contains_multiple_workflows, tokenise, upgrade_legacy_ids, ) from cylc.flow.pathutil import EXPLICIT_RELATIVE_PATH_REGEX from cylc.flow.workflow_files import ( check_flow_file, detect_both_flow_and_suite, get_flow_file, get_workflow_run_dir, infer_latest_run_from_id, validate_workflow_name, abort_if_flow_file_in_path ) FN_CHARS = re.compile(r'[\*\?\[\]\!]') TP_PARSER = TimePointParser() def cli_tokenise(id_: str) -> Tokens: """Tokenise with support for long-format datetimes. If a cycle selector is present, it could be part of a long-format ISO 8601 datetime that was erroneously split. Re-attach it if it results in a valid datetime. Examples: >>> f = lambda t: {k: v for k, v in t.items() if v is not None} >>> f(cli_tokenise('foo//2021-01-01T00:00Z')) {'workflow': 'foo', 'cycle': '2021-01-01T00:00Z'} >>> f(cli_tokenise('foo//2021-01-01T00:horse')) {'workflow': 'foo', 'cycle': '2021-01-01T00', 'cycle_sel': 'horse'} """ tokens = tokenise(id_) cycle = tokens['cycle'] cycle_sel = tokens['cycle_sel'] if not (cycle and cycle_sel) or '-' not in cycle: return tokens cycle = f'{cycle}:{cycle_sel}' try: TP_PARSER.parse(cycle) except ISO8601SyntaxError: return tokens dict.__setitem__(tokens, 'cycle', cycle) del tokens['cycle_sel'] return tokens def _parse_cli(*ids: str) -> List[Tokens]: """Parse a list of Cylc identifiers as provided on the CLI. * Validates identifiers. * Expands relative references to absolute ones. * Handles legacy Cylc7 syntax. Args: *ids (tuple): Identifier list. Raises: ValueError - For invalid identifiers or identifier lists. Returns: list - List of tokens dictionaries. Examples: # parse to tokens then detokenise back >>> from cylc.flow.id import detokenise >>> parse_back = lambda *ids: list(map(detokenise, _parse_cli(*ids))) # list of workflows: >>> parse_back('workworkflow') ['workworkflow'] >>> parse_back('workworkflow/') ['workworkflow'] >>> parse_back('workworkflow1', 'workworkflow2') ['workworkflow1', 'workworkflow2'] # absolute references >>> parse_back('workworkflow1//cycle1', 'workworkflow2//cycle2') ['workworkflow1//cycle1', 'workworkflow2//cycle2'] # relative references: >>> parse_back('workworkflow', '//cycle1', '//cycle2') ['workworkflow//cycle1', 'workworkflow//cycle2'] # mixed references >>> parse_back( ... 'workworkflow1', '//cycle', 'workworkflow2', ... '//cycle', 'workworkflow3//cycle' ... ) ['workworkflow1//cycle', 'workworkflow2//cycle', 'workworkflow3//cycle'] # legacy ids: >>> parse_back('workworkflow', 'task.123', 'a.b.c.234', '345/task') ['workworkflow//123/task', 'workworkflow//234/a.b.c', 'workworkflow//345/task'] # errors: >>> _parse_cli('////') Traceback (most recent call last): cylc.flow.exceptions.InputError: Invalid ID: //// >>> parse_back('//cycle') Traceback (most recent call last): cylc.flow.exceptions.InputError: Relative reference must follow an incomplete one... >>> parse_back('workflow//cycle', '//cycle') Traceback (most recent call last): cylc.flow.exceptions.InputError: Relative reference must follow an incomplete one... >>> parse_back('workflow///cycle/') Traceback (most recent call last): cylc.flow.exceptions.InputError: Invalid ID: workflow///cycle/ """ # upgrade legacy ids if required ids = upgrade_legacy_ids(*ids) partials: Optional[Tokens] = None partials_expended: bool = False tokens_list: List[Tokens] = [] for id_ in ids: try: tokens = cli_tokenise(id_) except ValueError: if id_.endswith('/') and not id_.endswith('//'): # noqa: SIM106 # tolerate IDs that end in a single slash on the CLI # (e.g. CLI auto completion) try: # this ID is invalid with or without the trailing slash tokens = cli_tokenise(id_[:-1]) except ValueError: raise InputError(f'Invalid ID: {id_}') from None else: raise InputError(f'Invalid ID: {id_}') from None is_partial = tokens.get('workflow') and not tokens.get('cycle') is_relative = not tokens.get('workflow') if partials: # we previously encountered a workflow ID which did not specify a # cycle if is_partial: # this is an absolute ID if not partials_expended: # no relative references were made to the previous ID # so add the whole workflow to the tokens list tokens_list.append(partials) partials = tokens partials_expended = False elif is_relative: # this is a relative reference => expand it using the context # of the partial ID tokens_list.append(Tokens( **{ **partials, **tokens, }, )) partials_expended = True else: # this is a fully expanded reference tokens_list.append(tokens) partials = None partials_expended = False else: # there was no previous reference that a relative reference # could apply to if is_partial: partials = tokens partials_expended = False elif is_relative: # so a relative reference is an error raise InputError( 'Relative reference must follow an incomplete one.' '\nE.G: workflow //cycle/task' ) else: tokens_list.append(tokens) if partials and not partials_expended: # if the last ID was a "partial" but not expanded add it to the list tokens_list.append(tokens) return tokens_list def parse_ids(*args, **kwargs): return asyncio.run(parse_ids_async(*args, **kwargs)) async def parse_ids_async( *ids: str, src: bool = False, match_workflows: bool = False, match_active: Optional[bool] = True, infer_latest_runs: bool = True, constraint: str = 'tasks', max_workflows: Optional[int] = None, max_tasks: Optional[int] = None, alt_run_dir: Optional[str] = None, ) -> Tuple[Dict[str, List[Tokens]], Any]: """Parse IDs from the command line. Args: ids: Collection of IDs to parse. src: If True then source workflows can be provided via an absolute path or a relative path starting "./". Infers max_workflows = 1. match_workflows: If True workflows can be globs. match_active: If match_workflows is True this determines the wokflow state filter. True - running & paused False - stopped None - any infer_latest_runs: If true infer the latest run for a workflow when applicable (allows 'cylc play one' rather than 'cylc play one/run1'). constraint: Constrain the types of objects the IDs should relate to. workflows - only allow workflows. tasks - require tasks to be defined. mixed - permit tasks not to be defined. max_workflows: Specify the maximum number of workflows permitted to be specified in the ids. max_tasks: Specify the maximum number of tasks permitted to be specified in the ids. alt_run_dir: Specify a non-standard cylc-run location, e.g. for another user. Returns: With src=True": (workflows, flow_file_path) Else: (workflow, multi_mode) Where: workflows: Dictionary containing workflow ID strings against lists of relative tokens specified on that workflow. {workflow_id: [relative_tokens]} flow_file_path: Path to the flow.cylc (or suite.rc in Cylc 7 compat mode) multi_mode: True if multiple workflows selected or if globs were provided in the IDs. """ if constraint not in {'tasks', 'workflows', 'mixed'}: raise ValueError(f'Invalid constraint: {constraint}') tokens_list = [] src_path = None flow_file_path = None multi_mode = False if src: # can only have one workflow if permitting source workflows max_workflows = 1 ret = _parse_src_path(ids[0]) if ret: # yes, replace the path with an ID and continue workflow_id, src_path, flow_file_path = ret ids = ( Tokens( user=None, workflow=workflow_id, ).id + '//', *ids[1:] ) tokens_list.extend(_parse_cli(*ids)) # ensure the IDS are compatible with the constraint _validate_constraint(*tokens_list, constraint=constraint) if match_workflows: # match workflow IDs via cylc-scan # if any patterns are present switch to multi_mode for clarity multi_mode = await _expand_workflow_tokens( tokens_list, match_active=match_active, ) # check the workflow part of the IDs are valid _validate_workflow_ids(*tokens_list, src_path=src_path) if not multi_mode: # check how many workflows we are working on multi_mode = contains_multiple_workflows(tokens_list) # infer the run number if not specified the ID (and if possible) if infer_latest_runs: _infer_latest_runs( tokens_list, src_path=src_path, alt_run_dir=alt_run_dir) _validate_number( *tokens_list, max_workflows=max_workflows, max_tasks=max_tasks, ) workflows = _batch_tokens_by_workflow(*tokens_list, constraint=constraint) if src: if not flow_file_path: # get the workflow file path from the run dir flow_file_path = get_flow_file(next(iter(workflows))) return workflows, flow_file_path return workflows, multi_mode def parse_id(*args, **kwargs) -> Tuple[str, Optional[Tokens], Any]: return asyncio.run(parse_id_async(*args, **kwargs)) async def parse_id_async( *args, **kwargs, ) -> Tuple[str, Optional[Tokens], Any]: """Special case of parse_ids with a more convenient return format. Infers: max_workflows: 1 max_tasks: 1 """ workflows, ret = await parse_ids_async( *args, **{ # type: ignore **kwargs, 'max_workflows': 1, 'max_tasks': 1, }, ) workflow_id = next(iter(workflows)) tokens_list = workflows[workflow_id] tokens: Optional[Tokens] if tokens_list: tokens = tokens_list[0] else: tokens = None return workflow_id, tokens, ret def contains_fnmatch(string: str) -> bool: """Return True if a string contains filename match chars. Examples: >>> contains_fnmatch('a') False >>> contains_fnmatch('*') True >>> contains_fnmatch('abc') False >>> contains_fnmatch('a*c') True """ return bool(FN_CHARS.search(string)) def _validate_constraint(*tokens_list, constraint=None): if constraint == 'workflows': for tokens in tokens_list: if tokens.is_null or tokens.is_task_like: raise InputError('IDs must be workflows') return if constraint == 'tasks': for tokens in tokens_list: if tokens.is_null or not tokens.is_task_like: raise InputError('IDs must be tasks') return if constraint == 'mixed': for tokens in tokens_list: if tokens.is_null: raise InputError('IDs cannot be null.') return def _validate_workflow_ids(*tokens_list, src_path): for ind, tokens in enumerate(tokens_list): if tokens['user'] and (tokens['user'] != get_user()): raise InputError( "Operating on other users' workflows is not supported" ) if not src_path: validate_workflow_name(tokens['workflow']) if ind == 0 and src_path: # source workflow passed in as a path pass else: src_path = Path(get_workflow_run_dir(tokens['workflow'])) if src_path.is_file(): raise InputError( f'Workflow ID cannot be a file: {tokens["workflow"]}' ) if tokens['cycle'] and tokens['cycle'].startswith('run'): # issue a warning if the run number is provided after the // # separator e.g. workflow//run1 rather than workflow/run1// suggested = Tokens( user=tokens['user'], workflow=f'{tokens["workflow"]}/{tokens["cycle"]}', cycle=tokens['task'], task=tokens['job'], ) LOG.warning(f'Did you mean: {suggested.id}') detect_both_flow_and_suite(src_path) def _infer_latest_runs(tokens_list, src_path, alt_run_dir=None): for ind, tokens in enumerate(tokens_list): if ind == 0 and src_path: # source workflow passed in as a path continue tokens_list[ind] = tokens.duplicate( workflow=infer_latest_run_from_id( tokens['workflow'], alt_run_dir ) ) pass def _validate_number(*tokens_list, max_workflows=None, max_tasks=None): if not max_workflows and not max_tasks: return workflows_seen = set() tasks_count = 0 for tokens in tokens_list: if tokens.is_task_like: tasks_count += 1 if tokens["workflow"] is not None: workflows_seen.add(tokens["workflow"]) if max_workflows and len(workflows_seen) > max_workflows: raise InputError( f'IDs contain too many workflows (max {max_workflows})' ) if max_tasks and tasks_count > max_tasks: raise InputError( f'IDs contain too many cycles/tasks/jobs (max {max_tasks})' ) def _batch_tokens_by_workflow(*tokens_list, constraint=None): """Sorts tokens into lists by workflow ID. Example: >>> _batch_tokens_by_workflow( ... Tokens(workflow='x', cycle='1'), ... Tokens(workflow='x', cycle='2'), ... ) {'x': [, ]} """ workflow_tokens = {} for tokens in tokens_list: w_tokens = workflow_tokens.setdefault(tokens['workflow'], []) relative_tokens = tokens.task if constraint in {'mixed', 'workflows'} and relative_tokens.is_null: continue w_tokens.append(relative_tokens) return workflow_tokens async def _expand_workflow_tokens(tokens_list, match_active=True): multi_mode = False for tokens in list(tokens_list): workflow = tokens['workflow'] if not contains_fnmatch(workflow): # no expansion to perform continue else: # remove the original entry multi_mode = True tokens_list.remove(tokens) async for tokens in _expand_workflow_tokens_impl( tokens, match_active=match_active, ): # add the expanded tokens back onto the list tokens_list.append(tokens) return multi_mode async def _expand_workflow_tokens_impl(tokens, match_active=True): """Use "cylc scan" to expand workflow patterns.""" workflow_sel = tokens['workflow_sel'] if workflow_sel and workflow_sel != 'running': raise InputError( f'The workflow selector :{workflow_sel} is not' 'currently supported.' ) # import only when needed to avoid slowing CLI unnecessarily from cylc.flow.network.scan import ( filter_name, is_active, scan, ) # construct the pipe pipe = scan | filter_name(fnmatch.translate(tokens['workflow'])) if match_active is not None: pipe |= is_active(match_active) # iter the results async for workflow in pipe: yield tokens.duplicate(workflow=workflow['name']) def _parse_src_path(id_): """Parse CLI workflow arg to find a valid source directory. Returns: - (dir name, dir path, config file path) if id_ is a valid src dir. - or None, if id_ could be a workflow ID A valid source directory is: - an existing directory that contains a worklow config file and not a relative path (which could be a workflow ID), i.e. it must be: - the current directory (".") - or a directory path that starts with "./" - or an absolute directory path It's OK if id_ happens to match a relative path to an existing directory or file (other than a workflow config file) because there could be a workflow ID with the same name. """ abort_if_flow_file_in_path(Path(id_)) src_path = Path(id_) if ( not EXPLICIT_RELATIVE_PATH_REGEX.match(id_) and not src_path.is_absolute() ): # Not a valid source path, but it could be a workflow ID. return None src_dir_path = src_path.resolve() if not src_dir_path.exists(): raise InputError(f'Source directory not found: {src_dir_path}') if not src_dir_path.is_dir(): raise InputError(f'Path is not a source directory: {src_dir_path}') src_file_path = check_flow_file(src_dir_path) return src_dir_path.name, src_dir_path, src_file_path cylc-flow-8.6.4/cylc/flow/hostuserutil.py0000664000175000017500000002520215202510242020633 0ustar alastairalastair# 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 . """Host name utilities ATTRIBUTION: https://web.archive.org/web/20140606052543/http://www.linux-support.com/cms/get-local-ip-address-with-python/ Fetching the outgoing IP address of a computer might be a difficult task. Computers can contain a large set of network devices, each connected to different and independent sub-networks. Additionally there might be available a number of devices, to be utilized in the manner of network devices to exchange data with external systems. However, if properly configured, your operating system knows what device has to be utilized. Querying results depend on target addresses and routing information. In our solution we are utilizing the features of the local operating system to determine the correct network device. It is the same step we will get the associated network address. To reach this goal we will utilize the UDP protocol. Unlike TCP/IP, UDP is a stateless networking protocol to transfer single data packages. You do not have to open a point-to-point connection to a service running at the target host. We have to provide the target address to enable the operating system to find the correct device. Due to the nature of UDP you are not required to choose a valid target address. You just have to make sure your are choosing an arbitrary address from the correct subnet. The following function is temporarily opening a UDP server socket. It is returning the IP address associated with this socket. """ from contextlib import suppress import os import pwd import socket import sys from time import time from typing import ( List, Optional, Tuple, cast, ) from cylc.flow.cfgspec.glbl_cfg import glbl_cfg IS_MAC_OS = 'darwin' in sys.platform.lower() class HostUtil: """host and user ID utility.""" EXPIRE = 3600.0 # singleton expires in 1 hour by default _instance = None @classmethod def get_inst(cls, new=False, expire=None): """Return the singleton instance of this class. "new": if True, create a new singleton instance. "expire": the expire duration in seconds. If None or not specified, the singleton expires after 3600.0 seconds (1 hour). Once expired, the next call to this method will create a new singleton. """ if expire is None: expire = cls.EXPIRE if cls._instance is None or new or time() > cls._instance.expire_time: cls._instance = cls(expire) return cls._instance def __init__(self, expire): self.expire_time = time() + expire self._host = None # preferred name of localhost self._host_exs = {} # host: socket.gethostbyname_ex(host), ... self._remote_hosts = {} # host: is_remote, ... self.user_pwent = None self.remote_users = {} @staticmethod def get_local_ip_address(target): """Return IP address of target. This finds the external address of the particular network adapter responsible for connecting to the target? Note that although no connection is made to the target, the target must be reachable on the network (or just recorded in the DNS?) or the function will hang and time out after a few seconds. """ ipaddr = "" with suppress(IOError): # noqa: SIM117 (use of as convolutes this) with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect((target, 8000)) ipaddr = sock.getsockname()[0] return ipaddr @staticmethod def get_host_ip_by_name(target): """Return internal IP address of target.""" return socket.gethostbyname(target) def _get_host_info( self, target: Optional[str] = None ) -> Tuple[str, List[str], List[str]]: """Return the extended info of the current host.""" if target is None: target = socket.getfqdn() if IS_MAC_OS and target in { '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.' '0.0.0.0.0.0.ip6.arpa', '1.0.0.127.in-addr.arpa', }: # Python's socket bindings don't play nicely with mac os # so by default we get the above ip6.arpa address from # socket.getfqdn, note this does *not* match `hostname -f`. # https://github.com/cylc/cylc-flow/issues/2689 # https://github.com/cylc/cylc-flow/issues/3595 target = socket.gethostname() if target not in self._host_exs: try: self._host_exs[target] = socket.gethostbyname_ex(target) except IOError as exc: if exc.filename is None: exc.filename = target raise return self._host_exs[target] @staticmethod def _get_identification_cfg(key) -> str: """Return the [workflow host self-identification]key global conf.""" return cast( 'str', # Possible keys: method, target, host are all strings glbl_cfg().get(['scheduler', 'host self-identification', key]) ) def get_host(self) -> str: """Return the preferred identifier for the workflow (or current) host. As specified by the "[scheduler][host self-identification]" settings in the site/user global.cylc files. This is mainly used for workflow host identification by tasks. """ if self._host is None: hardwired = self._get_identification_cfg('host') method = self._get_identification_cfg('method') if method == 'address': self._host = self.get_local_ip_address( self._get_identification_cfg('target')) elif method == 'hardwired' and hardwired: self._host = hardwired else: # if method == 'name': self._host = self._get_host_info()[0] return self._host def get_fqdn_by_host(self, target): """Return the fully qualified domain name of the target host.""" if not self.is_remote_host(target): return self.get_host() return self._get_host_info(target)[0] def get_user(self): """Return name of current user.""" return self._get_user_pwent().pw_name def get_user_home(self): """Return home directory of current user.""" return self._get_user_pwent().pw_dir def _get_user_pwent(self): """Ensure self.user_pwent is set to current user's password entry.""" if self.user_pwent is None: my_user_name = os.environ.get('USER') if my_user_name: self.user_pwent = pwd.getpwnam(my_user_name) else: self.user_pwent = pwd.getpwuid(os.getuid()) self.remote_users.update(((self.user_pwent.pw_name, False),)) return self.user_pwent def is_remote_host(self, name): """Return True if name has different IP address than the current host. Return False if name is None. Return True if host is unknown. """ if name not in self._remote_hosts: if ( not name or name.startswith("localhost") # e.g. localhost4.localdomain4 or name == self.get_host() ): self._remote_hosts[name] = False else: try: host_info = self._get_host_info(name) except IOError: self._remote_hosts[name] = True else: self._remote_hosts[name] = ( host_info != self._get_host_info()) return self._remote_hosts[name] def is_remote_user(self, name): """Return True if name is not a name of the current user. Return False if name is None. Return True if name is not in the password database. """ if not name: return False if name not in self.remote_users: try: self.remote_users[name] = ( pwd.getpwnam(name) != self._get_user_pwent()) except KeyError: self.remote_users[name] = True return self.remote_users[name] def _is_remote_platform(self, platform): """Return True if any job host in platform have different IP address to the current host. Return False if name is None. Return True if host is unknown. Todo: Should this fail miserably if some hosts are remote and some are not? """ if not platform: return False return any( is_remote_host(host) for host in platform['hosts'] ) def get_host_ip_by_name(target): """Shorthand for HostUtil.get_inst().get_host_ip_by_name(target).""" return HostUtil.get_inst().get_host_ip_by_name(target) def get_local_ip_address(target): """Shorthand for HostUtil.get_inst().get_local_ip_address(target).""" return HostUtil.get_inst().get_local_ip_address(target) def get_host(): """Shorthand for HostUtil.get_inst().get_host().""" return HostUtil.get_inst().get_host() def get_fqdn_by_host(target): """Shorthand for HostUtil.get_inst().get_fqdn_by_host(target).""" return HostUtil.get_inst().get_fqdn_by_host(target) def get_user(): """Shorthand for HostUtil.get_inst().get_user().""" return HostUtil.get_inst().get_user() def get_user_home(): """Shorthand for HostUtil.get_inst().get_user_home().""" return HostUtil.get_inst().get_user_home() def is_remote_platform(platform): """Shorthand for HostUtil.get_inst()._is_remote_platform(host, owner).""" return HostUtil.get_inst()._is_remote_platform(platform) def is_remote_host(name): """Shorthand for HostUtil.get_inst().is_remote_host(name).""" return HostUtil.get_inst().is_remote_host(name) def is_remote_user(name): """Return True if name is not a name of the current user.""" return HostUtil.get_inst().is_remote_user(name) cylc-flow-8.6.4/cylc/flow/main_loop/0000775000175000017500000000000015202510242017463 5ustar alastairalastaircylc-flow-8.6.4/cylc/flow/main_loop/__init__.py0000664000175000017500000002450715202510242021604 0ustar alastairalastair# 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 . """Plugins for running Python code inside of the Cylc scheduler. .. _BuiltInPlugins: Built In Plugins ---------------- Cylc Flow provides the following plugins: .. autosummary:: :toctree: built-in :template: main_loop_plugin.rst cylc.flow.main_loop.auto_restart cylc.flow.main_loop.health_check cylc.flow.main_loop.log_data_store cylc.flow.main_loop.log_main_loop cylc.flow.main_loop.log_memory cylc.flow.main_loop.reset_bad_hosts .. Note: Autosummary generates files in this directory, these are cleaned up by `make clean`. Configuring ----------- Main loop plugins can be activated either by: * Using the ``--main-loop`` option with ``cylc play`` e.g: .. code-block:: console $ # run a workflow using the "health check" and "auto restart" plugins: $ cylc play my-workflow --main-loop 'health check' \ --main-loop 'auto restart' * Adding them to the default list of plugins in :cylc:conf:`global.cylc[scheduler][main loop]plugins` e.g: .. code-block:: cylc [scheduler] [[main loop]] plugins = health check, auto restart Main loop plugins can be individually configured in their :cylc:conf:`global.cylc[scheduler][main loop][]` section e.g: .. code-block:: cylc [scheduler] [[main loop]] [[[health check]]] interval = PT5M # perform check every 5 minutes Developing Main Loop Plugins ---------------------------- Main loop plugins are Python modules containing asynchronous function(s) (sometimes referred to as coroutines) which Cylc Flow executes within the scheduler. Hello World ^^^^^^^^^^^ Here is the "hello world" of main loop plugins: .. code-block:: python :caption: my_plugin.py from cylc.flow import LOG from cylc.flow.main_loop import startup @startup async def my_startup_coroutine(schd, state): # write Hello to the Cylc log. LOG.info(f'Hello {schd.workflow}') Plugins are registered by registering them with the ``cylc.main_loop`` entry point: .. code-block:: python :caption: setup.py # plugins must be properly installed, in-place PYTHONPATH meddling will # not work. from setuptools import setup setup( name='my-plugin', version='1.0', py_modules=['my_plugin'], entry_points={ # register this plugin with Cylc 'cylc.main_loop': [ # name = python.namespace.of.module 'my_plugin=my_plugin.my_plugin' ] } ) Examples ^^^^^^^^ For examples see the built-in plugins in the :py:mod:`cylc.flow.main_loop` module which are registered in the Cylc Flow ``setup.cfg`` file. Coroutines ^^^^^^^^^^ .. _coroutines: https://docs.python.org/3/library/asyncio-task.html#coroutines Plugins provide asynchronous functions (`coroutines`_) which Cylc will then run inside the scheduler. Coroutines should be fast running (read as gentle on the scheduler) and perform IO asynchronously. Coroutines shouldn't meddle with the state of the scheduler and should be parallel-safe with other plugins. Event Types ^^^^^^^^^^^ Coroutines must be decorated using one of the main loop decorators. The choice of decorator effects when the coroutine is called and what arguments are provided to it. The available event types are: .. autofunction:: cylc.flow.main_loop.startup .. autofunction:: cylc.flow.main_loop.shutdown .. autofunction:: cylc.flow.main_loop.periodic """ from collections import deque from inspect import ( getmembers, isfunction ) from textwrap import indent from time import time from cylc.flow import LOG, iter_entry_points from cylc.flow.exceptions import CylcError, InputError, PluginError class MainLoopPluginException(Exception): """Raised in-place of CylcError exceptions. Note: * Not an instace of CylcError as that is used for controlled shutdown e.g. SchedulerStop. """ async def _wrapper(fcn, scheduler, state, timings=None): """Wrapper for all plugin functions. * Logs the function's execution. * Times the function. * Catches any exceptions which aren't subclasses of CylcError. """ sig = f'{fcn.__module__}:{fcn.__name__}' LOG.debug(f'main_loop [run] {sig}') start_time = time() try: await fcn(scheduler, state) except CylcError as exc: # allow CylcErrors through (e.g. SchedulerStop) # NOTE: the `from None` bit gets rid of this gunk: # > During handling of the above exception another exception raise MainLoopPluginException(exc) from None except Exception as exc: LOG.error(f'Error in main loop plugin {sig}') LOG.exception(exc) duration = time() - start_time LOG.debug(f'main_loop [end] {sig} ({duration:.3f}s)') if timings is not None: timings.append((start_time, duration)) def _debounce(interval, timings): """Rate limiter, returns True if the interval has elapsed. Arguments: interval (float): Time interval in seconds as a float-type object. timings (list): List-list object of the timings of previous runs in the form ``(completion_wallclock_time, run_duration)``. Wallclock times are unix epoch times in seconds. Examples: >>> from time import time No previous run (should always return True): >>> _debounce(1., [(0, 0)]) True Interval not yet elapsed since previous run: >>> _debounce(1., [(time(), 0)]) False Interval has elapsed since previous run: >>> _debounce(1., [(time() - 2, 0)]) True """ if not interval: return True try: last_run_at = timings[-1][0] except IndexError: last_run_at = 0 if (time() - last_run_at) > interval: return True return False def startup(fcn): """Decorates a coroutine which is run at workflow startup. The decorated coroutine should have the signature: ``async coroutine(scheduler, plugin_state) -> None`` Exceptions: * Regular Exceptions are caught and logged. * Exceptions which subclass CylcError are re-raised as MainLoopPluginException """ fcn.main_loop = CoroTypes.StartUp return fcn def shutdown(fcn): """Decorates a coroutine which is run at workflow shutdown. Note shutdown refers to "clean" shutdown as opposed to workflow abort. The decorated coroutine should have the signature: ``async coroutine(scheduler, plugin_state) -> None`` Exceptions: * Regular Exceptions are caught and logged. * Exceptions which subclass CylcError are re-raised as MainLoopPluginException """ fcn.main_loop = CoroTypes.ShutDown return fcn def periodic(fcn): """Decorates a coroutine which is run at a set interval. The decorated coroutine should have the signature: ``async coroutine(scheduler, plugin_state) -> None`` Exceptions: * Regular Exceptions are caught and logged. * Exceptions which subclass CylcError are re-raised as MainLoopPluginException Configuration: * The interval of execution can be altered using :cylc:conf:`global.cylc[scheduler][main loop][]interval` """ fcn.main_loop = CoroTypes.Periodic return fcn class CoroTypes: """Different types of coroutine which can be used with the main loop.""" StartUp = startup ShutDown = shutdown Periodic = periodic def load(config, additional_plugins=None): additional_plugins = additional_plugins or [] entry_points = { entry_point.name: entry_point for entry_point in iter_entry_points('cylc.main_loop') } plugins = { 'state': {}, 'timings': {} } for plugin_name in set(config['plugins'] + additional_plugins): # get plugin try: entry_point = entry_points[plugin_name.replace(' ', '_')] except KeyError: raise InputError( f'No main-loop plugin: "{plugin_name}"\n' + ' Available plugins:\n' + indent('\n'.join(sorted(entry_points)), ' ') ) from None # load plugin try: module = entry_point.load() except Exception as exc: raise PluginError( 'cylc.main_loop', entry_point.name, exc ) from None # load coroutines log = [] for coro_name, coro in getmembers(module): if isfunction(coro) and hasattr(coro, 'main_loop'): log.append(coro_name) plugins.setdefault( coro.main_loop, {} )[(plugin_name, coro_name)] = coro plugins['timings'][(plugin_name, coro_name)] = deque(maxlen=1) LOG.debug( 'Loaded main loop plugin "%s":\n%s', plugin_name, '\n'.join(f'* {x}' for x in log) ) # set the initial state of the plugin plugins['state'][plugin_name] = {} # make a note of the config here for ease of reference plugins['config'] = config return plugins def get_runners(plugins, coro_type, scheduler): return [ _wrapper( coro, scheduler, plugins['state'][plugin_name], timings=plugins['timings'][(plugin_name, coro_name)] ) for (plugin_name, coro_name), coro in plugins.get(coro_type, {}).items() if coro_type != CoroTypes.Periodic or _debounce( plugins['config'].get(plugin_name, {}).get('interval', None), plugins['timings'][(plugin_name, coro_name)] ) ] cylc-flow-8.6.4/cylc/flow/main_loop/log_main_loop.py0000664000175000017500000000570515202510242022662 0ustar alastairalastair# 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 . """Main loop plugin for monitoring main loop plugins. .. note:: This plugin is for Cylc developers debugging main loop operations. If ``matplotlib`` is installed this plugin will plot results as a PDF in the run directory when the workflow is shut down (cleanly). """ from collections import deque import json from pathlib import Path from cylc.flow.main_loop import startup, shutdown try: import matplotlib matplotlib.use('Agg') from matplotlib import pyplot as plt PLT = True except ModuleNotFoundError: PLT = False @startup async def init(scheduler, _): """Override default queue length of 1. This allows timings to accumulate, normally only the most recent is kept. """ plugins = scheduler.main_loop_plugins for plugin in plugins['timings']: plugins['timings'][plugin] = deque() @shutdown async def report(scheduler, _): """Extract plugin function timings.""" data = scheduler.main_loop_plugins['timings'] if data: data = _normalise(data) _dump(data, scheduler.workflow_run_dir) _plot(data, scheduler.workflow_run_dir) def _normalise(data): earliest_time = min(( start_time for _, timings in data.items() for start_time, duration in timings )) return { plugin_name: [ (start_time - earliest_time, duration) for start_time, duration in timings ] for (plugin_name, _), timings in data.items() } def _dump(data, path): json.dump( data, Path(path, f'{__name__}.json').open('w+'), indent=4 ) return True def _plot(data, path): if not PLT: return False _, ax1 = plt.subplots(figsize=(10, 7.5)) ax1.set_xlabel('Workflow Run Time (s)') ax1.set_ylabel('XTrigger Run Time (s)') for plugin_name, (timings) in data.items(): x_data = [] y_data = [] for start_time, duration in timings: x_data.append(start_time) y_data.append(duration) ax1.scatter(x_data, y_data, label=plugin_name) ax1.set_xlim(0, ax1.get_xlim()[1]) ax1.set_ylim(0, ax1.get_ylim()[1]) ax1.legend() plt.savefig( Path(path, f'{__name__}.pdf') ) return True cylc-flow-8.6.4/cylc/flow/main_loop/health_check.py0000664000175000017500000000423315202510242022441 0ustar alastairalastair# 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 . """Checks the integrity of the workflow run directory. * Ensures workflow run directory is still present. * Ensures contact file is present and consistent with the running workflow. Shuts down the workflow in the event of inconsistency or error. """ import os from cylc.flow import workflow_files from cylc.flow.exceptions import CylcError, ServiceFileError from cylc.flow.main_loop import periodic @periodic async def health_check(scheduler, _): """Perform workflow health checks.""" # 1. check if workflow run dir still present - if not shutdown. _check_workflow_run_dir(scheduler) # 2. check if contact file consistent with current start - if not # shutdown. _check_contact_file(scheduler) def _check_workflow_run_dir(scheduler): if not os.path.exists(scheduler.workflow_run_dir): raise CylcError( 'Workflow run directory does not exist:' f' {scheduler.workflow_run_dir}' ) def _check_contact_file(scheduler): try: contact_data = workflow_files.load_contact_file( scheduler.workflow) if contact_data != scheduler.contact_data: raise CylcError('contact file modified') except (AssertionError, IOError, ValueError, ServiceFileError) as exc: raise CylcError( '%s: contact file corrupted/modified and may be left' % workflow_files.get_contact_file_path(scheduler.workflow) ) from exc cylc-flow-8.6.4/cylc/flow/main_loop/log_data_store.py0000664000175000017500000001157615202510242023035 0ustar alastairalastair# 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 . """Log the number and size of each type of object in the data store. .. note:: This plugin is for Cylc developers debugging the data store. If ``matplotlib`` is installed this plugin will plot results as a PDF in the run directory when the workflow is shut down (cleanly). """ import json from pathlib import Path from time import time from cylc.flow.main_loop import (startup, shutdown, periodic) try: import matplotlib matplotlib.use('Agg') from matplotlib import pyplot as plt PLT = True except ModuleNotFoundError: PLT = False from pympler.asizeof import asizeof @startup async def init(scheduler, state): """Construct the initial state.""" state['objects'] = {} state['size'] = {} state['times'] = [] for key, _ in _iter_data_store(scheduler.data_store_mgr): state['objects'][key] = [] state['size'][key] = [] @periodic async def log_data_store(scheduler, state): """Count the number of objects and the data store size.""" state['times'].append(time()) for key, value in _iter_data_store(scheduler.data_store_mgr): if isinstance(value, (list, dict, set)): state['objects'][key].append( len(value) ) state['size'][key].append( asizeof(value) ) @shutdown async def report(scheduler, state): """Dump data to JSON, attempt to plot results.""" _dump(state, scheduler.workflow_run_dir) _plot(state, scheduler.workflow_run_dir) def _iter_data_store(data_store_mgr): # the data store itself (for a total measurement) yield ('data_store_mgr (total)', data_store_mgr) # the top-level attributes of the data store for key in dir(data_store_mgr): if ( key != 'data' and not key.startswith('__') and isinstance( value := getattr(data_store_mgr, key), (list, dict, set) ) ): yield (key, value) # the individual components of the "data" attribute for datum in data_store_mgr.data.values(): for key, value in datum.items(): if key == 'workflow': yield (f'data.{key}', [value]) else: yield (f'data.{key}', value) # there should only be one workflow in the data store break def _dump(state, path): data = { 'times': state['times'], 'objects': state['objects'], 'size': state['size'] } json.dump( data, Path(path, f'{__name__}.json').open('w+') ) return True def _plot(state, path, min_size_percent=2): if ( not PLT or len(state['times']) < 2 ): return False # extract snapshot times times = [tick - state['times'][0] for tick in state['times']] max_size = max( size for sizes in state['size'].values() for size in sizes ) # filter attributes by the minimum size min_size_bytes = max_size * (min_size_percent / 100) filtered_keys = { key for key, sizes in state['size'].items() if ( any(size > min_size_bytes for size in sizes) or key.startswith('data.') ) } # plot fig = plt.figure(figsize=(15, 8)) ax1 = fig.add_subplot(111) fig.suptitle( f'data_store_mgr data and attrs above {min_size_percent}% of largest' f' (> {int(min_size_bytes / 1000)}kb)' ) # plot sizes ax1.set_xlabel('Time (s)') ax1.set_ylabel('Size (kb)') for key, sizes in state['size'].items(): if key in filtered_keys: ax1.plot(times, [x / 1000 for x in sizes], label=key) # plot # objects ax2 = ax1.twinx() ax2.set_ylabel('Objects') for key, objects in state['objects'].items(): if objects and key in filtered_keys: ax2.plot(times, objects, label=key, linestyle=':') # legends ax1.legend(loc=0) ax2.legend( (ax1.get_children()[0], ax2.get_children()[0]), ('size', 'objects'), loc=0 ) # start the x-axis at zero ax1.set_xlim(0, ax1.get_xlim()[1]) plt.savefig( Path(path, f'{__name__}.pdf') ) return True cylc-flow-8.6.4/cylc/flow/main_loop/log_memory.py0000664000175000017500000001126715202510242022215 0ustar alastairalastair# 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 . """Log the memory usage of a running scheduler over time. .. note:: This plugin is for Cylc developers debugging cylc memory usage. For general interest memory measurement try ``/usr/bin/time -v cylc play`` or ``cylc play --profile``. .. note:: Pympler associates memory with the first object which references it. In Cylc we have some objects (e.g. the configuration) which are references from multiple places. This can result in a certain amount of "jitter" in the results where pympler has swapper from associating memory with one object to another. Watch out for matching increase/decrease in reported memory in different objects. .. warning:: This plugin can slow down a workflow significantly due to the complexity of memory calculations. Set a sensible interval before running workflows. If ``matplotlib`` is installed this plugin will plot results as a PDF in the run directory when the workflow is shut down (cleanly). """ import json from pathlib import Path from time import time from cylc.flow.main_loop import (startup, shutdown, periodic) try: import matplotlib matplotlib.use('Agg') from matplotlib import pyplot as plt PLT = True except ModuleNotFoundError: PLT = False from pympler.asizeof import asized # TODO: make this configurable in the global config MIN_SIZE = 10000 @startup async def init(scheduler, state): """Take an initial memory snapshot.""" state['data'] = [] await take_snapshot(scheduler, state) @periodic async def take_snapshot(scheduler, state): """Take a memory snapshot""" state['data'].append(( time(), _compute_sizes(scheduler, min_size=MIN_SIZE) )) @shutdown async def report(scheduler, state): """Take a final memory snapshot and dump the results.""" await take_snapshot(scheduler, state) _dump(state['data'], scheduler.workflow_run_dir) fields, times = _transpose(state['data']) _plot( fields, times, scheduler.workflow_run_dir, f'cylc.flow.scheduler.Scheduler attrs > {MIN_SIZE / 1000}kb' ) def _compute_sizes(obj, min_size=10000): """Return the sizes of the attributes of an object.""" size = asized(obj, detail=2) for ref in size.refs: if ref.name == '__dict__': break else: raise Exception('Cannot find __dict__ reference') return { **{ item.name.split(':')[0][4:]: item.size for item in ref.refs if item.size > min_size }, **{'total': size.size}, } def _transpose(data): """Pivot data from snapshot to series oriented.""" all_keys = set() for _, datum in data: all_keys.update(datum.keys()) # sort keys by the size of the last checkpoint so that the fields # get plotted from largest to smallest all_keys = list(all_keys) all_keys.sort(key=lambda x: data[-1][1].get(x, 0), reverse=True) # extract data for each field, if not present fields = {} for key in all_keys: fields[key] = [ datum.get(key, -1) for _, datum in data ] start_time = data[0][0] times = [ timestamp - start_time for timestamp, _ in data ] return fields, times def _dump(data, path): json.dump( data, Path(path, f'{__name__}.json').open('w+') ) return True def _plot(fields, times, path, title='Objects'): if ( not PLT or len(times) < 2 ): return False fig, ax1 = plt.subplots(figsize=(10, 7.5)) fig.suptitle(title) ax1.set_xlabel('Time (s)') ax1.set_ylabel('Memory (kb)') lines = [ ax1.plot(times, [x / 1000 for x in sizes], label=key)[0] for key, sizes in fields.items() ] ax1.legend(lines, fields, loc=0) # start both axis at 0 ax1.set_xlim(0, ax1.get_xlim()[1]) ax1.set_ylim(0, ax1.get_ylim()[1]) plt.savefig( Path(path, f'{__name__}.pdf') ) return True cylc-flow-8.6.4/cylc/flow/main_loop/log_db.py0000664000175000017500000000507615202510242021273 0ustar alastairalastair# 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 . """Log all database transactions. .. note:: This plugin is for Cylc developers debugging database issues. Writes an SQL file into the workflow run directory on shutown. """ import logging from logging.handlers import RotatingFileHandler from pathlib import Path import sqlparse from cylc.flow import CYLC_LOG from cylc.flow.main_loop import (startup, shutdown) DB_LOG = logging.getLogger(f'{CYLC_LOG}-db') def _format(sql_string): """Pretty print an SQL statement.""" return '\n'.join( sqlparse.format( statement, reindent_aligned=True, use_space_around_operators=True, strip_comments=True, keyword_case='upper', identifier_case='lower', ) for statement in sqlparse.split(sql_string) ) + '\n' def _log(sql_string): """Log a SQL string.""" DB_LOG.info(_format(sql_string)) def _patch_db_connect(db_connect_method): """Patch the workflow DAO to configure logging. We patch the connect method so that any subsequent re-connections are also patched. """ def _inner(*args, **kwargs): conn = db_connect_method(*args, **kwargs) conn.set_trace_callback(_log) return conn return _inner @startup async def init(scheduler, state): # configure log handler DB_LOG.setLevel(logging.INFO) handler = RotatingFileHandler( str(Path(scheduler.workflow_run_dir, f'{__name__}.sql')), maxBytes=1000000, ) state['log_handler'] = handler DB_LOG.addHandler(handler) # configure the DB manager to log all DB operations scheduler.workflow_db_mgr.pri_dao.connect = _patch_db_connect( scheduler.workflow_db_mgr.pri_dao.connect ) @shutdown async def stop(scheduler, state): handler = state.get('log_handler') if handler: handler.close() cylc-flow-8.6.4/cylc/flow/main_loop/reset_bad_hosts.py0000664000175000017500000000265715202510242023217 0ustar alastairalastair# 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 . """Resets the list of bad hosts. The scheduler stores a set of hosts which it has been unable to contact to save contacting these hosts again. This list is cleared if a task cannot be submitted because all of the hosts it might use cannot be reached. If a task succeeds in submitting a job on the second host it tries, then the first host remains in the set of unreachable (bad) hosts, even though the failure might have been transitory. For this reason, this plugin periodically clears the set. Suggested interval - an hour. """ from cylc.flow.main_loop import periodic @periodic async def reset_bad_hosts(scheduler, _): """Empty bad_hosts.""" scheduler.task_events_mgr.reset_bad_hosts() cylc-flow-8.6.4/cylc/flow/main_loop/auto_restart.py0000664000175000017500000002250015202510242022550 0ustar alastairalastair# 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 . """Automatically restart workflows if they are running on bad servers. Loads in the global configuration to check if the server a workflow is running on is listed in :cylc:conf:`global.cylc[scheduler][run hosts]condemned`. This is useful if a host needs to be taken off-line e.g. for scheduled maintenance. This functionality is configured via the following site configuration settings: .. cylc-scope:: global.cylc - :cylc:conf:`[scheduler]auto restart delay` - :cylc:conf:`[scheduler][run hosts]condemned` - :cylc:conf:`[scheduler][run hosts]available` The auto stop-restart feature has two modes: Normal Mode When a host is added to the :cylc:conf:`[scheduler][run hosts]condemned` list, any workflows running on that host will automatically shutdown then restart selecting a new host from :cylc:conf:`[scheduler][run hosts]available`. For safety, before attempting to stop the workflow Cylc will first wait for any jobs running locally (under background or at) to complete. In order for Cylc to be able to restart workflows the :cylc:conf:`[scheduler][run hosts]available` hosts must all be on a shared filesystem. Force Mode If a host is suffixed with an exclamation mark then Cylc will not attempt to automatically restart the workflow and any local jobs (running under background or at) will be left running. For example in the following configuration any workflows running on ``foo`` will attempt to restart on ``pub`` whereas any workflows running on ``bar`` will stop immediately, making no attempt to restart. .. code-block:: cylc [scheduler] [[run hosts]] available = pub condemned = foo, bar! .. warning:: Cylc will reject hosts with ambiguous names such as ``localhost`` or ``127.0.0.1`` for this configuration as :cylc:conf:`[scheduler][run hosts]condemned` are evaluated on the workflow host server. To prevent large numbers of workflows attempting to restart simultaneously the :cylc:conf:`[scheduler]auto restart delay` setting defines a period of time in seconds. Workflows will wait for a random period of time between zero and :cylc:conf:`[scheduler]auto restart delay` seconds before attempting to stop and restart. Workflows that are started up in no-detach mode cannot auto stop-restart on a different host - as it will still end up attached to the condemned host. Therefore, a workflow in no-detach mode running on a condemned host will abort with a non-zero return code. The parent process should manually handle the restart of the workflow if desired. .. cylc-scope:: """ from random import random from time import time import traceback from cylc.flow import LOG from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.exceptions import ( CylcConfigError, HostSelectException, ) from cylc.flow.host_select import select_workflow_host from cylc.flow.hostuserutil import get_fqdn_by_host from cylc.flow.main_loop import periodic from cylc.flow.parsec.exceptions import ParsecError from cylc.flow.scheduler import SchedulerError from cylc.flow.wallclock import get_time_string_from_unix_time as time2str from cylc.flow.workflow_status import AutoRestartMode @periodic async def auto_restart(scheduler, _): """Automatically restart the workflow if configured to do so.""" try: current_glbl_cfg = glbl_cfg(cached=False) except (CylcConfigError, ParsecError) as exc: LOG.error( 'auto restart: an error in the global config is preventing it from' f' being reloaded:\n{exc}' ) # skip check - we can't do anything until the global config has been # fixed return False # return False to make testing easier mode = _should_auto_restart(scheduler, current_glbl_cfg) if mode: LOG.info('The Cylc workflow host will soon become un-available.') _set_auto_restart( scheduler, restart_delay=current_glbl_cfg.get( ['scheduler', 'auto restart delay'] ), mode=mode ) def _should_auto_restart(scheduler, current_glbl_cfg): # check if workflow host is condemned - if so auto restart if scheduler.stop_mode is None: for host in current_glbl_cfg.get( ['scheduler', 'run hosts', 'condemned'] ): if host.endswith('!'): # host ends in an `!` -> force shutdown mode mode = AutoRestartMode.FORCE_STOP host = host[:-1] else: # normal mode (stop and restart the workflow) mode = AutoRestartMode.RESTART_NORMAL if scheduler.auto_restart_time is not None: # workflow is already scheduled to stop-restart only # AutoRestartMode.FORCE_STOP can override this. continue if get_fqdn_by_host(host) == scheduler.host: # this host is condemned, take the appropriate action return mode return False def _can_auto_restart(): """Determine whether this workflow can safely auto stop-restart.""" # Check whether there is currently an available host to restart on. err_msg = 'Workflow cannot automatically restart' try: select_workflow_host(cached=False) except HostSelectException as exc: LOG.critical( f"{err_msg}: No alternative host to restart workflow on.\n{exc}" ) return False except Exception: # Any unexpected error in host selection shouldn't be able to take # down the workflow. LOG.critical( f"{err_msg}: Error in host selection.\n{traceback.format_exc()}" ) return False else: return True def _set_auto_restart( scheduler, restart_delay=None, mode=AutoRestartMode.RESTART_NORMAL ): """Configure the workflow to automatically stop and restart. Restart handled by `workflow_auto_restart`. Args: scheduler (cylc.flow.scheduler.Scheduler): Scheduler instance of the running workflow. restart_delay (cylc.flow.parsec.DurationFloat): Workflow will wait a random period between 0 and `restart_delay` seconds before attempting to stop/restart in order to avoid multiple workflows restarting simultaneously. mode (str): Auto stop-restart mode. Return: bool: False if it is not possible to automatically stop/restart the workflow due to its configuration/runtime state. """ # Check that the workflow isn't already shutting down. if scheduler.stop_mode: return True # Force mode, stop the workflow now, don't restart it. if mode == AutoRestartMode.FORCE_STOP: LOG.critical( 'This workflow will be shutdown as the workflow ' 'host is unable to continue running it.\n' 'When another workflow host becomes available ' 'the workflow can be restarted by:\n' f' $ cylc play {scheduler.workflow}') if scheduler.auto_restart_time: LOG.info('Scheduled automatic restart canceled') scheduler.auto_restart_time = time() scheduler.auto_restart_mode = mode return True # Check workflow isn't already scheduled to auto-stop. if scheduler.auto_restart_time is not None: return True # Workflow host is condemned and workflow running in no detach mode. # Raise an error to cause the workflow to abort. # This should raise an "abort" event and return a non-zero code to the # caller still attached to the workflow process. if scheduler.options.no_detach: raise SchedulerError('Workflow host condemned in no detach mode') # Check workflow is able to be safely restarted. if not _can_auto_restart(): return False LOG.info('Workflow will automatically restart on a new host.') if restart_delay is not None and restart_delay != 0: if restart_delay > 0: # Delay shutdown by a random interval to avoid many # workflows restarting simultaneously. shutdown_delay = int(random() * restart_delay) # nosec else: # Un-documented feature, schedule exact restart interval for # testing purposes. shutdown_delay = abs(int(restart_delay)) shutdown_time = time() + shutdown_delay LOG.info('Workflow will restart in %ss (at %s)', shutdown_delay, time2str(shutdown_time)) scheduler.auto_restart_time = shutdown_time else: scheduler.auto_restart_time = time() scheduler.auto_restart_mode = AutoRestartMode.RESTART_NORMAL return True cylc-flow-8.6.4/cylc/flow/flow_mgr.py0000664000175000017500000001665215202510242017706 0ustar alastairalastair# 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 . """Manage flow counter and flow metadata.""" import datetime from typing import ( TYPE_CHECKING, Dict, Iterable, List, Optional, Set, ) from cylc.flow import LOG if TYPE_CHECKING: from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager FlowNums = Set[int] FLOW_NEW = "new" FLOW_NONE = "none" def add_flow_opts_for_trigger_and_set(parser): """Add flow options for the trigger and set commands.""" parser.add_option( "--flow", action="append", dest="flow", metavar=f"INT|{FLOW_NEW}|{FLOW_NONE}", default=[], help=( 'Assign affected tasks to specified flows.' # nosec # (false positive, this is not an SQL statement Bandit!) ' By default, active tasks (n=0) stay in their assigned flow(s)' ' and inactive tasks (n>0) will be assigned to all active flows.' ' Use this option to manually specify an integer flow to assign' ' tasks to, e.g, "--flow=2". Use this option multiple times to' ' select multiple flows.' f' Alternatively, use "--flow={FLOW_NEW}" to start a new' f' flow, or "--flow={FLOW_NONE}" to trigger an inactive task in' ' no flows (this means the workflow will not run on from the' ' triggered task, only works for inactive tasks).' ) ) parser.add_option( "--meta", metavar="DESCRIPTION", action="store", dest="flow_descr", default=None, help=f"description of new flow (with --flow={FLOW_NEW})." ) parser.add_option( "--wait", action="store_true", default=False, dest="flow_wait", help="Wait for merge with current active flows before flowing on." " Note you can use 'cylc set --pre=all' to unset a flow-wait." ) def add_flow_opts_for_remove(parser): """Add flow options for the remove command.""" parser.add_option( '--flow', action='append', dest='flow', metavar='INT', default=[], help=( "Remove the task(s) from the specified flow." " Use this option multiple times to specify multiple flows." " By default, the tasks will be removed from all flows." ), ) def stringify_flow_nums(flow_nums: Iterable[int]) -> str: """Return the canonical string for a set of flow numbers. Examples: >>> stringify_flow_nums({1}) '1' >>> stringify_flow_nums({3, 1, 2}) '1,2,3' >>> stringify_flow_nums({}) '' """ return ','.join(str(i) for i in sorted(flow_nums)) def repr_flow_nums(flow_nums: FlowNums, full: bool = False) -> str: """Return a representation of a set of flow numbers If `full` is False, return an empty string for flows=1. Examples: >>> repr_flow_nums({}) '(flows=none)' >>> repr_flow_nums({1}) '' >>> repr_flow_nums({1}, full=True) '(flows=1)' >>> repr_flow_nums({1,2,3}) '(flows=1,2,3)' """ if not full and flow_nums == {1}: return "" return f"(flows={stringify_flow_nums(flow_nums) or 'none'})" class FlowMgr: """Logic to manage flow counter and flow metadata.""" def __init__( self, db_mgr: "WorkflowDatabaseManager", utc: bool = True ) -> None: """Initialise the flow manager.""" self.db_mgr = db_mgr self.flows: Dict[int, Dict[str, str]] = {} self.counter: int = 0 self._timezone = datetime.timezone.utc if utc else None def cli_to_flow_nums( self, flow: List[str], meta: Optional[str] = None, ) -> Set[int]: """Convert validated --flow command options to valid int flow numbers. Args: flow: Strings: [int,], or [FLOW_NEW], or [FLOW_NONE]. meta: Flow description, for FLOW_NEW. Returns: Set of int flow nums. Note empty set can mean no-flow (FLOW_NONE); or all flows or all active flows (for default empty inputs). """ if flow == [FLOW_NONE]: return set() if flow == [FLOW_NEW]: return {self.get_flow(meta=meta)} return { self.get_flow(flow_num=int(n), meta=meta) for n in flow } def get_flow( self, flow_num: Optional[int] = None, meta: Optional[str] = None ) -> int: """Record and return a valid flow number. If asked for a new flow: - increment the automatic counter to find an unused number If given a flow number: - record a new flow if the number is unused - or just return it as an existing flow number The metadata string is only stored if it is a new flow. """ if flow_num is None: self.counter += 1 while self.counter in self.flows: # Skip manually-created out-of-sequence flows. self.counter += 1 flow_num = self.counter if flow_num in self.flows: if meta is not None: LOG.warning( f'Ignoring flow metadata "{meta}":' f' {flow_num} is not a new flow' ) else: # Record a new flow. now_sec = datetime.datetime.now(tz=self._timezone).isoformat( timespec="seconds" ) meta = meta or "no description" self.flows[flow_num] = { "description": meta, "start_time": now_sec } LOG.info( f"New flow: {flow_num} ({meta}) {now_sec}" ) self.db_mgr.put_insert_workflow_flows( flow_num, self.flows[flow_num] ) return flow_num def load_from_db(self, flow_nums: FlowNums) -> None: """Load flow data for scheduler restart. Sets the flow counter to the max flow number in the DB. Loads metadata for selected flows (those in the task pool at startup). """ self.counter = self.db_mgr.pri_dao.select_workflow_flows_max_flow_num() self.flows = self.db_mgr.pri_dao.select_workflow_flows(flow_nums) self._log() def _log(self) -> None: """Write current flow info to log.""" if not self.flows: LOG.info("Flows: (none)") return LOG.info( "Flows:\n" + "\n".join( ( f"flow: {f} " f"({self.flows[f]['description']}) " f"{self.flows[f]['start_time']}" ) for f in self.flows ) ) cylc-flow-8.6.4/cylc/flow/task_outputs.py0000664000175000017500000005175715202510242020644 0ustar alastairalastair# 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 . """Task output message manager and constants.""" import ast import re from typing import ( TYPE_CHECKING, Dict, Iterable, Iterator, List, Literal, Optional, Tuple, Union, ) from cylc.flow.exceptions import InvalidCompletionExpression from cylc.flow.util import ( BOOL_SYMBOLS, get_variable_names, restricted_evaluator, ) if TYPE_CHECKING: from cylc.flow.taskdef import TaskDef # Standard task output strings, used for triggering. TASK_OUTPUT_EXPIRED = "expired" TASK_OUTPUT_SUBMITTED = "submitted" TASK_OUTPUT_SUBMIT_FAILED = "submit-failed" TASK_OUTPUT_STARTED = "started" TASK_OUTPUT_SUCCEEDED = "succeeded" TASK_OUTPUT_FAILED = "failed" TASK_OUTPUT_FINISHED = "finished" SORT_ORDERS = ( TASK_OUTPUT_EXPIRED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, ) TASK_OUTPUTS = ( TASK_OUTPUT_EXPIRED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, TASK_OUTPUT_FINISHED, ) # DB output message for forced completion FORCED_COMPLETION_MSG = "(manually completed)" # this evaluates task completion expressions CompletionEvaluator = restricted_evaluator( # expressions ast.Expression, # variables ast.Name, ast.Load, # operations ast.BoolOp, ast.And, ast.Or, ast.BinOp, error_class=InvalidCompletionExpression, ) # regex for splitting expressions into individual parts for formatting RE_EXPR_SPLIT = re.compile(r'([\(\) ])') def trigger_to_completion_variable(output: str) -> str: """Turn a trigger into something that can be used in an expression. Examples: >>> trigger_to_completion_variable('succeeded') 'succeeded' >>> trigger_to_completion_variable('submit-failed') 'submit_failed' """ return output.replace('-', '_') def get_trigger_completion_variable_maps(triggers: Iterable[str]): """Return a bi-map of trigger to completion variable. Args: triggers: All triggers for a task. Returns: (trigger_to_completion_variable, completion_variable_to_trigger) Tuple of mappings for converting in either direction. """ _trigger_to_completion_variable = {} _completion_variable_to_trigger = {} for trigger in triggers: compvar = trigger_to_completion_variable(trigger) _trigger_to_completion_variable[trigger] = compvar _completion_variable_to_trigger[compvar] = trigger return ( _trigger_to_completion_variable, _completion_variable_to_trigger, ) def get_completion_expression(tdef: 'TaskDef') -> str: """Return a completion expression for this task definition. If there is *not* a user provided completion statement: 1. Create a completion expression that ensures all required ouputs are completed. 2. If success is optional add "or succeeded or failed" onto the end. 3. If submission is optional add "or submit-failed" onto the end of it. 4. If expiry is optional add "or expired" onto the end of it. """ # check if there is a user-configured completion expression completion = tdef.rtconfig.get('completion') if completion: # completion expression is defined in the runtime -> return it return completion # (1) start with an expression that ensures all required outputs are # generated (if the task runs) required = { trigger_to_completion_variable(trigger) for trigger, (_message, required) in tdef.outputs.items() if required } parts = [] if required: _part = ' and '.join(sorted(required)) if len(required) > 1: # wrap the expression in brackets for clarity parts.append(f'({_part})') else: parts.append(_part) # (2) handle optional success if ( tdef.outputs[TASK_OUTPUT_SUCCEEDED][1] is False or tdef.outputs[TASK_OUTPUT_FAILED][1] is False ): # failure is tolerated -> ensure the task succeeds OR fails if required: # required outputs are required only if the task actually runs parts = [ f'({parts[0]} and {TASK_OUTPUT_SUCCEEDED})' f' or {TASK_OUTPUT_FAILED}' ] else: parts.append( f'{TASK_OUTPUT_SUCCEEDED} or {TASK_OUTPUT_FAILED}' ) # (3) handle optional submission if ( tdef.outputs[TASK_OUTPUT_SUBMITTED][1] is False or tdef.outputs[TASK_OUTPUT_SUBMIT_FAILED][1] is False ): # submit-fail tolerated -> ensure the task executes OR submit-fails parts.append( trigger_to_completion_variable(TASK_OUTPUT_SUBMIT_FAILED) ) # (4) handle optional expiry if tdef.outputs[TASK_OUTPUT_EXPIRED][1] is False: # expiry tolerated -> ensure the task executes OR expires parts.append(TASK_OUTPUT_EXPIRED) return ' or '.join(parts) def get_optional_outputs( expression: str, outputs: Iterable[str], disable: "Optional[str]" = None ) -> Dict[str, Optional[bool]]: """Determine which outputs in an expression are optional. Args: expression: The completion expression. outputs: All outputs that apply to this task. disable: Disable this output and any others it is joined with by `and` (which will mean they are necessarily optional). Returns: dict: compvar: is_optional compvar: The completion variable, i.e. the trigger as used in the completion expression. is_optional: * True if var is optional. * False if var is required. * None if var is not referenced. Examples: >>> sorted(get_optional_outputs( ... '(succeeded and (x or y)) or failed', ... {'succeeded', 'x', 'y', 'failed', 'expired'} ... ).items()) [('expired', None), ('failed', True), ('succeeded', True), ('x', True), ('y', True)] >>> sorted(get_optional_outputs( ... '(succeeded and x and y) or expired', ... {'succeeded', 'x', 'y', 'failed', 'expired'} ... ).items()) [('expired', True), ('failed', None), ('succeeded', False), ('x', False), ('y', False)] >>> sorted(get_optional_outputs( ... '(succeeded and towel) or (failed and bugblatter)', ... {'succeeded', 'towel', 'failed', 'bugblatter'}, ... ).items()) [('bugblatter', True), ('failed', True), ('succeeded', True), ('towel', True)] >>> sorted(get_optional_outputs( ... '(succeeded and towel) or (failed and bugblatter)', ... {'succeeded', 'towel', 'failed', 'bugblatter'}, ... disable='failed' ... ).items()) [('bugblatter', True), ('failed', True), ('succeeded', False), ('towel', False)] """ # determine which triggers are used in the expression used_compvars = get_variable_names(expression) # all completion variables which could appear in the expression all_compvars = {trigger_to_completion_variable(out) for out in outputs} # Allows exclusion of additional outcomes: extra_excludes = {disable: False} if disable else {} return { # output: is_optional # the outputs that are used in the expression **{ output: CompletionEvaluator( expression, **{ **{out: out != output for out in all_compvars}, # don't consider pre-execution conditions as optional # (pre-conditions are considered separately) 'expired': False, 'submit_failed': False, **extra_excludes }, ) for output in used_compvars }, **dict.fromkeys(all_compvars - used_compvars), } # a completion expression that considers the outputs complete if any final task # output is received FINAL_OUTPUT_COMPLETION = ' or '.join( map( trigger_to_completion_variable, [ TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED, TASK_OUTPUT_SUBMIT_FAILED, TASK_OUTPUT_EXPIRED, ], ) ) class TaskOutputs: """Represents a collection of outputs for a task. Task outputs have a trigger and a message: * The trigger is used in the graph and with "cylc set". * Messages map onto triggers and are used with "cylc message", they can provide additional context to an output which will appear in the workflow log. [scheduling] [[graph]] R1 = t1:trigger1 => t2 [runtime] [[t1]] [[[outputs]]] trigger1 = message 1 Args: tdef: The task definition for the task these outputs represent. For use outside of the scheduler, this argument can be completion expression string. """ __slots__ = ( "_message_to_trigger", "_message_to_compvar", "_completed", "_completion_expression", "_forced", ) _message_to_trigger: Dict[str, str] # message: trigger _message_to_compvar: Dict[str, str] # message: completion variable _completed: Dict[str, bool] # message: is_complete _completion_expression: str _forced: List[str] # list of messages of force-completed outputs def __init__(self, tdef: 'Union[TaskDef, str]'): self._message_to_trigger = {} self._message_to_compvar = {} self._completed = {} self._forced = [] if isinstance(tdef, str): # abnormal use e.g. from the "cylc show" command self._completion_expression = tdef else: # normal use e.g. from within the scheduler self._completion_expression = get_completion_expression(tdef) for trigger, (message, _required) in tdef.outputs.items(): self.add(trigger, message) def add(self, trigger: str, message: str) -> None: """Register a new output. Note, normally outputs are listed automatically from the provided TaskDef so there is no need to call this interface. It exists for cases where TaskOutputs are used outside of the scheduler where there is no TaskDef object handy so outputs must be listed manually. """ self._message_to_trigger[message] = trigger self._message_to_compvar[message] = trigger_to_completion_variable( trigger ) self._completed[message] = False def get_trigger(self, message: str) -> str: """Return the trigger associated with this message.""" return self._message_to_trigger[message] def set_trigger_complete( self, trigger: str, forced=False ) -> Optional[bool]: """Set the provided output trigger as complete. Args: trigger: The task output trigger to satisfy. Returns: True: If the output was unset before. False: If the output was already set. None If the output does not apply. """ trg_to_msg = { v: k for k, v in self._message_to_trigger.items() } return self.set_message_complete(trg_to_msg[trigger], forced) def set_message_complete( self, message: str, forced=False ) -> Optional[bool]: """Set the provided task message as complete. Args: message: The task output message to satisfy. Returns: True: If the output was unset before. False: If the output was already set. None If the output does not apply. """ if message not in self._completed: # no matching output return None if self._completed[message] is False: # output was incomplete self._completed[message] = True if forced: self._forced.append(message) return True # output was already completed return False def is_message_complete(self, message: str) -> Optional[bool]: """Return True if this message is complete. Returns: * True if the message is complete. * False if the message is not complete. * None if the message does not apply to these outputs. """ if message in self._completed: return self._completed[message] return None def get_completed_outputs(self) -> Dict[str, str]: """Return a dict {trigger: message} of completed outputs. Replace message with "forced" if the output was forced. """ return { self._message_to_trigger[message]: ( FORCED_COMPLETION_MSG if message in self._forced else message ) for message, is_completed in self._completed.items() if is_completed } def __iter__(self) -> Iterator[Tuple[str, str, bool]]: """A generator that yields all outputs. Yields: (trigger, message, is_complete) trigger: The output trigger. message: The output message. is_complete: True if the output is complete, else False. """ for message, is_complete in self._completed.items(): yield self._message_to_trigger[message], message, is_complete def is_complete(self) -> bool: """Return True if the outputs are complete.""" # NOTE: If a task has been removed from the workflow via restart / # reload, then it is possible for the completion expression to be blank # (empty string). In this case, we consider the task outputs to be # complete when any final output has been generated. # See https://github.com/cylc/cylc-flow/pull/5067 expr = self._completion_expression or FINAL_OUTPUT_COMPLETION return CompletionEvaluator( expr, **{ self._message_to_compvar[message]: completed for message, completed in self._completed.items() }, ) def get_incomplete_implied(self, message: str) -> List[str]: """Return an ordered list of incomplete implied messages. Use to determined implied outputs to complete automatically. Implied outputs are necessarily earlier outputs. - started implies submitted - succeeded and failed imply started - custom outputs and expired do not imply other outputs """ implied: List[str] = [] if message in [TASK_OUTPUT_SUCCEEDED, TASK_OUTPUT_FAILED]: # Finished, so it must have submitted and started. implied = [TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_STARTED] elif message == TASK_OUTPUT_STARTED: # It must have submitted. implied = [TASK_OUTPUT_SUBMITTED] return [ message for message in implied if not self.is_message_complete(message) ] def format_completion_status( self, indent: int = 2, gutter: int = 2, ansimarkup: int = 0, ) -> str: """Return a text representation of the status of these outputs. Returns a multiline string representing the status of each output used in the expression within the context of the expression itself. Args: indent: Number of spaces to indent each level of the expression. gutter: Number of spaces to pad the left column from the expression. ansimarkup: Turns on colour coding using ansimarkup tags. These will need to be parsed before display. There are three options 0: No colour coding. 1: Only success colours will be used. This is easier to read in colour coded logs. 2: Both success and fail colours will be used. Returns: A multiline textural representation of the completion status. """ indent_space: str = ' ' * indent _gutter: str = ' ' * gutter def color_wrap(string, is_complete): if ansimarkup == 0: return string if is_complete: return f'{string}' if ansimarkup == 2: return f'{string}' return string ret: List[str] = [] indent_level: int = 0 op: Optional[str] = None fence = '┆' # U+2506 (Box Drawings Light Triple Dash Vertical) for part in RE_EXPR_SPLIT.split(self._completion_expression): if not part.strip(): continue if part in {'and', 'or'}: op = part continue elif part == '(': if op: ret.append( f' {fence}{_gutter}{(indent_space * indent_level)}' f'{op} {part}' ) else: ret.append( f' {fence}{_gutter}' f'{(indent_space * indent_level)}{part}' ) indent_level += 1 elif part == ')': indent_level -= 1 ret.append( f' {fence}{_gutter}{(indent_space * indent_level)}{part}' ) else: _symbol = BOOL_SYMBOLS[bool(self._is_compvar_complete(part))] is_complete = self._is_compvar_complete(part) _pre = ( f'{color_wrap(_symbol, is_complete)} {fence}' f'{_gutter}{(indent_space * indent_level)}' ) if op: ret.append(f'{_pre}{op} {color_wrap(part, is_complete)}') else: ret.append(f'{_pre}{color_wrap(part, is_complete)}') op = None return '\n'.join(ret) @staticmethod def is_valid_std_name(name: str) -> bool: """Check name is a valid standard output name.""" return name in SORT_ORDERS @staticmethod def output_sort_key(item: Iterable[str]) -> float: """Compare by output order. Examples: >>> this = TaskOutputs.output_sort_key >>> sorted(['finished', 'started', 'custom'], key=this) ['started', 'custom', 'finished'] """ if item in TASK_OUTPUTS: return TASK_OUTPUTS.index(item) # Sort custom outputs after started. return TASK_OUTPUTS.index(TASK_OUTPUT_STARTED) + .5 def _is_compvar_complete(self, compvar: str) -> Optional[bool]: """Return True if the completion variable is complete. Returns: * True if var is optional. * False if var is required. * None if var is not referenced. """ for message, _compvar in self._message_to_compvar.items(): if _compvar == compvar: return self.is_message_complete(message) else: raise KeyError(compvar) def iter_required_messages( self, disable: 'Optional[Literal["succeeded", "failed"]]' = None ) -> Iterator[str]: """Yield task messages that are required for this task to be complete. Note, in some cases tasks might not have any required messages, e.g. "completion = succeeded or failed". Args: disable: Consider this output and any others it is joined with by `and` to not exist. In skip mode we only want to check either succeeded or failed, but not both. """ for compvar, is_optional in get_optional_outputs( self._completion_expression, set(self._message_to_compvar.values()), disable=disable ).items(): if is_optional is False: for message, _compvar in self._message_to_compvar.items(): if _compvar == compvar: yield message cylc-flow-8.6.4/cylc/flow/task_job_mgr.py0000664000175000017500000016133615202510242020533 0ustar alastairalastair# 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 . """Manage jobs. This module provides logic to: * Set up the directory structure on remote job hosts. * Copy workflow service files to remote job hosts for communication clients. * Clean up of service files on workflow shutdown. * Prepare job files. * Prepare jobs submission, and manage the callbacks. * Prepare jobs poll/kill, and manage the callbacks. """ from contextlib import suppress import json from logging import ( CRITICAL, DEBUG, INFO, WARNING, ) import os from shutil import rmtree from time import time from typing import ( TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Set, Tuple, Union, cast, ) from cylc.flow import LOG from cylc.flow.cfgspec.globalcfg import SYSPATH from cylc.flow.exceptions import ( NoHostsError, NoPlatformsError, PlatformError, PlatformLookupError, ) from cylc.flow.hostuserutil import ( get_host, is_remote_platform, ) from cylc.flow.job_file import JobFileWriter from cylc.flow.job_runner_mgr import ( JOB_FILES_REMOVED_MESSAGE, JobPollContext, ) from cylc.flow.pathutil import get_remote_workflow_run_job_dir from cylc.flow.platforms import ( FORBIDDEN_WITH_PLATFORM, fail_if_platform_and_host_conflict, get_host_from_platform, get_install_target_from_platform, get_localhost_install_target, get_platform, ) from cylc.flow.remote import construct_ssh_cmd from cylc.flow.run_modes import ( WORKFLOW_ONLY_MODES, RunMode, ) from cylc.flow.subprocctx import SubProcContext from cylc.flow.subprocpool import SubProcPool from cylc.flow.task_action_timer import ( TaskActionTimer, TimerFlags, ) from cylc.flow.task_events_mgr import ( TaskEventsManager, log_task_job_activity, ) from cylc.flow.task_job_logs import ( JOB_LOG_JOB, NN, get_task_job_activity_log, get_task_job_job_log, get_task_job_log, ) from cylc.flow.task_message import FAIL_MESSAGE_PREFIX from cylc.flow.task_outputs import ( TASK_OUTPUT_FAILED, TASK_OUTPUT_STARTED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, ) from cylc.flow.task_remote_mgr import ( REMOTE_FILE_INSTALL_255, REMOTE_FILE_INSTALL_DONE, REMOTE_FILE_INSTALL_FAILED, REMOTE_FILE_INSTALL_IN_PROGRESS, REMOTE_INIT_255, REMOTE_INIT_DONE, REMOTE_INIT_FAILED, REMOTE_INIT_IN_PROGRESS, TaskRemoteMgr, ) from cylc.flow.task_state import ( TASK_STATUS_PREPARING, TASK_STATUS_RUNNING, TASK_STATUS_SUBMITTED, TASK_STATUS_WAITING, ) from cylc.flow.util import serialise_set from cylc.flow.wallclock import ( get_current_time_string, get_time_string_from_unix_time, get_utc_mode, ) if TYPE_CHECKING: from cylc.flow.data_store_mgr import DataStoreMgr from cylc.flow.task_proxy import TaskProxy from cylc.flow.workflow_db_mgr import WorkflowDatabaseManager class TaskJobManager: """Manage task job submit, poll and kill. This class provides logic to: * Submit jobs. * Poll jobs. * Kill jobs. * Set up the directory structure on job hosts. * Install workflow communicate client files on job hosts. * Remove workflow contact files on job hosts. """ JOBS_KILL = 'jobs-kill' JOBS_POLL = 'jobs-poll' JOBS_SUBMIT = SubProcPool.JOBS_SUBMIT POLL_FAIL = 'poll failed' REMOTE_SELECT_MSG = 'waiting for remote host selection' REMOTE_INIT_MSG = 'remote host initialising' REMOTE_FILE_INSTALL_MSG = 'file installation in progress' REMOTE_INIT_255_MSG = 'remote init failed with an unreachable host' KEY_EXECUTE_TIME_LIMIT = TaskEventsManager.KEY_EXECUTE_TIME_LIMIT IN_PROGRESS = { REMOTE_FILE_INSTALL_IN_PROGRESS: REMOTE_FILE_INSTALL_MSG, REMOTE_INIT_IN_PROGRESS: REMOTE_INIT_MSG } def __init__( self, workflow, proc_pool, workflow_db_mgr, task_events_mgr, data_store_mgr, bad_hosts, server, ): self.workflow: str = workflow self.proc_pool = proc_pool self.workflow_db_mgr: WorkflowDatabaseManager = workflow_db_mgr self.task_events_mgr: TaskEventsManager = task_events_mgr self.data_store_mgr: DataStoreMgr = data_store_mgr self.job_file_writer = JobFileWriter() self.job_runner_mgr = self.job_file_writer.job_runner_mgr self.bad_hosts: Set[str] = bad_hosts self.bad_hosts_to_clear: Set[str] = set() self.task_remote_mgr = TaskRemoteMgr( workflow, proc_pool, self.bad_hosts, self.workflow_db_mgr, server ) def check_task_jobs(self, task_pool): """Check submission and execution timeout and polling timers. Poll tasks that have timed out and/or have reached next polling time. """ now = time() poll_tasks = set() for itask in task_pool.get_tasks(): if self.task_events_mgr.check_job_time(itask, now): poll_tasks.add(itask) if itask.poll_timer.delay is not None: LOG.info( f"[{itask}] poll now, (next in " f"{itask.poll_timer.delay_timeout_as_str()})" ) if poll_tasks: self.poll_task_jobs(poll_tasks) def kill_task_jobs( self, itasks: 'Iterable[TaskProxy]' ) -> None: """Issue the command to kill jobs of active tasks.""" self._run_job_cmd( self.JOBS_KILL, itasks, self._kill_task_jobs_callback, self._kill_task_jobs_callback_255, ) def kill_prep_task(self, itask: 'TaskProxy') -> None: """Kill a preparing task.""" itask.summary['platforms_used'][itask.submit_num] = '' itask.waiting_on_job_prep = False itask.local_job_file_path = None # reset for retry self._set_retry_timers(itask) self._prep_submit_task_job_error(itask, '(killed in job prep)', '') def poll_task_jobs( self, itasks: 'Iterable[TaskProxy]', msg: str | None = None ): """Poll jobs of specified tasks. This method uses _poll_task_jobs_callback() and _manip_task_jobs_callback() as help/callback methods. _poll_task_job_callback() executes one specific job. """ if itasks: if msg is not None: LOG.info(msg) self._run_job_cmd( self.JOBS_POLL, [ # Don't poll waiting tasks. (This is not only pointless, it # is dangerous because a task waiting to rerun has the # submit number of its previous job, which can be polled). itask for itask in itasks if itask.state.status != TASK_STATUS_WAITING ], self._poll_task_jobs_callback, self._poll_task_jobs_callback_255 ) def prep_submit_task_jobs( self, itasks: 'Iterable[TaskProxy]', check_syntax: bool = True, ) -> 'Tuple[List[TaskProxy], List[TaskProxy]]': """Prepare task jobs for submit. Prepare tasks where possible. Ignore tasks that are waiting for host select command to complete. Bad host select command or error writing to a job file will cause a bad task - leading to submission failure. Return (good_tasks, bad_tasks) """ prepared_tasks = [] bad_tasks = [] for itask in itasks: if not itask.state(TASK_STATUS_PREPARING): # bump the submit_num *before* resetting the state so that the # state transition message reflects the correct submit_num itask.submit_num += 1 itask.state_reset(TASK_STATUS_PREPARING) self.data_store_mgr.delta_task_state(itask) prep_task = self._prep_submit_task_job( itask, check_syntax=check_syntax ) if prep_task: prepared_tasks.append(itask) elif prep_task is False: bad_tasks.append(itask) return (prepared_tasks, bad_tasks) def submit_task_jobs( self, itasks: 'Iterable[TaskProxy]', run_mode: RunMode, ) -> 'List[TaskProxy]': """Prepare for job submission and submit task jobs. Return: tasks that attempted submission. """ # submit "simulation/skip" mode tasks, modify "dummy" task configs: itasks, submitted_nonlive_tasks = self.submit_nonlive_task_jobs( itasks, run_mode ) # submit "live" mode tasks (and "dummy" mode tasks) submitted_live_tasks = self.submit_livelike_task_jobs(itasks) return submitted_nonlive_tasks + submitted_live_tasks def submit_livelike_task_jobs( self, itasks: 'Iterable[TaskProxy]' ) -> 'List[TaskProxy]': """Submission for live tasks and dummy tasks. Preparation (host selection, remote host init, and remote install) is done asynchronously. Newly released tasks may be sent here several times until these init subprocesses have returned. Failure during preparation is considered to be job submission failure. Once preparation has completed or failed, reset .waiting_on_job_prep in task instances so the scheduler knows to stop sending them back here. This method uses prep_submit_task_jobs() as helper. Return: tasks that attempted submission. """ prepared_tasks, bad_tasks = self.prep_submit_task_jobs(itasks) # Reset consumed host selection results self.task_remote_mgr.subshell_eval_reset() if not prepared_tasks: return bad_tasks ri_map = self.task_remote_mgr.remote_init_map # Mapping of platform names to task proxies # (resolved platforms, not platform groups): platform_itasks: 'Dict[str, List[TaskProxy]]' = {} for itask in prepared_tasks: platform_itasks.setdefault(itask.platform['name'], []).append( itask ) # Submit task jobs for each platform # Non-prepared tasks can be considered done for now: done_tasks = bad_tasks for _platform_name, itasks in sorted(platform_itasks.items()): # All tasks in this iteration have the same platform platform = itasks[0].platform if self.bad_hosts.issuperset(platform['hosts']): # Out of hosts for this platform. for itask in itasks: # Get another platform, if task config platform is a group # (Note there may be tasks with different but intersecting # platform groups) if not self._select_new_platform(itask): # else set task to submit failed. self._platform_submit_failure(itask) done_tasks.append(itask) continue install_target = get_install_target_from_platform(platform) if ri_map.get(install_target) != REMOTE_FILE_INSTALL_DONE: if install_target == get_localhost_install_target(): # Skip init and file install for localhost. LOG.debug(f"REMOTE INIT NOT REQUIRED for {install_target}") ri_map[install_target] = (REMOTE_FILE_INSTALL_DONE) elif install_target not in ri_map: # Remote init not in progress for target, so start it. self.task_remote_mgr.remote_init(platform) for itask in itasks: self.data_store_mgr.delta_job_msg( itask.job_tokens, self.REMOTE_INIT_MSG ) continue elif ri_map[install_target] == REMOTE_INIT_DONE: # Already done remote init so move on to file install self.task_remote_mgr.file_install(platform) continue elif ri_map[install_target] in self.IN_PROGRESS: # Remote init or file install in progress. for itask in itasks: msg = self.IN_PROGRESS[ri_map[install_target]] self.data_store_mgr.delta_job_msg( itask.job_tokens, msg ) continue elif ri_map[install_target] == REMOTE_INIT_255: # Remote init previously failed because a host was # unreachable, so start it again. del ri_map[install_target] self.task_remote_mgr.remote_init(platform) for itask in itasks: self.data_store_mgr.delta_job_msg( itask.job_tokens, self.REMOTE_INIT_MSG ) continue # Ensure that localhost background/at jobs are recorded as running # on the host name of the current workflow host, rather than just # "localhost". On restart on a different workflow host, this # allows the restart logic to correctly poll the status of the # background/at jobs that may still be running on the previous # workflow host. try: host = get_host_from_platform( platform, bad_hosts=self.bad_hosts ) except NoHostsError: del ri_map[install_target] self.task_remote_mgr.remote_init(platform) for itask in itasks: self.data_store_mgr.delta_job_msg( itask.job_tokens, self.REMOTE_INIT_MSG ) continue if self.job_runner_mgr.is_job_local_to_host( itask.summary['job_runner_name'] ) and not is_remote_platform(platform): host = get_host() done_tasks.extend(itasks) for itask in itasks: # Log and persist LOG.debug(f"[{itask}] host={host}") self.workflow_db_mgr.put_insert_task_jobs(itask, { 'flow_nums': serialise_set(itask.flow_nums), 'is_manual_submit': itask.is_manual_submit, 'try_num': itask.get_try_num(), 'time_submit': get_current_time_string(), 'platform_name': itask.platform['name'], 'job_runner_name': itask.summary['job_runner_name'], }) # reset the is_manual_submit flag in case of retries itask.is_manual_submit = False if ri_map[install_target] == REMOTE_FILE_INSTALL_255: del ri_map[install_target] self.task_remote_mgr.file_install(platform) for itask in itasks: self.data_store_mgr.delta_job_msg( itask.job_tokens, REMOTE_FILE_INSTALL_IN_PROGRESS ) continue if ri_map[install_target] in { REMOTE_INIT_FAILED, REMOTE_FILE_INSTALL_FAILED }: # Remote init or install failed. Set submit-failed for all # affected tasks and remove target from remote init map # - this enables new tasks to re-initialise that target init_error = ri_map[install_target] del ri_map[install_target] for itask in itasks: itask.waiting_on_job_prep = False itask.local_job_file_path = None # reset for retry log_task_job_activity( SubProcContext( self.JOBS_SUBMIT, '(init %s)' % host, err=init_error, ret_code=1, ), self.workflow, itask.point, itask.tdef.name, ) self._prep_submit_task_job_error( itask, '(remote init)', '' ) continue # Build the "cylc jobs-submit" command cmd = [self.JOBS_SUBMIT] if LOG.isEnabledFor(DEBUG): cmd.append('--debug') if get_utc_mode(): cmd.append('--utc-mode') if is_remote_platform(itask.platform): remote_mode = True cmd.append('--remote-mode') else: remote_mode = False if itask.platform[ 'clean job submission environment']: cmd.append('--clean-env') for var in itask.platform[ 'job submission environment pass-through']: cmd.append(f"--env={var}") for path in itask.platform[ 'job submission executable paths'] + SYSPATH: cmd.append(f"--path={path}") cmd.append('--') cmd.append(get_remote_workflow_run_job_dir(self.workflow)) # Chop itasks into a series of shorter lists if it's very big # to prevent overloading of stdout and stderr pipes. itasks = sorted(itasks, key=lambda itask: itask.identity) chunk_size = ( len(itasks) // ( (len(itasks) // platform['max batch submit size']) + 1 ) + 1 ) itasks_batches = [ itasks[i:i + chunk_size] for i in range(0, len(itasks), chunk_size) ] LOG.debug( '%s ... # will invoke in batches, sizes=%s', cmd, [len(b) for b in itasks_batches]) if remote_mode: cmd = construct_ssh_cmd( cmd, platform, host ) else: cmd = ['cylc'] + cmd for itasks_batch in itasks_batches: stdin_files = [] job_log_dirs = [] for itask in itasks_batch: if not itask.waiting_on_job_prep: # Avoid duplicate job submissions when flushing # preparing tasks before a reload. See # https://github.com/cylc/cylc-flow/pull/6345 continue if remote_mode: stdin_files.append( os.path.expandvars( get_task_job_job_log( self.workflow, itask.point, itask.tdef.name, itask.submit_num, ) ) ) job_log_dirs.append( itask.job_tokens.relative_id ) # The job file is now (about to be) used: reset the file # write flag so that subsequent manual retrigger will # generate a new job file. itask.local_job_file_path = None itask.waiting_on_job_prep = False if not job_log_dirs: continue self.proc_pool.put_command( SubProcContext( self.JOBS_SUBMIT, cmd + job_log_dirs, stdin_files=stdin_files, job_log_dirs=job_log_dirs, host=host ), bad_hosts=self.bad_hosts, callback=self._submit_task_jobs_callback, callback_args=[itasks_batch], callback_255=self._submit_task_jobs_callback_255, ) return done_tasks def _select_new_platform(self, itask: 'TaskProxy') -> bool: """Try to select a new platform for a task if it is using a platform group and the current platform is not available. Return True if a new platform was selected. """ rtconf = self.task_events_mgr.broadcast_mgr.get_updated_rtconfig( itask ) try: new_platform = get_platform(rtconf, bad_hosts=self.bad_hosts) except PlatformLookupError: return False # If were able to select a new platform; if new_platform and new_platform != itask.platform: # store the previous platform's hosts so that when # we record a submit fail we can clear all hosts # from all platforms from bad_hosts. self.bad_hosts_to_clear.update(itask.platform['hosts']) itask.platform = new_platform self._prep_submit_task_job_impl(itask, rtconf) return True return False def _platform_submit_failure(self, itask: 'TaskProxy') -> None: """If there are no good platforms for a task then we set it to submit-failed.""" itask.waiting_on_job_prep = False itask.local_job_file_path = None self._prep_submit_task_job_error(itask, '(remote init)', '') # Now that all hosts on all platforms in platform # group selected in task config are exhausted we # clear bad_hosts for all the hosts we have # tried for this platform or group. self.bad_hosts -= set(itask.platform['hosts']) self.bad_hosts -= self.bad_hosts_to_clear self.bad_hosts_to_clear.clear() LOG.critical( PlatformError( f"{PlatformError.MSG_INIT} (no hosts were reachable)", itask.platform['name'], ) ) def _create_job_log_path(self, itask): """Create job log directory for a task job, etc. Create local job directory, and NN symbolic link. If NN => 01, remove numbered directories with submit numbers greater than 01. Return a string in the form "POINT/NAME/SUBMIT_NUM". """ job_file_dir = get_task_job_log( self.workflow, itask.point, itask.tdef.name, itask.submit_num ) job_file_dir = os.path.expandvars(job_file_dir) task_log_dir = os.path.dirname(job_file_dir) if itask.submit_num == 1: try: names = os.listdir(task_log_dir) except OSError: pass else: for name in names: if name not in ["01", NN]: rmtree( os.path.join(task_log_dir, name), ignore_errors=True) else: rmtree(job_file_dir, ignore_errors=True) os.makedirs(job_file_dir, exist_ok=True) target = os.path.join(task_log_dir, NN) source = os.path.basename(job_file_dir) try: prev_source = os.readlink(target) except OSError: prev_source = None if prev_source == source: return try: if prev_source: os.unlink(target) os.symlink(source, target) except OSError as exc: if not exc.filename: exc.filename = target raise exc def _job_cmd_out_callback(self, itask, cmd_ctx, line): """Callback on job command STDOUT/STDERR.""" if cmd_ctx.cmd_kwargs.get("host"): host = "(%(host)s) " % cmd_ctx.cmd_kwargs else: host = "" try: timestamp, _, content = line.split("|") except ValueError: pass else: line = "%s %s" % (timestamp, content) job_activity_log = get_task_job_activity_log( self.workflow, itask.point, itask.tdef.name) if not line.endswith("\n"): line += "\n" line = host + line try: with open(os.path.expandvars(job_activity_log), "a") as handle: handle.write(line) except IOError as exc: LOG.warning("%s: write failed\n%s" % (job_activity_log, exc)) LOG.warning(f"[{itask}] {host}{line}") def _kill_task_jobs_callback(self, ctx, itasks): """Callback when kill tasks command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._kill_task_job_callback, {self.job_runner_mgr.OUT_PREFIX_COMMAND: self._job_cmd_out_callback} ) def _kill_task_jobs_callback_255(self, ctx, itasks): """Callback when kill tasks command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._kill_task_job_callback_255, {self.job_runner_mgr.OUT_PREFIX_COMMAND: self._job_cmd_out_callback} ) def _kill_task_job_callback_255(self, itask, cmd_ctx, line): """Helper for _kill_task_jobs_callback, on one task job.""" with suppress(NoHostsError): # if there is another host to kill on, try again, otherwise fail get_host_from_platform(itask.platform, bad_hosts=self.bad_hosts) self.kill_task_jobs([itask]) def _kill_task_job_callback(self, itask, cmd_ctx, line): """Helper for _kill_task_jobs_callback, on one task job.""" ctx = SubProcContext(self.JOBS_KILL, None) ctx.out = line try: ctx.timestamp, _, ctx.ret_code = line.split("|", 2) except ValueError: ctx.ret_code = 1 ctx.cmd = cmd_ctx.cmd # print original command on failure else: ctx.ret_code = int(ctx.ret_code) if ctx.ret_code: ctx.cmd = cmd_ctx.cmd # print original command on failure log_task_job_activity(ctx, self.workflow, itask.point, itask.tdef.name) log_lvl = WARNING log_msg = 'job killed' if ctx.ret_code: # non-zero exit status log_lvl = WARNING log_msg = 'job kill failed' itask.state.kill_failed = True elif itask.state(TASK_STATUS_SUBMITTED): self.task_events_mgr.process_message( itask, CRITICAL, self.task_events_mgr.EVENT_SUBMIT_FAILED, ctx.timestamp) elif itask.state(TASK_STATUS_RUNNING): self.task_events_mgr.process_message( itask, CRITICAL, TASK_OUTPUT_FAILED) else: log_lvl = DEBUG log_msg = ( 'ignoring job kill result, unexpected task state: %s' % itask.state.status) self.data_store_mgr.delta_job_msg(itask.job_tokens, log_msg) LOG.log(log_lvl, f"[{itask}] {log_msg}") def _manip_task_jobs_callback( self, ctx, itasks, summary_callback, more_callbacks=None ): """Callback when submit/poll/kill tasks command exits.""" # Swallow SSH 255 (can't contact host) errors unless debugging. if ( (ctx.ret_code and LOG.isEnabledFor(DEBUG)) or (ctx.ret_code and ctx.ret_code != 255) ): LOG.error(ctx) # A dict for easy reference of (CYCLE, NAME, SUBMIT_NUM) -> TaskProxy # # Note for "reload": A TaskProxy instance may be replaced on reload, so # the "itasks" list may not reference the TaskProxy objects that # replace the old ones. The .reload_successor attribute provides the # link(s) for us to get to the latest replacement. # # Note for "kill": It is possible for a job to trigger its trap and # report back to the workflow before (or after?) this logic is called. # If so, it will no longer be status SUBMITTED or RUNNING, and # its output line will be ignored here. tasks = {} for itask in itasks: while itask.reload_successor is not None: # Note submit number could be incremented since reload. subnum = itask.submit_num itask = itask.reload_successor itask.submit_num = subnum if itask.point is not None and itask.submit_num: submit_num = "%02d" % (itask.submit_num) tasks[(str(itask.point), itask.tdef.name, submit_num)] = itask handlers = [(self.job_runner_mgr.OUT_PREFIX_SUMMARY, summary_callback)] if more_callbacks: for prefix, callback in more_callbacks.items(): handlers.append((prefix, callback)) out = ctx.out if not out: out = "" bad_tasks = dict(tasks) for line in out.splitlines(True): for prefix, callback in handlers: if line.startswith(prefix): line = line[len(prefix):].strip() try: # TODO this massive try block should be unpacked. path = line.split("|", 2)[1] # timestamp, path, status point, name, submit_num = path.split(os.sep, 2) if prefix == self.job_runner_mgr.OUT_PREFIX_SUMMARY: del bad_tasks[(point, name, submit_num)] itask = tasks[(point, name, submit_num)] callback(itask, ctx, line) except (LookupError, ValueError) as exc: # (Note this catches KeyError too). LOG.warning( 'Unhandled %s output: %s', ctx.cmd_key, line) LOG.warning(str(exc)) # Task jobs that are in the original command but did not get a status # in the output. Handle as failures. for key, itask in sorted(bad_tasks.items()): line = ( "|".join([ctx.timestamp, os.sep.join(key), "1"]) + "\n") summary_callback(itask, ctx, line) def _poll_task_jobs_callback(self, ctx, itasks): """Callback when poll tasks command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._poll_task_job_callback, {self.job_runner_mgr.OUT_PREFIX_MESSAGE: self._poll_task_job_message_callback}) def _poll_task_jobs_callback_255(self, ctx, itasks): """Callback when poll tasks command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._poll_task_job_callback_255, {self.job_runner_mgr.OUT_PREFIX_MESSAGE: self._poll_task_job_message_callback}) def _poll_task_job_callback_255(self, itask, cmd_ctx, line): with suppress(NoHostsError): # if there is another host to poll on, try again, otherwise fail get_host_from_platform(itask.platform, bad_hosts=self.bad_hosts) self.poll_task_jobs([itask]) def _poll_task_job_callback( self, itask: 'TaskProxy', cmd_ctx: SubProcContext, line: str, ): """Helper for _poll_task_jobs_callback, on one task job.""" ctx = SubProcContext(self.JOBS_POLL, None) ctx.out = line ctx.ret_code = 0 # See cylc.flow.job_runner_mgr.JobPollContext try: job_log_dir, context = line.split('|')[1:3] items = json.loads(context) jp_ctx = JobPollContext(job_log_dir, **items) except (TypeError, ValueError): self.data_store_mgr.delta_job_msg(itask.job_tokens, self.POLL_FAIL) ctx.cmd = cmd_ctx.cmd # print original command on failure return finally: log_task_job_activity( ctx, self.workflow, itask.point, itask.tdef.name ) flag = self.task_events_mgr.FLAG_POLLED # Only log at INFO level if manually polling log_lvl = DEBUG if ( itask.platform.get('communication method') == 'poll' ) else INFO if jp_ctx.run_signal == JOB_FILES_REMOVED_MESSAGE: LOG.error( f"platform: {itask.platform['name']} - job log directory " f"{itask.job_tokens.relative_id} no longer exists" ) if jp_ctx.run_status == 1 and jp_ctx.run_signal in ["ERR", "EXIT"]: # Failed normally self.task_events_mgr.process_message( itask, log_lvl, TASK_OUTPUT_FAILED, jp_ctx.time_run_exit, flag) elif jp_ctx.run_status == 1 and jp_ctx.job_runner_exit_polled == 1: # Failed by a signal, and no longer in job runner self.task_events_mgr.process_message( itask, log_lvl, f"{FAIL_MESSAGE_PREFIX}/{jp_ctx.run_signal}", jp_ctx.time_run_exit, flag) elif jp_ctx.run_status == 1: # noqa: SIM114 # The job has terminated, but is still managed by job runner. # Some job runners may restart a job in this state, so don't # mark as failed yet. self.task_events_mgr.process_message( itask, log_lvl, TASK_OUTPUT_STARTED, jp_ctx.time_run, flag) elif jp_ctx.run_status == 0: # The job succeeded self.task_events_mgr.process_message( itask, log_lvl, TASK_OUTPUT_SUCCEEDED, jp_ctx.time_run_exit, flag) elif jp_ctx.time_run and jp_ctx.job_runner_exit_polled == 1: # The job has terminated without executing the error trap self.task_events_mgr.process_message( itask, log_lvl, TASK_OUTPUT_FAILED, get_current_time_string(), flag) elif jp_ctx.time_run: # The job has started, and is still managed by job runner self.task_events_mgr.process_message( itask, log_lvl, TASK_OUTPUT_STARTED, jp_ctx.time_run, flag) elif jp_ctx.job_runner_exit_polled == 1: # The job never ran, and no longer in job runner self.task_events_mgr.process_message( itask, log_lvl, self.task_events_mgr.EVENT_SUBMIT_FAILED, jp_ctx.time_submit_exit, flag) else: # The job never ran, and is in job runner self.task_events_mgr.process_message( itask, log_lvl, TASK_STATUS_SUBMITTED, jp_ctx.time_submit_exit, flag) def _poll_task_job_message_callback(self, itask, cmd_ctx, line): """Helper for _poll_task_jobs_callback, on message of one task job.""" ctx = SubProcContext(self.JOBS_POLL, None) ctx.out = line try: event_time, severity, message = line.split("|")[2:5] except ValueError: ctx.ret_code = 1 ctx.cmd = cmd_ctx.cmd # print original command on failure else: ctx.ret_code = 0 self.task_events_mgr.process_message( itask, severity, message, event_time, self.task_events_mgr.FLAG_POLLED) log_task_job_activity(ctx, self.workflow, itask.point, itask.tdef.name) def _run_job_cmd( self, cmd_key, itasks, callback, callback_255 ): """Run job commands, e.g. poll, kill, etc. Group itasks with their platform_name and host. Put a job command for each group to the multiprocess pool. """ if not itasks: return # sort itasks into lists based upon where they were run. auth_itasks = {} for itask in itasks: auth_itasks.setdefault(itask.platform['name'], []).append(itask) # Go through each list of itasks and carry out commands as required. for platform_name, itasks in sorted(auth_itasks.items()): try: platform = get_platform(platform_name) except NoPlatformsError: LOG.error( f'Unable to run command {cmd_key}: Unable to find' f' platform {platform_name} with accessible hosts.' ) continue except PlatformLookupError: LOG.error( f'Unable to run command {cmd_key}: Unable to find' f' platform {platform_name}.' ) continue if is_remote_platform(platform): remote_mode = True cmd = [cmd_key] else: cmd = ["cylc", cmd_key] remote_mode = False if LOG.isEnabledFor(DEBUG): cmd.append("--debug") cmd.append("--") cmd.append(get_remote_workflow_run_job_dir(self.workflow)) job_log_dirs = [] host = 'localhost' ctx = SubProcContext(cmd_key, cmd, host=host) if remote_mode: try: host = get_host_from_platform( platform, bad_hosts=self.bad_hosts ) cmd = construct_ssh_cmd( cmd, platform, host ) except NoHostsError: ctx.err = f'No available hosts for {platform["name"]}' LOG.debug(ctx) callback_255(ctx, itasks) continue else: ctx = SubProcContext(cmd_key, cmd, host=host) for itask in sorted(itasks, key=lambda task: task.identity): job_log_dirs.append(itask.job_tokens.relative_id) cmd += job_log_dirs LOG.debug(f'{cmd_key} for {platform["name"]} on {host}') self.proc_pool.put_command( ctx, bad_hosts=self.bad_hosts, callback=callback, callback_args=[itasks], callback_255=callback_255, ) @staticmethod def _set_retry_timers( itask: 'TaskProxy', rtconfig: Optional[dict] = None ) -> None: """Set try number and retry delays.""" if rtconfig is None: rtconfig = itask.tdef.rtconfig submit_delays = ( rtconfig['submission retry delays'] or itask.platform['submission retry delays'] ) for key, delays in [ (TimerFlags.SUBMISSION_RETRY, submit_delays), (TimerFlags.EXECUTION_RETRY, rtconfig['execution retry delays']) ]: if delays is None: delays = [] try: itask.try_timers[key].set_delays(delays) except KeyError: itask.try_timers[key] = TaskActionTimer(delays=delays) def submit_nonlive_task_jobs( self: 'TaskJobManager', itasks: 'Iterable[TaskProxy]', workflow_run_mode: RunMode, ) -> 'Tuple[List[TaskProxy], List[TaskProxy]]': """Identify task mode and carry out alternative submission paths if required: * Simulation: Job submission. * Skip: Entire job lifecycle happens here! * Dummy: Pre-submission preparation (removing task script's content) before returning to live pathway. * Live: return to main submission pathway without doing anything. Returns: lively_tasks: A list of tasks which require subsequent processing **as if** they were live mode tasks. (This includes live and dummy mode tasks) nonlive_tasks: A list of tasks which require no further processing because their apparent execution is done entirely inside the scheduler. (This includes skip and simulation mode tasks). """ lively_tasks: 'List[TaskProxy]' = [] nonlive_tasks: 'List[TaskProxy]' = [] now = time() now = (now, get_time_string_from_unix_time(now)) for itask in itasks: # Get task config with broadcasts applied: rtconfig = self.task_events_mgr.broadcast_mgr.get_updated_rtconfig( itask) # Apply task run mode if workflow_run_mode.value in WORKFLOW_ONLY_MODES: # Task run mode cannot override workflow run-mode sim or dummy: itask.run_mode = workflow_run_mode else: # If workflow mode is skip or live and task mode is set, # override workflow mode, else use workflow mode. itask.run_mode = RunMode( rtconfig.get('run mode', workflow_run_mode)) # Submit nonlive tasks, or add live-like (live or dummy) # tasks to list of tasks to put through live submission pipeline. submit_func = itask.run_mode.get_submit_method() if submit_func and submit_func(self, itask, rtconfig, now): # A submit function returns true if this is a nonlive task: self.workflow_db_mgr.put_insert_task_states(itask) nonlive_tasks.append(itask) else: lively_tasks.append(itask) return lively_tasks, nonlive_tasks def _submit_task_jobs_callback(self, ctx, itasks): """Callback when submit task jobs command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._submit_task_job_callback, {self.job_runner_mgr.OUT_PREFIX_COMMAND: self._job_cmd_out_callback} ) def _submit_task_jobs_callback_255(self, ctx, itasks): """Callback when submit task jobs command exits.""" self._manip_task_jobs_callback( ctx, itasks, self._submit_task_job_callback_255, {self.job_runner_mgr.OUT_PREFIX_COMMAND: self._job_cmd_out_callback} ) def _submit_task_job_callback_255(self, itask, cmd_ctx, line): """Helper for _submit_task_jobs_callback, on one task job.""" # send this task back for submission again itask.waiting_on_job_prep = True # (task is in the preparing state) def _submit_task_job_callback(self, itask, cmd_ctx, line): """Helper for _submit_task_jobs_callback, on one task job.""" ctx = SubProcContext(self.JOBS_SUBMIT, None, cmd_ctx.host) ctx.out = line items = line.split("|") try: ctx.timestamp, _, ctx.ret_code = items[0:3] except ValueError: ctx.ret_code = 1 ctx.cmd = cmd_ctx.cmd # print original command on failure else: ctx.ret_code = int(ctx.ret_code) if ctx.ret_code: ctx.cmd = cmd_ctx.cmd # print original command on failure if cmd_ctx.ret_code != 255: log_task_job_activity( ctx, self.workflow, itask.point, itask.tdef.name ) if ctx.ret_code == SubProcPool.RET_CODE_WORKFLOW_STOPPING: return try: itask.summary['submit_method_id'] = items[3] except IndexError: itask.summary['submit_method_id'] = None if itask.summary['submit_method_id'] == "None": itask.summary['submit_method_id'] = None if itask.summary['submit_method_id'] and ctx.ret_code == 0: self.task_events_mgr.process_message( itask, DEBUG, TASK_OUTPUT_SUBMITTED, ctx.timestamp) else: self.task_events_mgr.process_message( itask, CRITICAL, self.task_events_mgr.EVENT_SUBMIT_FAILED, ctx.timestamp) def _prep_submit_task_job( self, itask: 'TaskProxy', check_syntax: bool = True ) -> 'Union[TaskProxy, None, Literal[False]]': """Prepare a task job submission. Returns: * itask - preparation complete. * None - preparation in progress. * False - preparation failed. """ if itask.local_job_file_path: return itask # Handle broadcasts rtconfig = self.task_events_mgr.broadcast_mgr.get_updated_rtconfig( itask ) # BACK COMPAT: host logic # Determine task host or platform now, just before job submission, # because dynamic host/platform selection may be used. # cases: # - Platform exists, host does = throw error here: # Although errors of this sort should ideally be caught on config # load this cannot be done because inheritance may create conflicts # which appear later. Although this error is also raised # by the platforms module it's probably worth putting it here too # to prevent trying to run the remote_host/platform_select logic for # tasks which will fail anyway later. # - Platform exists, host doesn't = eval platform_name # - host exists - eval host_n # remove at: # Cylc8.x fail_if_platform_and_host_conflict(rtconfig, itask.tdef.name) host_name, platform_name = None, None try: # We need to assume that we want a host if any of the items # for the old host/batch system plaform selection system are set. if any( rtconfig[section][key] is not None for section, values in FORBIDDEN_WITH_PLATFORM.items() for key in values ): host_name = self.task_remote_mgr.eval_host( rtconfig['remote']['host'] ) else: platform_name = self.task_remote_mgr.eval_platform( rtconfig['platform'] ) except PlatformError as exc: self._prep_submit_task_job_platform_error(itask, rtconfig, exc) return False else: if host_name is None and platform_name is None: # host/platform select not ready return None elif ( host_name is None and rtconfig['platform'] and rtconfig['platform'] != platform_name ): msg = ( f"for task {itask.identity}: platform = " f"{rtconfig['platform']} evaluated as '{platform_name}'" ) if not platform_name: self._prep_submit_task_job_platform_error( itask, rtconfig, msg ) return False LOG.debug(msg) elif ( platform_name is None and rtconfig['remote']['host'] != host_name ): msg = ( f"[{itask}] host = " f"{rtconfig['remote']['host']} evaluated as '{host_name}'" ) if not host_name: self._prep_submit_task_job_platform_error( itask, rtconfig, msg ) return False LOG.debug(msg) try: platform = cast( # We know this is not None because eval_platform() or # eval_host() called above ensure it is set or else we # return early if the subshell is still evaluating. 'dict', get_platform( platform_name or rtconfig, itask.tdef.name, bad_hosts=self.bad_hosts, evaluated_host=host_name, ), ) except PlatformLookupError as exc: itask.waiting_on_job_prep = False itask.summary['platforms_used'][itask.submit_num] = '' # Retry delays, needed for the try_num self._create_job_log_path(itask) msg = '(platform not defined)' if isinstance(exc, NoPlatformsError): msg = '(no platforms available)' # Clear all hosts from all platforms in group from # bad_hosts: self.bad_hosts -= exc.hosts_consumed self._set_retry_timers(itask, rtconfig) # Provide dummy platform otherwise it will incorrectly show as # the default localhost platform in the data store: itask.platform = { 'name': rtconfig['platform'], 'job runner': '', } self._prep_submit_task_job_error(itask, msg, exc) return False itask.platform = platform # Retry delays, needed for the try_num self._set_retry_timers(itask, rtconfig) try: job_conf = self._prep_submit_task_job_impl( itask, rtconfig, ) itask.jobs.append(job_conf) local_job_file_path = get_task_job_job_log( self.workflow, itask.point, itask.tdef.name, itask.submit_num, ) self.job_file_writer.write( local_job_file_path, job_conf, check_syntax=check_syntax, ) except Exception as exc: # Could be a bad command template, IOError, etc itask.waiting_on_job_prep = False self._prep_submit_task_job_error(itask, '(prepare job file)', exc) return False itask.local_job_file_path = local_job_file_path return itask def _prep_submit_task_job_platform_error( self, itask: 'TaskProxy', rtconfig: dict, exc: Exception | str ): """Helper for self._prep_submit_task_job. On platform selection error. """ itask.waiting_on_job_prep = False itask.summary['platforms_used'][itask.submit_num] = '' # Retry delays, needed for the try_num self._create_job_log_path(itask) self._set_retry_timers(itask, rtconfig) self._prep_submit_task_job_error( itask, '(remote host select)', exc ) def _prep_submit_task_job_error( self, itask: 'TaskProxy', action: str, exc: Union[Exception, str], ) -> None: """Helper for self._prep_submit_task_job. On error.""" log_task_job_activity( SubProcContext(self.JOBS_SUBMIT, action, err=exc, ret_code=1), self.workflow, itask.point, itask.tdef.name, submit_num=itask.submit_num ) itask.is_manual_submit = False # job failed in preparation i.e. is really preparation-failed rather # than submit-failed try_num = itask.get_try_num() if not itask.jobs or ( itask.jobs[-1]['submit_num'] != itask.submit_num ): # provide a dummy job config - this info will be added to the data # store itask.jobs.append({ 'task_id': itask.identity, 'platform': itask.platform, 'job_runner_name': itask.platform['job runner'], 'submit_num': itask.submit_num, 'try_num': try_num, 'flow_nums': itask.flow_nums, }) # create a DB entry for the submit-failed job self.workflow_db_mgr.put_insert_task_jobs( itask, { 'flow_nums': serialise_set(itask.flow_nums), 'job_id': itask.summary.get('submit_method_id'), 'is_manual_submit': itask.is_manual_submit, 'try_num': try_num, 'time_submit': get_current_time_string(), 'platform_name': itask.platform['name'], 'job_runner_name': itask.summary['job_runner_name'], } ) self.task_events_mgr.process_message( itask, CRITICAL, self.task_events_mgr.EVENT_SUBMIT_FAILED) def _prep_submit_task_job_impl(self, itask, rtconfig): """Helper for self._prep_submit_task_job.""" itask.summary['platforms_used'][ itask.submit_num] = itask.platform['name'] itask.summary['job_runner_name'] = itask.platform['job runner'] # None is an allowed non-float number for Execution time limit. itask.summary[ self.KEY_EXECUTE_TIME_LIMIT ] = self.get_execution_time_limit(rtconfig['execution time limit']) # Location of job file, etc self._create_job_log_path(itask) job_d = itask.job_tokens.relative_id job_file_path = get_remote_workflow_run_job_dir( self.workflow, job_d, JOB_LOG_JOB ) return self.get_job_conf( itask, rtconfig, job_file_path=job_file_path, job_d=job_d ) @staticmethod def get_execution_time_limit( config_execution_time_limit: Any ) -> Union[None, float]: """Get execution time limit from config and process it. If the etl from the config is a Falsy then return None. Otherwise try and parse value as float. Examples: >>> from pytest import raises >>> this = TaskJobManager.get_execution_time_limit >>> this(None) >>> this("54") 54.0 >>> this({}) >>> with raises(ValueError): ... this('🇳🇿') """ if config_execution_time_limit: return float(config_execution_time_limit) return None def get_job_conf( self, itask: 'TaskProxy', rtconfig: dict, job_file_path: Optional[str] = None, job_d: Optional[str] = None, ): """Return a job config. Note that rtconfig should have any broadcasts applied. """ return { # NOTE: these fields should match get_simulation_job_conf # TODO: formalise this # https://github.com/cylc/cylc-flow/issues/5387 'job_runner_name': itask.platform['job runner'], 'job_runner_command_template': ( itask.platform['job runner command template'] ), 'dependencies': itask.state.get_resolved_dependencies(), 'directives': { **itask.platform['directives'], **rtconfig['directives'] }, 'environment': rtconfig['environment'], 'execution_time_limit': itask.summary[self.KEY_EXECUTE_TIME_LIMIT], 'env-script': rtconfig['env-script'], 'err-script': rtconfig['err-script'], 'exit-script': rtconfig['exit-script'], 'platform': itask.platform, 'init-script': rtconfig['init-script'], 'job_file_path': job_file_path, 'job_d': job_d, 'namespace_hierarchy': itask.tdef.namespace_hierarchy, 'param_var': itask.tdef.param_var, 'post-script': rtconfig['post-script'], 'pre-script': rtconfig['pre-script'], 'script': rtconfig['script'], 'submit_num': itask.submit_num, 'flow_nums': itask.flow_nums, 'workflow_name': self.workflow, 'task_id': itask.identity, 'try_num': itask.get_try_num(), 'uuid_str': self.task_events_mgr.uuid_str, 'work_d': rtconfig['work sub-directory'], } def get_simulation_job_conf(self, itask): """Return a job config for a simulated task.""" return { # NOTE: these fields should match _prep_submit_task_job_impl 'job_runner_name': 'SIMULATION', 'job_runner_command_template': '', 'dependencies': itask.state.get_resolved_dependencies(), 'directives': {}, 'environment': {}, 'execution_time_limit': itask.summary[self.KEY_EXECUTE_TIME_LIMIT], 'env-script': 'SIMULATION', 'err-script': 'SIMULATION', 'exit-script': 'SIMULATION', 'platform': itask.platform, 'init-script': 'simulation', 'job_file_path': 'simulation', 'job_d': 'SIMULATION', 'namespace_hierarchy': itask.tdef.namespace_hierarchy, 'param_var': itask.tdef.param_var, 'post-script': 'SIMULATION', 'pre-script': 'SIMULATION', 'script': 'SIMULATION', 'submit_num': itask.submit_num, 'flow_nums': itask.flow_nums, 'workflow_name': self.workflow, 'task_id': itask.identity, 'try_num': itask.get_try_num(), 'uuid_str': self.task_events_mgr.uuid_str, 'work_d': 'SIMULATION', } cylc-flow-8.6.4/cylc/flow/__init__.py0000664000175000017500000000400715202510242017620 0ustar alastairalastair# 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 . """Set up the cylc environment.""" import logging import os CYLC_LOG = 'cylc' LOG = logging.getLogger(CYLC_LOG) # Start with a null handler LOG.addHandler(logging.NullHandler()) LOG_LEVELS = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "NORMAL": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, } class LoggerAdaptor(logging.LoggerAdapter): """Adds a prefix to log messages.""" def process(self, msg, kwargs): ret = f"[{self.extra['prefix']}] {msg}" if self.extra else msg return ret, kwargs def environ_init(): """Initialise cylc environment.""" # Python output buffering delays appearance of stdout and stderr # when output is not directed to a terminal (this occurred when # running pre-5.0 cylc via the posix nohup command; is it still the # case in post-5.0 daemon-mode cylc?) os.environ['PYTHONUNBUFFERED'] = 'true' environ_init() __version__ = '8.6.4' def iter_entry_points(entry_point_name): """Iterate over Cylc entry points.""" from importlib.metadata import entry_points yield from ( entry_point # for entry_point in entry_points()[entry_point_name] for entry_point in entry_points().select(group=entry_point_name) ) cylc-flow-8.6.4/cylc/flow/async_util.py0000664000175000017500000004262715202510242020245 0ustar alastairalastair# 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 . """Utilities for use with asynchronous code.""" import asyncio from contextlib import asynccontextmanager from functools import partial, wraps from inspect import signature import os from pathlib import Path from typing import List, Union from cylc.flow import LOG class _AsyncPipe: """Implement the @pipe interface. Represents and implements an asynchronous pipe. Note: _AsyncPipe objects are created when you construct a pipe (using __or__) or attempt to iterate over an @pipe function. Attrs: func (callable): The function that this stage of the pipe represents. args (tuple): Args to call the function with. kwargs (dict): Kwargs to call the function with. filter_stop (bool): If True then items which fail a filter will not get yielded. If False then they will get yielded immediately. preserve_order (bool): If True then this will behave like a "conventional" pipe i.e. first-in first-out. If False then results will be yielded as soon as they arrive. Concurrency is the same for both options as results get cached in the first case. _left (_AsyncPipe): The previous item in the pipe or None. _right (_AsyncPipe): The next item in the pipe or None. """ def __init__( self, func, args=None, kwargs=None, filter_stop=True, preserve_order=True ): self.func = func self.args = args or () self.kwargs = kwargs or {} self.filter_stop = filter_stop self.preserve_order = preserve_order self._left = None self._right = None async def __aiter__(self): # aiter = async iter coros = self.__iter__() gen = next(coros) # the generator we start the pipe with coros = list(coros) # the coros to push data through running = [] # list of running asyncio tasks completed = asyncio.Queue() # queue of processed items to yield try: # run the generator running.append( asyncio.create_task( self._generate(gen, coros, running, completed) ) ) # push the data through the pipe and yield results if self.preserve_order: meth = self._ordered else: meth = self._unordered async for item in meth(running, completed): yield item finally: # tidy up after ourselves for task in running: task.cancel() async def _ordered(self, running, completed): """The classic first-in first-out pipe behaviour.""" cache = {} # cache of results {index: result} skip_cache = [] # list of results which have been filtered out yield_ind = 0 # the result index used when preserve_order == True while cache or running or not completed.empty(): # cache any completed items if not completed.empty(): ind, item = await completed.get() # add the item to the cache so we can yield in order cache[ind] = item # skip over any results which have been filtered out while yield_ind in skip_cache: skip_cache.remove(yield_ind) yield_ind += 1 # yield any cached results while yield_ind in cache: yield cache.pop(yield_ind) yield_ind += 1 # process completed tasks for task in running: if task.done(): running.remove(task) ind = task.result() if ind is not None: # this item has been filtered out skip_cache.append(ind) await asyncio.sleep(0) # don't allow this loop to block async def _unordered(self, running, completed): """The optimal yield items as they are processed behaviour.""" while running or not completed.empty(): # return any completed items if not completed.empty(): _, item = await completed.get() yield item # process completed tasks for task in running: if task.done(): running.remove(task) await asyncio.sleep(0) # don't allow this loop to block async def _generate(self, gen, coros, running, completed): """Pull data out of the generator.""" ind = 0 async for item in gen.func(*gen.args, **gen.kwargs): running.append( asyncio.create_task( self._chain((ind, item), coros, completed) ) ) ind += 1 async def _chain(self, item, coros, completed): """Push data through the coroutine pipe.""" ind, item = item for coro in coros: try: ret = await coro.func(item, *coro.args, **coro.kwargs) except Exception as exc: # if something goes wrong log the error and skip the item LOG.warning(exc) ret = False if ret is True: # filter passed -> continue continue elif ret is False and coro.filter_stop: # filter failed -> stop return ind elif ret is False: # filter failed but pipe configured to yield -> stop + yield break else: # returned an object -> continue item = ret await completed.put((ind, item)) def __or__(self, other): if isinstance(other, _PipeFunction): other = _AsyncPipe(other.func) other._left = self self.fastforward()._right = other # because we return self we only need __or__ not __ror__ return self def rewind(self): """Return the head of the pipe.""" ptr = self while ptr._left: ptr = ptr._left return ptr def fastforward(self): """Return the tail of the pipe.""" ptr = self while ptr._right: ptr = ptr._right return ptr def __iter__(self): ptr = self.rewind() while ptr._right: yield ptr ptr = ptr._right yield ptr def __repr__(self): return ' | '.join((str(ptr) for ptr in self)) def __str__(self): args = '' if self.args: args = ', '.join(map(repr, self.args)) if self.kwargs: if args: args += ', ' args += ', '.join(f'{k}={repr(v)}' for k, v in self.kwargs.items()) return f'{self.func.__name__}({args})' class _PipeFunction: """Represent a function for use in an async pipe. This class is just for syntactic sugar, it enables us to assign arguments via the __call__ interface and enables us to add an interface for preprocessing args. """ def __init__(self, func, preproc=None): self.func = func self.preproc = preproc def __call__(self, *args, filter_stop=True, **kwargs): # assign args/kwargs to a function in a pipe if self.preproc: args, kwargs = self.preproc(*args, **kwargs) return _AsyncPipe( self.func, args, kwargs, filter_stop ) def __or__(self, other): this = _AsyncPipe(self.func) return this | other async def __aiter__(self): # this permits pipes with only one step async for item in _AsyncPipe(self.func): yield item def __str__(self): return _AsyncPipe(self.func).__str__() def __repr__(self): return _AsyncPipe(self.func).__repr__() @property def __name__(self): return self.func.__name__ @property def __doc__(self): return self.func.__doc__ @property def __signature__(self): return signature(self.func) @property def __annotations__(self): return self.func.__annotations__ def pipe(func=None, preproc=None): """An asynchronous pipe implementation in pure Python. Use this to decorate async functions in order to arrange them into asynchronous pipes. These pipes can process multiple items through multiple stages of the pipe simultaneously by doing what processing it can whilst waiting on IO to take place in the background. Async pipes perform maximum concurrency running as far ahead as they can. Don't use for cases where you only want the first N items as the pipe may process items outside of this window. Args: func (callable): The function this decorator decorates. preproc (callable): An optional function for pre-processing any args or kwargs provided to a function when the pipe is created. preproc(args: tuple, kwargs: dict) -> (args: tuple, kwargs: dict) Example: A generator to begin our pipe with: >>> @pipe ... async def arange(): ... for i in range(10): ... yield i A filter which returns a boolean: >>> @pipe ... async def even(x): ... # note the first argument (x) is the value passed down the pipe ... return x % 2 == 0 A transformation returns anything other than a boolean: >>> @pipe ... async def mult(x, y): ... # note subsequent args must be provided when you build the pipe ... return x * y Assemble them into a pipe >>> mypipe = arange | even | mult(2) >>> mypipe arange() | even() | mult(2) Write a function to "consume items": >>> async def consumer(pipe): ... async for item in pipe: ... print(item) Run pipe run: >>> import asyncio >>> asyncio.run(consumer(mypipe)) 0 4 8 12 16 Real world examples will involve a bit of awaiting. Result Order By default this behaves like a "conventional" pipe where results are yielded in the order which the generator at the start of the pipe created them. By setting the ``preserve_order`` attribute on a pipe to ``False`` you can make it yield items as soon as they are processed irrespective or order for more immediate results e.g:: pipe = arange | even pipe.preserve_order = False Providing Arguments To Functions: The first function in the pipe will receive no data. All subsequent functions will receive the result of the previous function as its first argument (unless the previous function was a filter). To provide extra args/kwargs call the function when the pipe is being constructed e.g:: pipe = my_function(arg1, kwarg1='x') If you want to transform args/kwargs before running the pipe use the ``preproc`` argument e.g:: def my_preproc(*args, **kwargs): # do some transformation return args, kwargs @pipe(preproc=my_preproc) def my_pipe_step(x, *args, *kwargs): pass Filters And Transforms: If a function in the pipe returns a bool then it will be interpreted as a filter. If it returns any other object then it is a transform. Transforms mutate data as it passes through the pipe. Filters stop data from travelling further through the pipe. True means the filter passed, False means it failed. By default if a value fails a filter then it will not get yielded, you can change this using the filter_stop argument e.g:: # if the filter fails yield the item straight away # if it passes run the item through function and yield the result pipe = generator | filter(filter_stop=False) | function """ if preproc and not func: # @pipe(preproc=x) def _pipe(func): return _PipeFunction(func, preproc) return _pipe elif func: # @pipe return _PipeFunction(func) else: # @pipe() def _pipe(func): return _PipeFunction(func) return _pipe async def scandir(path: Union[Path, str]) -> List[Path]: """Asynchronous directory listing (performs os.listdir in an executor).""" return [ Path(path, sub_path) for sub_path in await async_listdir(path) ] async def asyncqgen(queue): """Turn a queue into an async generator.""" while not queue.empty(): yield await queue.get() def wrap_exception(coroutine): """Catch and return exceptions rather than raising them. Examples: >>> async def myfcn(): ... raise Exception('foo') >>> mywrappedfcn = wrap_exception(myfcn) >>> ret = asyncio.run(mywrappedfcn()) # the exception is not raised... >>> ret # ...it is returned Exception('foo') """ async def _inner(*args, **kwargs): try: return await coroutine(*args, **kwargs) except Exception as exc: return exc return _inner async def unordered_map(coroutine, iterator, wrap_exceptions=False): """An asynchronous map function which does not preserve order. Use in situations where you want results as they are completed rather than once they are all completed. Args: coroutine: The async function you want to call. iterator: The arguments you want to call it with. wrap_exceptions: If True, then exceptions will be caught and returned rather than raised. Example: # define your async coroutine >>> async def square(x): return x**2 # define your iterator (must yield tuples) >>> iterator = [(num,) for num in range(5)] # use `async for` to iterate over the results # (sorted in this case so the test is repeatable) >>> async def test(): ... ret = [] ... async for x in unordered_map(square, iterator): ... ret.append(x) ... return sorted(ret) >>> asyncio.run(test()) [((0,), 0), ((1,), 1), ((2,), 4), ((3,), 9), ((4,), 16)] """ if wrap_exceptions: coroutine = wrap_exception(coroutine) # create tasks pending = [] for args in iterator: task = asyncio.create_task(coroutine(*args)) task._args = args pending.append(task) # run tasks while pending: done, pending = await asyncio.wait( pending, return_when=asyncio.FIRST_COMPLETED ) for task in done: yield task._args, task.result() def make_async(fcn): """Make a synchronous function async by running it in an executor. The default asyncio executor is the ThreadPoolExecutor so this essentially syntactic sugar for running the wrapped function in a thread. """ @wraps(fcn) async def _fcn(*args, executor=None, **kwargs): return await asyncio.get_event_loop().run_in_executor( executor, partial(fcn, *args, **kwargs), ) return _fcn async_listdir = make_async(os.listdir) @asynccontextmanager async def async_block(): """Ensure all tasks started within the context are awaited when it closes. Normally, you would await a task e.g: await three() If it's possible to await the task, do that, however, this isn't always an option. This interface exists is to help patch over issues where async code (one) calls sync code (two) which calls async code (three) e.g: async def one(): two() def two(): # this breaks - event loop is already running asyncio.get_event_loop().run_until_complete(three()) async def three(): await asyncio.sleep(1) This code will error because you can't nest asyncio (without nest-asyncio) which means you can schedule tasks the tasks in "two", but you can't await them. def two(): # this works, but it doesn't wait for three() to complete asyncio.create_task(three()) This interface allows you to await the tasks async def one() async with async_block(): two() # any tasks two() started will have been awaited by now """ # make a list of all tasks running before we enter the context manager tasks_before = asyncio.all_tasks() # run the user code yield # await any new tasks await asyncio.gather(*(asyncio.all_tasks() - tasks_before)) cylc-flow-8.6.4/cylc/flow/log_level.py0000664000175000017500000000616115202510242020034 0ustar alastairalastair# 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 . """Utilities for configuring logging level via the CLI.""" import logging from typing import List, Dict, Union, TYPE_CHECKING if TYPE_CHECKING: import os def verbosity_to_log_level(verb: int) -> int: """Convert Cylc verbosity to log severity level.""" if verb < 0: return logging.WARNING if verb > 0: return logging.DEBUG return logging.INFO def log_level_to_verbosity(lvl: int) -> int: """Convert log severity level to Cylc verbosity. Examples: >>> log_level_to_verbosity(logging.NOTSET) 2 >>> log_level_to_verbosity(logging.DEBUG) 2 >>> log_level_to_verbosity(logging.INFO) 0 >>> log_level_to_verbosity(logging.WARNING) -1 >>> log_level_to_verbosity(logging.ERROR) -1 """ if lvl <= logging.DEBUG: return 2 if lvl < logging.INFO: return 1 if lvl == logging.INFO: return 0 return -1 def verbosity_to_opts(verb: int) -> List[str]: """Convert Cylc verbosity to the CLI opts required to replicate it. Examples: >>> verbosity_to_opts(0) [] >>> verbosity_to_opts(-2) ['-q', '-q'] >>> verbosity_to_opts(2) ['-v', '-v'] """ return [ '-q' for _ in range(verb, 0) ] + [ '-v' for _ in range(0, verb) ] def verbosity_to_env(verb: int) -> Dict[str, str]: """Convert Cylc verbosity to the env vars required to replicate it. Examples: >>> verbosity_to_env(0) {'CYLC_VERBOSE': 'false', 'CYLC_DEBUG': 'false'} >>> verbosity_to_env(1) {'CYLC_VERBOSE': 'true', 'CYLC_DEBUG': 'false'} >>> verbosity_to_env(2) {'CYLC_VERBOSE': 'true', 'CYLC_DEBUG': 'true'} """ return { 'CYLC_VERBOSE': str((verb > 0)).lower(), 'CYLC_DEBUG': str((verb > 1)).lower(), } def env_to_verbosity(env: 'Union[Dict, os._Environ]') -> int: """Extract verbosity from environment variables. Examples: >>> env_to_verbosity({}) 0 >>> env_to_verbosity({'CYLC_VERBOSE': 'true'}) 1 >>> env_to_verbosity({'CYLC_DEBUG': 'true'}) 2 >>> env_to_verbosity({'CYLC_DEBUG': 'TRUE'}) 2 """ return ( 2 if env.get('CYLC_DEBUG', '').lower() == 'true' else 1 if env.get('CYLC_VERBOSE', '').lower() == 'true' else 0 ) cylc-flow-8.6.4/cylc/flow/data_messages_pb2.pyi0000664000175000017500000010631415202510242021601 0ustar alastairalastairfrom google.protobuf.internal import containers as _containers from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor class PbMeta(_message.Message): __slots__ = ("title", "description", "URL", "user_defined") TITLE_FIELD_NUMBER: _ClassVar[int] DESCRIPTION_FIELD_NUMBER: _ClassVar[int] URL_FIELD_NUMBER: _ClassVar[int] USER_DEFINED_FIELD_NUMBER: _ClassVar[int] title: str description: str URL: str user_defined: str def __init__(self, title: _Optional[str] = ..., description: _Optional[str] = ..., URL: _Optional[str] = ..., user_defined: _Optional[str] = ...) -> None: ... class PbTimeZone(_message.Message): __slots__ = ("hours", "minutes", "string_basic", "string_extended") HOURS_FIELD_NUMBER: _ClassVar[int] MINUTES_FIELD_NUMBER: _ClassVar[int] STRING_BASIC_FIELD_NUMBER: _ClassVar[int] STRING_EXTENDED_FIELD_NUMBER: _ClassVar[int] hours: int minutes: int string_basic: str string_extended: str def __init__(self, hours: _Optional[int] = ..., minutes: _Optional[int] = ..., string_basic: _Optional[str] = ..., string_extended: _Optional[str] = ...) -> None: ... class PbTaskProxyRefs(_message.Message): __slots__ = ("task_proxies",) TASK_PROXIES_FIELD_NUMBER: _ClassVar[int] task_proxies: _containers.RepeatedScalarFieldContainer[str] def __init__(self, task_proxies: _Optional[_Iterable[str]] = ...) -> None: ... class PbWorkflow(_message.Message): __slots__ = ("stamp", "id", "name", "status", "host", "port", "owner", "tasks", "families", "edges", "api_version", "cylc_version", "last_updated", "meta", "newest_active_cycle_point", "oldest_active_cycle_point", "reloaded", "run_mode", "cycling_mode", "state_totals", "workflow_log_dir", "time_zone_info", "tree_depth", "job_log_names", "ns_def_order", "states", "task_proxies", "family_proxies", "status_msg", "is_held_total", "jobs", "pub_port", "broadcasts", "is_queued_total", "latest_state_tasks", "pruned", "is_runahead_total", "states_updated", "n_edge_distance", "log_records", "contains_held", "contains_retry") class StateTotalsEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: int def __init__(self, key: _Optional[str] = ..., value: _Optional[int] = ...) -> None: ... class LatestStateTasksEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: PbTaskProxyRefs def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[PbTaskProxyRefs, _Mapping]] = ...) -> None: ... STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] STATUS_FIELD_NUMBER: _ClassVar[int] HOST_FIELD_NUMBER: _ClassVar[int] PORT_FIELD_NUMBER: _ClassVar[int] OWNER_FIELD_NUMBER: _ClassVar[int] TASKS_FIELD_NUMBER: _ClassVar[int] FAMILIES_FIELD_NUMBER: _ClassVar[int] EDGES_FIELD_NUMBER: _ClassVar[int] API_VERSION_FIELD_NUMBER: _ClassVar[int] CYLC_VERSION_FIELD_NUMBER: _ClassVar[int] LAST_UPDATED_FIELD_NUMBER: _ClassVar[int] META_FIELD_NUMBER: _ClassVar[int] NEWEST_ACTIVE_CYCLE_POINT_FIELD_NUMBER: _ClassVar[int] OLDEST_ACTIVE_CYCLE_POINT_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] RUN_MODE_FIELD_NUMBER: _ClassVar[int] CYCLING_MODE_FIELD_NUMBER: _ClassVar[int] STATE_TOTALS_FIELD_NUMBER: _ClassVar[int] WORKFLOW_LOG_DIR_FIELD_NUMBER: _ClassVar[int] TIME_ZONE_INFO_FIELD_NUMBER: _ClassVar[int] TREE_DEPTH_FIELD_NUMBER: _ClassVar[int] JOB_LOG_NAMES_FIELD_NUMBER: _ClassVar[int] NS_DEF_ORDER_FIELD_NUMBER: _ClassVar[int] STATES_FIELD_NUMBER: _ClassVar[int] TASK_PROXIES_FIELD_NUMBER: _ClassVar[int] FAMILY_PROXIES_FIELD_NUMBER: _ClassVar[int] STATUS_MSG_FIELD_NUMBER: _ClassVar[int] IS_HELD_TOTAL_FIELD_NUMBER: _ClassVar[int] JOBS_FIELD_NUMBER: _ClassVar[int] PUB_PORT_FIELD_NUMBER: _ClassVar[int] BROADCASTS_FIELD_NUMBER: _ClassVar[int] IS_QUEUED_TOTAL_FIELD_NUMBER: _ClassVar[int] LATEST_STATE_TASKS_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] IS_RUNAHEAD_TOTAL_FIELD_NUMBER: _ClassVar[int] STATES_UPDATED_FIELD_NUMBER: _ClassVar[int] N_EDGE_DISTANCE_FIELD_NUMBER: _ClassVar[int] LOG_RECORDS_FIELD_NUMBER: _ClassVar[int] CONTAINS_HELD_FIELD_NUMBER: _ClassVar[int] CONTAINS_RETRY_FIELD_NUMBER: _ClassVar[int] stamp: str id: str name: str status: str host: str port: int owner: str tasks: _containers.RepeatedScalarFieldContainer[str] families: _containers.RepeatedScalarFieldContainer[str] edges: PbEdges api_version: int cylc_version: str last_updated: float meta: PbMeta newest_active_cycle_point: str oldest_active_cycle_point: str reloaded: bool run_mode: str cycling_mode: str state_totals: _containers.ScalarMap[str, int] workflow_log_dir: str time_zone_info: PbTimeZone tree_depth: int job_log_names: _containers.RepeatedScalarFieldContainer[str] ns_def_order: _containers.RepeatedScalarFieldContainer[str] states: _containers.RepeatedScalarFieldContainer[str] task_proxies: _containers.RepeatedScalarFieldContainer[str] family_proxies: _containers.RepeatedScalarFieldContainer[str] status_msg: str is_held_total: int jobs: _containers.RepeatedScalarFieldContainer[str] pub_port: int broadcasts: str is_queued_total: int latest_state_tasks: _containers.MessageMap[str, PbTaskProxyRefs] pruned: bool is_runahead_total: int states_updated: bool n_edge_distance: int log_records: _containers.RepeatedCompositeFieldContainer[PbLogRecord] contains_held: bool contains_retry: bool def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., name: _Optional[str] = ..., status: _Optional[str] = ..., host: _Optional[str] = ..., port: _Optional[int] = ..., owner: _Optional[str] = ..., tasks: _Optional[_Iterable[str]] = ..., families: _Optional[_Iterable[str]] = ..., edges: _Optional[_Union[PbEdges, _Mapping]] = ..., api_version: _Optional[int] = ..., cylc_version: _Optional[str] = ..., last_updated: _Optional[float] = ..., meta: _Optional[_Union[PbMeta, _Mapping]] = ..., newest_active_cycle_point: _Optional[str] = ..., oldest_active_cycle_point: _Optional[str] = ..., reloaded: bool = ..., run_mode: _Optional[str] = ..., cycling_mode: _Optional[str] = ..., state_totals: _Optional[_Mapping[str, int]] = ..., workflow_log_dir: _Optional[str] = ..., time_zone_info: _Optional[_Union[PbTimeZone, _Mapping]] = ..., tree_depth: _Optional[int] = ..., job_log_names: _Optional[_Iterable[str]] = ..., ns_def_order: _Optional[_Iterable[str]] = ..., states: _Optional[_Iterable[str]] = ..., task_proxies: _Optional[_Iterable[str]] = ..., family_proxies: _Optional[_Iterable[str]] = ..., status_msg: _Optional[str] = ..., is_held_total: _Optional[int] = ..., jobs: _Optional[_Iterable[str]] = ..., pub_port: _Optional[int] = ..., broadcasts: _Optional[str] = ..., is_queued_total: _Optional[int] = ..., latest_state_tasks: _Optional[_Mapping[str, PbTaskProxyRefs]] = ..., pruned: bool = ..., is_runahead_total: _Optional[int] = ..., states_updated: bool = ..., n_edge_distance: _Optional[int] = ..., log_records: _Optional[_Iterable[_Union[PbLogRecord, _Mapping]]] = ..., contains_held: bool = ..., contains_retry: bool = ...) -> None: ... class PbLogRecord(_message.Message): __slots__ = ("level", "message") LEVEL_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] level: str message: str def __init__(self, level: _Optional[str] = ..., message: _Optional[str] = ...) -> None: ... class PbRuntime(_message.Message): __slots__ = ("platform", "script", "init_script", "env_script", "err_script", "exit_script", "pre_script", "post_script", "work_sub_dir", "execution_polling_intervals", "execution_retry_delays", "execution_time_limit", "submission_polling_intervals", "submission_retry_delays", "directives", "environment", "outputs", "completion", "run_mode") PLATFORM_FIELD_NUMBER: _ClassVar[int] SCRIPT_FIELD_NUMBER: _ClassVar[int] INIT_SCRIPT_FIELD_NUMBER: _ClassVar[int] ENV_SCRIPT_FIELD_NUMBER: _ClassVar[int] ERR_SCRIPT_FIELD_NUMBER: _ClassVar[int] EXIT_SCRIPT_FIELD_NUMBER: _ClassVar[int] PRE_SCRIPT_FIELD_NUMBER: _ClassVar[int] POST_SCRIPT_FIELD_NUMBER: _ClassVar[int] WORK_SUB_DIR_FIELD_NUMBER: _ClassVar[int] EXECUTION_POLLING_INTERVALS_FIELD_NUMBER: _ClassVar[int] EXECUTION_RETRY_DELAYS_FIELD_NUMBER: _ClassVar[int] EXECUTION_TIME_LIMIT_FIELD_NUMBER: _ClassVar[int] SUBMISSION_POLLING_INTERVALS_FIELD_NUMBER: _ClassVar[int] SUBMISSION_RETRY_DELAYS_FIELD_NUMBER: _ClassVar[int] DIRECTIVES_FIELD_NUMBER: _ClassVar[int] ENVIRONMENT_FIELD_NUMBER: _ClassVar[int] OUTPUTS_FIELD_NUMBER: _ClassVar[int] COMPLETION_FIELD_NUMBER: _ClassVar[int] RUN_MODE_FIELD_NUMBER: _ClassVar[int] platform: str script: str init_script: str env_script: str err_script: str exit_script: str pre_script: str post_script: str work_sub_dir: str execution_polling_intervals: str execution_retry_delays: str execution_time_limit: str submission_polling_intervals: str submission_retry_delays: str directives: str environment: str outputs: str completion: str run_mode: str def __init__(self, platform: _Optional[str] = ..., script: _Optional[str] = ..., init_script: _Optional[str] = ..., env_script: _Optional[str] = ..., err_script: _Optional[str] = ..., exit_script: _Optional[str] = ..., pre_script: _Optional[str] = ..., post_script: _Optional[str] = ..., work_sub_dir: _Optional[str] = ..., execution_polling_intervals: _Optional[str] = ..., execution_retry_delays: _Optional[str] = ..., execution_time_limit: _Optional[str] = ..., submission_polling_intervals: _Optional[str] = ..., submission_retry_delays: _Optional[str] = ..., directives: _Optional[str] = ..., environment: _Optional[str] = ..., outputs: _Optional[str] = ..., completion: _Optional[str] = ..., run_mode: _Optional[str] = ...) -> None: ... class PbJob(_message.Message): __slots__ = ("stamp", "id", "submit_num", "state", "task_proxy", "submitted_time", "started_time", "finished_time", "job_id", "job_runner_name", "execution_time_limit", "platform", "job_log_dir", "name", "cycle_point", "messages", "runtime", "estimated_finish_time") STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] SUBMIT_NUM_FIELD_NUMBER: _ClassVar[int] STATE_FIELD_NUMBER: _ClassVar[int] TASK_PROXY_FIELD_NUMBER: _ClassVar[int] SUBMITTED_TIME_FIELD_NUMBER: _ClassVar[int] STARTED_TIME_FIELD_NUMBER: _ClassVar[int] FINISHED_TIME_FIELD_NUMBER: _ClassVar[int] JOB_ID_FIELD_NUMBER: _ClassVar[int] JOB_RUNNER_NAME_FIELD_NUMBER: _ClassVar[int] EXECUTION_TIME_LIMIT_FIELD_NUMBER: _ClassVar[int] PLATFORM_FIELD_NUMBER: _ClassVar[int] JOB_LOG_DIR_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] CYCLE_POINT_FIELD_NUMBER: _ClassVar[int] MESSAGES_FIELD_NUMBER: _ClassVar[int] RUNTIME_FIELD_NUMBER: _ClassVar[int] ESTIMATED_FINISH_TIME_FIELD_NUMBER: _ClassVar[int] stamp: str id: str submit_num: int state: str task_proxy: str submitted_time: str started_time: str finished_time: str job_id: str job_runner_name: str execution_time_limit: float platform: str job_log_dir: str name: str cycle_point: str messages: _containers.RepeatedScalarFieldContainer[str] runtime: PbRuntime estimated_finish_time: str def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., submit_num: _Optional[int] = ..., state: _Optional[str] = ..., task_proxy: _Optional[str] = ..., submitted_time: _Optional[str] = ..., started_time: _Optional[str] = ..., finished_time: _Optional[str] = ..., job_id: _Optional[str] = ..., job_runner_name: _Optional[str] = ..., execution_time_limit: _Optional[float] = ..., platform: _Optional[str] = ..., job_log_dir: _Optional[str] = ..., name: _Optional[str] = ..., cycle_point: _Optional[str] = ..., messages: _Optional[_Iterable[str]] = ..., runtime: _Optional[_Union[PbRuntime, _Mapping]] = ..., estimated_finish_time: _Optional[str] = ...) -> None: ... class PbTask(_message.Message): __slots__ = ("stamp", "id", "name", "meta", "mean_elapsed_time", "depth", "proxies", "namespace", "parents", "first_parent", "runtime") STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] META_FIELD_NUMBER: _ClassVar[int] MEAN_ELAPSED_TIME_FIELD_NUMBER: _ClassVar[int] DEPTH_FIELD_NUMBER: _ClassVar[int] PROXIES_FIELD_NUMBER: _ClassVar[int] NAMESPACE_FIELD_NUMBER: _ClassVar[int] PARENTS_FIELD_NUMBER: _ClassVar[int] FIRST_PARENT_FIELD_NUMBER: _ClassVar[int] RUNTIME_FIELD_NUMBER: _ClassVar[int] stamp: str id: str name: str meta: PbMeta mean_elapsed_time: float depth: int proxies: _containers.RepeatedScalarFieldContainer[str] namespace: _containers.RepeatedScalarFieldContainer[str] parents: _containers.RepeatedScalarFieldContainer[str] first_parent: str runtime: PbRuntime def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., name: _Optional[str] = ..., meta: _Optional[_Union[PbMeta, _Mapping]] = ..., mean_elapsed_time: _Optional[float] = ..., depth: _Optional[int] = ..., proxies: _Optional[_Iterable[str]] = ..., namespace: _Optional[_Iterable[str]] = ..., parents: _Optional[_Iterable[str]] = ..., first_parent: _Optional[str] = ..., runtime: _Optional[_Union[PbRuntime, _Mapping]] = ...) -> None: ... class PbPollTask(_message.Message): __slots__ = ("local_proxy", "workflow", "remote_proxy", "req_state", "graph_string") LOCAL_PROXY_FIELD_NUMBER: _ClassVar[int] WORKFLOW_FIELD_NUMBER: _ClassVar[int] REMOTE_PROXY_FIELD_NUMBER: _ClassVar[int] REQ_STATE_FIELD_NUMBER: _ClassVar[int] GRAPH_STRING_FIELD_NUMBER: _ClassVar[int] local_proxy: str workflow: str remote_proxy: str req_state: str graph_string: str def __init__(self, local_proxy: _Optional[str] = ..., workflow: _Optional[str] = ..., remote_proxy: _Optional[str] = ..., req_state: _Optional[str] = ..., graph_string: _Optional[str] = ...) -> None: ... class PbCondition(_message.Message): __slots__ = ("task_proxy", "expr_alias", "req_state", "satisfied", "message") TASK_PROXY_FIELD_NUMBER: _ClassVar[int] EXPR_ALIAS_FIELD_NUMBER: _ClassVar[int] REQ_STATE_FIELD_NUMBER: _ClassVar[int] SATISFIED_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] task_proxy: str expr_alias: str req_state: str satisfied: bool message: str def __init__(self, task_proxy: _Optional[str] = ..., expr_alias: _Optional[str] = ..., req_state: _Optional[str] = ..., satisfied: bool = ..., message: _Optional[str] = ...) -> None: ... class PbPrerequisite(_message.Message): __slots__ = ("expression", "conditions", "cycle_points", "satisfied") EXPRESSION_FIELD_NUMBER: _ClassVar[int] CONDITIONS_FIELD_NUMBER: _ClassVar[int] CYCLE_POINTS_FIELD_NUMBER: _ClassVar[int] SATISFIED_FIELD_NUMBER: _ClassVar[int] expression: str conditions: _containers.RepeatedCompositeFieldContainer[PbCondition] cycle_points: _containers.RepeatedScalarFieldContainer[str] satisfied: bool def __init__(self, expression: _Optional[str] = ..., conditions: _Optional[_Iterable[_Union[PbCondition, _Mapping]]] = ..., cycle_points: _Optional[_Iterable[str]] = ..., satisfied: bool = ...) -> None: ... class PbOutput(_message.Message): __slots__ = ("label", "message", "satisfied", "time") LABEL_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] SATISFIED_FIELD_NUMBER: _ClassVar[int] TIME_FIELD_NUMBER: _ClassVar[int] label: str message: str satisfied: bool time: float def __init__(self, label: _Optional[str] = ..., message: _Optional[str] = ..., satisfied: bool = ..., time: _Optional[float] = ...) -> None: ... class PbTrigger(_message.Message): __slots__ = ("id", "label", "message", "satisfied", "time") ID_FIELD_NUMBER: _ClassVar[int] LABEL_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] SATISFIED_FIELD_NUMBER: _ClassVar[int] TIME_FIELD_NUMBER: _ClassVar[int] id: str label: str message: str satisfied: bool time: float def __init__(self, id: _Optional[str] = ..., label: _Optional[str] = ..., message: _Optional[str] = ..., satisfied: bool = ..., time: _Optional[float] = ...) -> None: ... class PbTaskProxy(_message.Message): __slots__ = ("stamp", "id", "task", "state", "cycle_point", "depth", "job_submits", "outputs", "namespace", "prerequisites", "jobs", "first_parent", "name", "is_held", "edges", "ancestors", "flow_nums", "external_triggers", "xtriggers", "is_queued", "is_runahead", "flow_wait", "runtime", "graph_depth", "is_retry", "is_wallclock", "is_xtriggered") class OutputsEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: PbOutput def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[PbOutput, _Mapping]] = ...) -> None: ... class ExternalTriggersEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: PbTrigger def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[PbTrigger, _Mapping]] = ...) -> None: ... class XtriggersEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: PbTrigger def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[PbTrigger, _Mapping]] = ...) -> None: ... STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] TASK_FIELD_NUMBER: _ClassVar[int] STATE_FIELD_NUMBER: _ClassVar[int] CYCLE_POINT_FIELD_NUMBER: _ClassVar[int] DEPTH_FIELD_NUMBER: _ClassVar[int] JOB_SUBMITS_FIELD_NUMBER: _ClassVar[int] OUTPUTS_FIELD_NUMBER: _ClassVar[int] NAMESPACE_FIELD_NUMBER: _ClassVar[int] PREREQUISITES_FIELD_NUMBER: _ClassVar[int] JOBS_FIELD_NUMBER: _ClassVar[int] FIRST_PARENT_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] IS_HELD_FIELD_NUMBER: _ClassVar[int] EDGES_FIELD_NUMBER: _ClassVar[int] ANCESTORS_FIELD_NUMBER: _ClassVar[int] FLOW_NUMS_FIELD_NUMBER: _ClassVar[int] EXTERNAL_TRIGGERS_FIELD_NUMBER: _ClassVar[int] XTRIGGERS_FIELD_NUMBER: _ClassVar[int] IS_QUEUED_FIELD_NUMBER: _ClassVar[int] IS_RUNAHEAD_FIELD_NUMBER: _ClassVar[int] FLOW_WAIT_FIELD_NUMBER: _ClassVar[int] RUNTIME_FIELD_NUMBER: _ClassVar[int] GRAPH_DEPTH_FIELD_NUMBER: _ClassVar[int] IS_RETRY_FIELD_NUMBER: _ClassVar[int] IS_WALLCLOCK_FIELD_NUMBER: _ClassVar[int] IS_XTRIGGERED_FIELD_NUMBER: _ClassVar[int] stamp: str id: str task: str state: str cycle_point: str depth: int job_submits: int outputs: _containers.MessageMap[str, PbOutput] namespace: _containers.RepeatedScalarFieldContainer[str] prerequisites: _containers.RepeatedCompositeFieldContainer[PbPrerequisite] jobs: _containers.RepeatedScalarFieldContainer[str] first_parent: str name: str is_held: bool edges: _containers.RepeatedScalarFieldContainer[str] ancestors: _containers.RepeatedScalarFieldContainer[str] flow_nums: str external_triggers: _containers.MessageMap[str, PbTrigger] xtriggers: _containers.MessageMap[str, PbTrigger] is_queued: bool is_runahead: bool flow_wait: bool runtime: PbRuntime graph_depth: int is_retry: bool is_wallclock: bool is_xtriggered: bool def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., task: _Optional[str] = ..., state: _Optional[str] = ..., cycle_point: _Optional[str] = ..., depth: _Optional[int] = ..., job_submits: _Optional[int] = ..., outputs: _Optional[_Mapping[str, PbOutput]] = ..., namespace: _Optional[_Iterable[str]] = ..., prerequisites: _Optional[_Iterable[_Union[PbPrerequisite, _Mapping]]] = ..., jobs: _Optional[_Iterable[str]] = ..., first_parent: _Optional[str] = ..., name: _Optional[str] = ..., is_held: bool = ..., edges: _Optional[_Iterable[str]] = ..., ancestors: _Optional[_Iterable[str]] = ..., flow_nums: _Optional[str] = ..., external_triggers: _Optional[_Mapping[str, PbTrigger]] = ..., xtriggers: _Optional[_Mapping[str, PbTrigger]] = ..., is_queued: bool = ..., is_runahead: bool = ..., flow_wait: bool = ..., runtime: _Optional[_Union[PbRuntime, _Mapping]] = ..., graph_depth: _Optional[int] = ..., is_retry: bool = ..., is_wallclock: bool = ..., is_xtriggered: bool = ...) -> None: ... class PbFamily(_message.Message): __slots__ = ("stamp", "id", "name", "meta", "depth", "proxies", "parents", "child_tasks", "child_families", "first_parent", "runtime") STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] META_FIELD_NUMBER: _ClassVar[int] DEPTH_FIELD_NUMBER: _ClassVar[int] PROXIES_FIELD_NUMBER: _ClassVar[int] PARENTS_FIELD_NUMBER: _ClassVar[int] CHILD_TASKS_FIELD_NUMBER: _ClassVar[int] CHILD_FAMILIES_FIELD_NUMBER: _ClassVar[int] FIRST_PARENT_FIELD_NUMBER: _ClassVar[int] RUNTIME_FIELD_NUMBER: _ClassVar[int] stamp: str id: str name: str meta: PbMeta depth: int proxies: _containers.RepeatedScalarFieldContainer[str] parents: _containers.RepeatedScalarFieldContainer[str] child_tasks: _containers.RepeatedScalarFieldContainer[str] child_families: _containers.RepeatedScalarFieldContainer[str] first_parent: str runtime: PbRuntime def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., name: _Optional[str] = ..., meta: _Optional[_Union[PbMeta, _Mapping]] = ..., depth: _Optional[int] = ..., proxies: _Optional[_Iterable[str]] = ..., parents: _Optional[_Iterable[str]] = ..., child_tasks: _Optional[_Iterable[str]] = ..., child_families: _Optional[_Iterable[str]] = ..., first_parent: _Optional[str] = ..., runtime: _Optional[_Union[PbRuntime, _Mapping]] = ...) -> None: ... class PbFamilyProxy(_message.Message): __slots__ = ("stamp", "id", "cycle_point", "name", "family", "state", "depth", "first_parent", "child_tasks", "child_families", "is_held", "ancestors", "states", "state_totals", "is_held_total", "is_queued", "is_queued_total", "is_runahead", "is_runahead_total", "runtime", "graph_depth", "is_retry", "is_wallclock", "is_xtriggered") class StateTotalsEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: int def __init__(self, key: _Optional[str] = ..., value: _Optional[int] = ...) -> None: ... STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] CYCLE_POINT_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] FAMILY_FIELD_NUMBER: _ClassVar[int] STATE_FIELD_NUMBER: _ClassVar[int] DEPTH_FIELD_NUMBER: _ClassVar[int] FIRST_PARENT_FIELD_NUMBER: _ClassVar[int] CHILD_TASKS_FIELD_NUMBER: _ClassVar[int] CHILD_FAMILIES_FIELD_NUMBER: _ClassVar[int] IS_HELD_FIELD_NUMBER: _ClassVar[int] ANCESTORS_FIELD_NUMBER: _ClassVar[int] STATES_FIELD_NUMBER: _ClassVar[int] STATE_TOTALS_FIELD_NUMBER: _ClassVar[int] IS_HELD_TOTAL_FIELD_NUMBER: _ClassVar[int] IS_QUEUED_FIELD_NUMBER: _ClassVar[int] IS_QUEUED_TOTAL_FIELD_NUMBER: _ClassVar[int] IS_RUNAHEAD_FIELD_NUMBER: _ClassVar[int] IS_RUNAHEAD_TOTAL_FIELD_NUMBER: _ClassVar[int] RUNTIME_FIELD_NUMBER: _ClassVar[int] GRAPH_DEPTH_FIELD_NUMBER: _ClassVar[int] IS_RETRY_FIELD_NUMBER: _ClassVar[int] IS_WALLCLOCK_FIELD_NUMBER: _ClassVar[int] IS_XTRIGGERED_FIELD_NUMBER: _ClassVar[int] stamp: str id: str cycle_point: str name: str family: str state: str depth: int first_parent: str child_tasks: _containers.RepeatedScalarFieldContainer[str] child_families: _containers.RepeatedScalarFieldContainer[str] is_held: bool ancestors: _containers.RepeatedScalarFieldContainer[str] states: _containers.RepeatedScalarFieldContainer[str] state_totals: _containers.ScalarMap[str, int] is_held_total: int is_queued: bool is_queued_total: int is_runahead: bool is_runahead_total: int runtime: PbRuntime graph_depth: int is_retry: bool is_wallclock: bool is_xtriggered: bool def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., cycle_point: _Optional[str] = ..., name: _Optional[str] = ..., family: _Optional[str] = ..., state: _Optional[str] = ..., depth: _Optional[int] = ..., first_parent: _Optional[str] = ..., child_tasks: _Optional[_Iterable[str]] = ..., child_families: _Optional[_Iterable[str]] = ..., is_held: bool = ..., ancestors: _Optional[_Iterable[str]] = ..., states: _Optional[_Iterable[str]] = ..., state_totals: _Optional[_Mapping[str, int]] = ..., is_held_total: _Optional[int] = ..., is_queued: bool = ..., is_queued_total: _Optional[int] = ..., is_runahead: bool = ..., is_runahead_total: _Optional[int] = ..., runtime: _Optional[_Union[PbRuntime, _Mapping]] = ..., graph_depth: _Optional[int] = ..., is_retry: bool = ..., is_wallclock: bool = ..., is_xtriggered: bool = ...) -> None: ... class PbEdge(_message.Message): __slots__ = ("stamp", "id", "source", "target", "suicide", "cond") STAMP_FIELD_NUMBER: _ClassVar[int] ID_FIELD_NUMBER: _ClassVar[int] SOURCE_FIELD_NUMBER: _ClassVar[int] TARGET_FIELD_NUMBER: _ClassVar[int] SUICIDE_FIELD_NUMBER: _ClassVar[int] COND_FIELD_NUMBER: _ClassVar[int] stamp: str id: str source: str target: str suicide: bool cond: bool def __init__(self, stamp: _Optional[str] = ..., id: _Optional[str] = ..., source: _Optional[str] = ..., target: _Optional[str] = ..., suicide: bool = ..., cond: bool = ...) -> None: ... class PbEdges(_message.Message): __slots__ = ("id", "edges", "workflow_polling_tasks", "leaves", "feet") ID_FIELD_NUMBER: _ClassVar[int] EDGES_FIELD_NUMBER: _ClassVar[int] WORKFLOW_POLLING_TASKS_FIELD_NUMBER: _ClassVar[int] LEAVES_FIELD_NUMBER: _ClassVar[int] FEET_FIELD_NUMBER: _ClassVar[int] id: str edges: _containers.RepeatedScalarFieldContainer[str] workflow_polling_tasks: _containers.RepeatedCompositeFieldContainer[PbPollTask] leaves: _containers.RepeatedScalarFieldContainer[str] feet: _containers.RepeatedScalarFieldContainer[str] def __init__(self, id: _Optional[str] = ..., edges: _Optional[_Iterable[str]] = ..., workflow_polling_tasks: _Optional[_Iterable[_Union[PbPollTask, _Mapping]]] = ..., leaves: _Optional[_Iterable[str]] = ..., feet: _Optional[_Iterable[str]] = ...) -> None: ... class PbEntireWorkflow(_message.Message): __slots__ = ("workflow", "tasks", "task_proxies", "jobs", "families", "family_proxies", "edges") WORKFLOW_FIELD_NUMBER: _ClassVar[int] TASKS_FIELD_NUMBER: _ClassVar[int] TASK_PROXIES_FIELD_NUMBER: _ClassVar[int] JOBS_FIELD_NUMBER: _ClassVar[int] FAMILIES_FIELD_NUMBER: _ClassVar[int] FAMILY_PROXIES_FIELD_NUMBER: _ClassVar[int] EDGES_FIELD_NUMBER: _ClassVar[int] workflow: PbWorkflow tasks: _containers.RepeatedCompositeFieldContainer[PbTask] task_proxies: _containers.RepeatedCompositeFieldContainer[PbTaskProxy] jobs: _containers.RepeatedCompositeFieldContainer[PbJob] families: _containers.RepeatedCompositeFieldContainer[PbFamily] family_proxies: _containers.RepeatedCompositeFieldContainer[PbFamilyProxy] edges: _containers.RepeatedCompositeFieldContainer[PbEdge] def __init__(self, workflow: _Optional[_Union[PbWorkflow, _Mapping]] = ..., tasks: _Optional[_Iterable[_Union[PbTask, _Mapping]]] = ..., task_proxies: _Optional[_Iterable[_Union[PbTaskProxy, _Mapping]]] = ..., jobs: _Optional[_Iterable[_Union[PbJob, _Mapping]]] = ..., families: _Optional[_Iterable[_Union[PbFamily, _Mapping]]] = ..., family_proxies: _Optional[_Iterable[_Union[PbFamilyProxy, _Mapping]]] = ..., edges: _Optional[_Iterable[_Union[PbEdge, _Mapping]]] = ...) -> None: ... class EDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbEdge] updated: _containers.RepeatedCompositeFieldContainer[PbEdge] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbEdge, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbEdge, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class FDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbFamily] updated: _containers.RepeatedCompositeFieldContainer[PbFamily] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbFamily, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbFamily, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class FPDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbFamilyProxy] updated: _containers.RepeatedCompositeFieldContainer[PbFamilyProxy] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbFamilyProxy, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbFamilyProxy, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class JDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbJob] updated: _containers.RepeatedCompositeFieldContainer[PbJob] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbJob, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbJob, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class TDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbTask] updated: _containers.RepeatedCompositeFieldContainer[PbTask] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbTask, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbTask, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class TPDeltas(_message.Message): __slots__ = ("time", "checksum", "added", "updated", "pruned", "reloaded") TIME_FIELD_NUMBER: _ClassVar[int] CHECKSUM_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] time: float checksum: int added: _containers.RepeatedCompositeFieldContainer[PbTaskProxy] updated: _containers.RepeatedCompositeFieldContainer[PbTaskProxy] pruned: _containers.RepeatedScalarFieldContainer[str] reloaded: bool def __init__(self, time: _Optional[float] = ..., checksum: _Optional[int] = ..., added: _Optional[_Iterable[_Union[PbTaskProxy, _Mapping]]] = ..., updated: _Optional[_Iterable[_Union[PbTaskProxy, _Mapping]]] = ..., pruned: _Optional[_Iterable[str]] = ..., reloaded: bool = ...) -> None: ... class WDeltas(_message.Message): __slots__ = ("time", "added", "updated", "reloaded", "pruned") TIME_FIELD_NUMBER: _ClassVar[int] ADDED_FIELD_NUMBER: _ClassVar[int] UPDATED_FIELD_NUMBER: _ClassVar[int] RELOADED_FIELD_NUMBER: _ClassVar[int] PRUNED_FIELD_NUMBER: _ClassVar[int] time: float added: PbWorkflow updated: PbWorkflow reloaded: bool pruned: str def __init__(self, time: _Optional[float] = ..., added: _Optional[_Union[PbWorkflow, _Mapping]] = ..., updated: _Optional[_Union[PbWorkflow, _Mapping]] = ..., reloaded: bool = ..., pruned: _Optional[str] = ...) -> None: ... class AllDeltas(_message.Message): __slots__ = ("families", "family_proxies", "jobs", "tasks", "task_proxies", "edges", "workflow") FAMILIES_FIELD_NUMBER: _ClassVar[int] FAMILY_PROXIES_FIELD_NUMBER: _ClassVar[int] JOBS_FIELD_NUMBER: _ClassVar[int] TASKS_FIELD_NUMBER: _ClassVar[int] TASK_PROXIES_FIELD_NUMBER: _ClassVar[int] EDGES_FIELD_NUMBER: _ClassVar[int] WORKFLOW_FIELD_NUMBER: _ClassVar[int] families: FDeltas family_proxies: FPDeltas jobs: JDeltas tasks: TDeltas task_proxies: TPDeltas edges: EDeltas workflow: WDeltas def __init__(self, families: _Optional[_Union[FDeltas, _Mapping]] = ..., family_proxies: _Optional[_Union[FPDeltas, _Mapping]] = ..., jobs: _Optional[_Union[JDeltas, _Mapping]] = ..., tasks: _Optional[_Union[TDeltas, _Mapping]] = ..., task_proxies: _Optional[_Union[TPDeltas, _Mapping]] = ..., edges: _Optional[_Union[EDeltas, _Mapping]] = ..., workflow: _Optional[_Union[WDeltas, _Mapping]] = ...) -> None: ... cylc-flow-8.6.4/cylc/flow/job_runner_mgr.py0000664000175000017500000010316415202510242021075 0ustar alastairalastair# 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 . """Manage submission, poll and kill of a job to the job runners. The job runner interface is documented in cylc.flow.job_runner_handlers.documentation. Please update this file as the interface changes. """ from contextlib import suppress import json import os from pathlib import Path import shlex import stat import sys import traceback from shutil import rmtree from signal import SIGKILL from subprocess import DEVNULL # nosec from cylc.flow.task_message import ( CYLC_JOB_PID, CYLC_JOB_INIT_TIME, CYLC_JOB_EXIT_TIME, CYLC_JOB_EXIT, CYLC_MESSAGE) from cylc.flow.cylc_subproc import procopen from cylc.flow.task_job_logs import ( JOB_LOG_ERR, JOB_LOG_JOB, JOB_LOG_OUT, JOB_LOG_STATUS) from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED from cylc.flow.wallclock import get_current_time_string from cylc.flow.parsec.OrderedDict import OrderedDict JOB_FILES_REMOVED_MESSAGE = 'ERR_JOB_FILES_REMOVED' class JobPollContext(): """Context object for a job poll.""" CONTEXT_ATTRIBUTES = ( 'job_log_dir', # cycle/task/submit_num 'job_runner_name', 'job_id', # job id in job runner 'job_runner_exit_polled', # 0 for false, 1 for true 'run_status', # 0 for success, 1 for failure 'run_signal', # signal received on run failure 'time_submit_exit', # submit (exit) time 'time_run', # run start time 'time_run_exit', # run exit time 'job_runner_call_no_lines', # line count in job runner call stdout ) __slots__ = CONTEXT_ATTRIBUTES + ( 'pid', 'messages' ) def __init__(self, job_log_dir, **attrs): self.job_log_dir = job_log_dir self.job_runner_name = None self.job_id = None self.job_runner_exit_polled = None self.pid = None self.run_status = None self.run_signal = None self.time_submit_exit = None self.time_run = None self.time_run_exit = None self.job_runner_call_no_lines = None self.messages = [] if attrs: for key, value in attrs.items(): if key not in self.CONTEXT_ATTRIBUTES: raise ValueError('Invalid kwarg "%s"' % key) setattr(self, key, value) def update(self, other): """Update my data from given file context.""" for i in self.__slots__: setattr(self, i, getattr(other, i)) def get_summary_str(self): """Return the poll context as a summary string delimited by "|".""" ret = OrderedDict() for key in self.CONTEXT_ATTRIBUTES: value = getattr(self, key) if key == 'job_log_dir' or value is None: continue ret[key] = value return '%s|%s' % (self.job_log_dir, json.dumps(ret)) class JobRunnerManager(): """Job submission, poll and kill. Manage the importing of job submission method modules. """ CYLC_JOB_RUNNER_NAME = "CYLC_JOB_RUNNER_NAME" CYLC_JOB_ID = "CYLC_JOB_ID" CYLC_JOB_RUNNER_SUBMIT_TIME = "CYLC_JOB_RUNNER_SUBMIT_TIME" CYLC_JOB_RUNNER_EXIT_POLLED = "CYLC_JOB_RUNNER_EXIT_POLLED" FAIL_SIGNALS = ("EXIT", "ERR", "TERM", "XCPU") LINE_PREFIX_JOB_RUNNER_NAME = "# Job runner: " LINE_PREFIX_JOB_RUNNER_CMD_TMPL = "# Job runner command template: " LINE_PREFIX_EXECUTION_TIME_LIMIT = "# Execution time limit: " LINE_PREFIX_EOF = "#EOF: " LINE_PREFIX_JOB_LOG_DIR = "# Job log directory: " OUT_PREFIX_COMMAND = "[TASK JOB COMMAND]" OUT_PREFIX_MESSAGE = "[TASK JOB MESSAGE]" OUT_PREFIX_SUMMARY = "[TASK JOB SUMMARY]" OUT_PREFIX_CMD_ERR = "[TASK JOB ERROR]" _INSTANCES: dict = {} @classmethod def configure_workflow_run_dir(cls, workflow_run_dir): """Add local python module paths if not already done.""" for sub_dir in ["python", os.path.join("lib", "python")]: # TODO - eventually drop the deprecated "python" sub-dir. workflow_py = os.path.join(workflow_run_dir, sub_dir) if os.path.isdir(workflow_py) and workflow_py not in sys.path: sys.path.append(workflow_py) def __init__(self, clean_env=False, env=None, path=None): """Initialise JobRunnerManager.""" # Job submission environment. self.clean_env = clean_env self.path = path self.env = env def _get_sys(self, job_runner_name): """Return an instance of the class for "job_runner_name".""" if job_runner_name in self._INSTANCES: return self._INSTANCES[job_runner_name] for key in [f"cylc.flow.job_runner_handlers.{job_runner_name}", job_runner_name]: try: mod_of_name = __import__(key, fromlist=[key]) self._INSTANCES[job_runner_name] = getattr( mod_of_name, "JOB_RUNNER_HANDLER", None) return self._INSTANCES[job_runner_name] except ImportError: if key == job_runner_name: raise def format_directives(self, job_conf): """Format the job directives for a job file, if relevant.""" job_runner = self._get_sys(job_conf['platform']['job runner']) if hasattr(job_runner, "format_directives"): job_conf = { # strip $HOME from the job file path # paths in directives should be interpreted relative to $HOME # https://github.com/cylc/cylc-flow/issues/4247 **job_conf, 'job_file_path': ( job_conf["job_file_path"].replace(r"$HOME/", "") ) } return job_runner.format_directives(job_conf) def get_fail_signals(self, job_conf): """Return a list of failure signal names to trap in the job file.""" job_runner = self._get_sys(job_conf['platform']['job runner']) return getattr(job_runner, "FAIL_SIGNALS", self.FAIL_SIGNALS) def get_vacation_signal(self, job_conf): """Return the vacation signal name for a job file.""" job_runner = self._get_sys(job_conf['platform']['job runner']) if hasattr(job_runner, "get_vacation_signal"): return job_runner.get_vacation_signal(job_conf) def is_job_local_to_host(self, job_runner_name): """Return True if job runner runs jobs local to the submit host.""" return getattr( self._get_sys(job_runner_name), "SHOULD_KILL_PROC_GROUP", False) def jobs_kill(self, job_log_root, job_log_dirs): """Kill multiple jobs. job_log_root -- The log/job/ sub-directory of the workflow. job_log_dirs -- A list containing point/name/submit_num for jobs. """ # Note: The more efficient way to do this is to group the jobs by their # job runners, and call the kill command for each job runner once. # However, this will make it more difficult to determine if the kill # command for a particular job is successful or not. if "$" in job_log_root: job_log_root = os.path.expandvars(job_log_root) self.configure_workflow_run_dir(job_log_root.rsplit(os.sep, 2)[0]) now = get_current_time_string() for job_log_dir in job_log_dirs: ret_code, err = self.job_kill( os.path.join(job_log_root, job_log_dir, JOB_LOG_STATUS)) sys.stdout.write("%s%s|%s|%d\n" % ( self.OUT_PREFIX_SUMMARY, now, job_log_dir, ret_code)) # Note: Print STDERR to STDOUT may look a bit strange, but it # requires less logic for the workflow to parse the output. for line in err.strip().splitlines(): sys.stdout.write( f"{self.OUT_PREFIX_CMD_ERR}{now}|{job_log_dir}|{line}\n" ) def jobs_poll(self, job_log_root, job_log_dirs): """Poll multiple jobs. job_log_root -- The log/job/ sub-directory of the workflow. job_log_dirs -- A list containing point/name/submit_num for jobs. """ if "$" in job_log_root: job_log_root = os.path.expandvars(job_log_root) self.configure_workflow_run_dir(job_log_root.rsplit(os.sep, 2)[0]) ctx_list = [] # Contexts for all relevant jobs ctx_list_by_job_runner = {} # {job_runner_name1: [ctx1, ...], ...} for job_log_dir in job_log_dirs: ctx = self._jobs_poll_status_files(job_log_root, job_log_dir) if ctx is None: continue ctx_list.append(ctx) if not ctx.job_runner_name or not ctx.job_id: # Lost job runner information for some reason. # Mark the job as if it is no longer in the job runner. ctx.job_runner_exit_polled = 1 sys.stderr.write( "%s/%s: incomplete job runner info\n" % ( ctx.job_log_dir, JOB_LOG_STATUS)) # We can trust: # * Jobs previously polled to have exited the job runner. # * Jobs succeeded or failed with ERR/EXIT. if (ctx.job_runner_exit_polled or ctx.run_status == 0 or ctx.run_signal in ["ERR", "EXIT"]): continue if ctx.job_runner_name not in ctx_list_by_job_runner: ctx_list_by_job_runner[ctx.job_runner_name] = [] ctx_list_by_job_runner[ctx.job_runner_name].append(ctx) for job_runner_name, my_ctx_list in ctx_list_by_job_runner.items(): self._jobs_poll_runner( job_log_root, job_runner_name, my_ctx_list) cur_time_str = get_current_time_string() for ctx in ctx_list: for message in ctx.messages: sys.stdout.write("%s%s|%s|%s\n" % ( self.OUT_PREFIX_MESSAGE, cur_time_str, ctx.job_log_dir, message)) sys.stdout.write("%s%s|%s\n" % ( self.OUT_PREFIX_SUMMARY, cur_time_str, ctx.get_summary_str())) def jobs_submit(self, job_log_root, job_log_dirs, remote_mode=False, utc_mode=False): """Submit multiple jobs. job_log_root -- The log/job/ sub-directory of the workflow. job_log_dirs -- A list containing point/name/submit_num for jobs. remote_mode -- am I running on the remote job host? utc_mode -- is the workflow running in UTC mode? """ if "$" in job_log_root: job_log_root = os.path.expandvars(job_log_root) self.configure_workflow_run_dir(job_log_root.rsplit(os.sep, 2)[0]) if remote_mode: items = self._jobs_submit_prep_by_stdin(job_log_root, job_log_dirs) else: items = self._jobs_submit_prep_by_args(job_log_root, job_log_dirs) now = get_current_time_string(override_use_utc=utc_mode) for job_log_dir, job_runner_name, submit_opts in items: job_file_path = os.path.join( job_log_root, job_log_dir, JOB_LOG_JOB) if not job_runner_name: sys.stdout.write("%s%s|%s|1|\n" % ( self.OUT_PREFIX_SUMMARY, now, job_log_dir)) continue ret_code, out, err, job_id = self._job_submit_impl( job_file_path, job_runner_name, submit_opts) sys.stdout.write("%s%s|%s|%d|%s\n" % ( self.OUT_PREFIX_SUMMARY, now, job_log_dir, ret_code, job_id)) for key, value in [("STDERR", err), ("STDOUT", out)]: if value is None: continue for line in value.strip().splitlines(): sys.stdout.write( f"{self.OUT_PREFIX_COMMAND}{now}" f"|{job_log_dir}|[{key}] {line}\n" ) def job_kill(self, st_file_path): """Ask job runner to terminate the job specified in "st_file_path". Return 0 on success, non-zero integer on failure. """ # WORKFLOW_RUN_DIR/log/job/CYCLE/TASK/SUBMIT/job.status self.configure_workflow_run_dir(st_file_path.rsplit(os.sep, 6)[0]) try: with open(st_file_path) as st_file: for line in st_file: if line.startswith(f"{self.CYLC_JOB_RUNNER_NAME}="): job_runner = self._get_sys( line.strip().split("=", 1)[1] ) break else: return ( 1, "Cannot determine job runner from " f"{JOB_LOG_STATUS} file" ) st_file.seek(0, 0) # rewind if getattr(job_runner, "SHOULD_KILL_PROC_GROUP", False): for line in st_file: if line.startswith(CYLC_JOB_PID + "="): pid = line.strip().split("=", 1)[1] try: os.killpg(os.getpgid(int(pid)), SIGKILL) except (OSError, ValueError) as exc: traceback.print_exc() return (1, str(exc)) else: return (0, "") st_file.seek(0, 0) # rewind if hasattr(job_runner, "KILL_CMD_TMPL"): for line in st_file: if not line.startswith(f"{self.CYLC_JOB_ID}="): continue job_id = line.strip().split("=", 1)[1] command = shlex.split( job_runner.KILL_CMD_TMPL % {"job_id": job_id}) try: proc = procopen(command, stdindevnull=True, stderrpipe=True) except OSError as exc: # subprocess.Popen has a bad habit of not setting # the filename of the executable when it raises an # OSError. if not exc.filename: exc.filename = command[0] traceback.print_exc() return (1, str(exc)) else: return ( proc.wait(), proc.communicate()[1].decode() ) return (1, f"Cannot determine job ID from {JOB_LOG_STATUS} file") except IOError as exc: return (1, str(exc)) @classmethod def _create_nn(cls, job_file_path): """Create NN symbolic link if necessary, and remove any old job logs. If NN => 01, remove numbered dirs with submit numbers greater than 01. Helper for "self._job_submit_impl". """ job_file_dir = os.path.dirname(job_file_path) source = os.path.basename(job_file_dir) task_log_dir = os.path.dirname(job_file_dir) nn_path = os.path.join(task_log_dir, "NN") try: old_source = os.readlink(nn_path) except OSError: old_source = None if old_source is not None and old_source != source: os.unlink(nn_path) old_source = None if old_source is None: os.symlink(source, nn_path) # On submit 1, remove any left over digit directories from prev runs if source == "01": for name in os.listdir(task_log_dir): if name != source and name.isdigit(): # Ignore errors, not disastrous if rmtree fails rmtree( os.path.join(task_log_dir, name), ignore_errors=True) # Delete old job logs if necessary for name in JOB_LOG_ERR, JOB_LOG_OUT: with suppress(FileNotFoundError): os.unlink(os.path.join(job_file_dir, name)) @classmethod def _filter_submit_output(cls, st_file_path, job_runner, out, err): """Filter submit command output, if relevant.""" job_id = None if hasattr(job_runner, "REC_ID_FROM_SUBMIT_ERR"): text = err rec_id = job_runner.REC_ID_FROM_SUBMIT_ERR elif hasattr(job_runner, "REC_ID_FROM_SUBMIT_OUT"): text = out rec_id = job_runner.REC_ID_FROM_SUBMIT_OUT if rec_id: for line in str(text).splitlines(): match = rec_id.match(line) if match: job_id = match.group("id") if hasattr(job_runner, "manip_job_id"): job_id = job_runner.manip_job_id(job_id) with open(st_file_path, "a") as job_status_file: job_status_file.write("{0}={1}\n".format( cls.CYLC_JOB_ID, job_id)) job_status_file.write("{0}={1}\n".format( cls.CYLC_JOB_RUNNER_SUBMIT_TIME, get_current_time_string())) break if hasattr(job_runner, "filter_submit_output"): out, err = job_runner.filter_submit_output(out, err) return out, err, job_id def _jobs_poll_status_files(self, job_log_root, job_log_dir): """Helper 1 for self.jobs_poll(job_log_root, job_log_dirs).""" ctx = JobPollContext(job_log_dir) # If the log directory has been deleted prematurely, return a task # failure and an explanation: if not os.path.exists(os.path.join(job_log_root, ctx.job_log_dir)): # The job may still be in the job runner and may yet succeed, # but we assume it failed & exited because it's the best we # can do as it is no longer possible to poll it. ctx.run_status = 1 ctx.job_runner_exit_polled = 1 ctx.run_signal = JOB_FILES_REMOVED_MESSAGE return ctx try: with open( os.path.join(job_log_root, ctx.job_log_dir, JOB_LOG_STATUS) ) as handle: for line in handle: if "=" not in line: continue key, value = line.strip().split("=", 1) if key == self.CYLC_JOB_RUNNER_NAME: ctx.job_runner_name = value elif key == self.CYLC_JOB_ID: ctx.job_id = value elif key == self.CYLC_JOB_RUNNER_EXIT_POLLED: ctx.job_runner_exit_polled = 1 elif key == CYLC_JOB_PID: ctx.pid = value elif key == self.CYLC_JOB_RUNNER_SUBMIT_TIME: ctx.time_submit_exit = value elif key == CYLC_JOB_INIT_TIME: ctx.time_run = value elif key == CYLC_JOB_EXIT_TIME: ctx.time_run_exit = value elif key == CYLC_JOB_EXIT: if value == TASK_OUTPUT_SUCCEEDED.upper(): ctx.run_status = 0 else: ctx.run_status = 1 ctx.run_signal = value elif key == CYLC_MESSAGE: ctx.messages.append(value) except IOError as exc: sys.stderr.write(f"{exc}\n") return return ctx def _jobs_poll_runner(self, job_log_root, job_runner_name, my_ctx_list): """Helper 2 for self.jobs_poll(job_log_root, job_log_dirs).""" exp_job_ids = [ctx.job_id for ctx in my_ctx_list] bad_job_ids = list(exp_job_ids) exp_pids = [] bad_pids = [] items = [[self._get_sys(job_runner_name), exp_job_ids, bad_job_ids]] if getattr(items[0][0], "SHOULD_POLL_PROC_GROUP", False): exp_pids = [ctx.pid for ctx in my_ctx_list if ctx.pid is not None] bad_pids.extend(exp_pids) items.append([self._get_sys("background"), exp_pids, bad_pids]) debug_messages = [] for job_runner, exp_ids, bad_ids in items: if hasattr(job_runner, "get_poll_many_cmd"): # Some poll commands may not be as simple cmd = job_runner.get_poll_many_cmd(exp_ids) else: # if hasattr(job_runner, "POLL_CMD"): # Simple poll command that takes a list of job IDs cmd = [job_runner.POLL_CMD, *exp_ids] try: proc = procopen(cmd, stdindevnull=True, stderrpipe=True, stdoutpipe=True) except OSError as exc: # subprocess.Popen has a bad habit of not setting the # filename of the executable when it raises an OSError. if not exc.filename: exc.filename = cmd[0] sys.stderr.write(f"{exc}\n") return ret_code = proc.wait() out, err = (f.decode() for f in proc.communicate()) debug_messages.append('{0} - {1}'.format( job_runner, len(out.split('\n'))) ) sys.stderr.write(err) if (ret_code and hasattr(job_runner, "POLL_CANT_CONNECT_ERR") and job_runner.POLL_CANT_CONNECT_ERR in err): # Poll command failed because it cannot connect to job runner # Assume jobs are still healthy until the job runner is back. bad_ids[:] = [] elif hasattr(job_runner, "filter_poll_many_output"): # Allow custom filter for id_ in job_runner.filter_poll_many_output(out): with suppress(ValueError): bad_ids.remove(id_) else: # Just about all poll commands return a table, with column 1 # being the job ID. The logic here should be sufficient to # ensure that any table header is ignored. for line in out.splitlines(): try: head = line.split(None, 1)[0] except IndexError: continue if head in exp_ids: with suppress(ValueError): bad_ids.remove(head) debug_flag = False for ctx in my_ctx_list: ctx.job_runner_exit_polled = int( ctx.job_id in bad_job_ids) # Exited job runner, but process still running # This can happen to jobs in some "at" implementation if ctx.job_runner_exit_polled and ctx.pid in exp_pids: if ctx.pid not in bad_pids: ctx.job_runner_exit_polled = 0 else: debug_flag = True # Add information to "job.status" if ctx.job_runner_exit_polled: try: with open(os.path.join( job_log_root, ctx.job_log_dir, JOB_LOG_STATUS), "a" ) as handle: handle.write("{0}={1}\n".format( self.CYLC_JOB_RUNNER_EXIT_POLLED, get_current_time_string()) ) except IOError as exc: sys.stderr.write(f"{exc}\n") # Re-read the status file in case the job started and exited # between the file and batch system checks, which would be # interpreted as submit-failed (job exited without starting). # Possible if polling many jobs and/or system heavily loaded. file_ctx = self._jobs_poll_status_files( job_log_root, ctx.job_log_dir) ctx.update(file_ctx) if debug_flag: ctx.job_runner_call_no_lines = ', '.join(debug_messages) def _job_submit_impl( self, job_file_path, job_runner_name, submit_opts): """Helper for self.jobs_submit() and self.job_submit().""" # Create NN symbolic link, if necessary self._create_nn(job_file_path) # Start new status file with open(f"{job_file_path}.status", "w") as job_status_file: job_status_file.write( "{0}={1}\n".format( self.CYLC_JOB_RUNNER_NAME, job_runner_name ) ) # Submit job job_runner = self._get_sys(job_runner_name) if not self.clean_env: # Pass the whole environment to the job submit subprocess. # (Note this runs on the job host). env = os.environ else: # $HOME is required by job.sh on the job host. env = {'HOME': os.environ.get('HOME', '')} # Pass selected extra variables to the job submit subprocess. for var in self.env: env[var] = os.environ.get(var, '') if self.path is not None: # Append to avoid overriding an inherited PATH (e.g. in a venv) env['PATH'] = env.get('PATH', '') + ':' + ':'.join(self.path) if hasattr(job_runner, "submit"): submit_opts['env'] = env # job_runner.submit should handle OSError, if relevant. ret_code, out, err = job_runner.submit(job_file_path, submit_opts) else: proc_stdin_arg = None # Set command STDIN to DEVNULL by default to prevent leakage of # STDIN from current environment. proc_stdin_value = DEVNULL # nosec if hasattr(job_runner, "get_submit_stdin"): proc_stdin_arg, proc_stdin_value = job_runner.get_submit_stdin( job_file_path, submit_opts) if isinstance(proc_stdin_value, str): proc_stdin_value = proc_stdin_value.encode() if hasattr(job_runner, "SUBMIT_CMD_ENV"): env.update(job_runner.SUBMIT_CMD_ENV) job_runner_cmd_tmpl = submit_opts.get("job_runner_cmd_tmpl") if job_runner_cmd_tmpl: # No need to catch OSError when using shell. It is unlikely # that we do not have a shell, and still manage to get as far # as here. job_runner_cmd = job_runner_cmd_tmpl % {"job": job_file_path} proc = procopen(job_runner_cmd, stdin=proc_stdin_arg, stdoutpipe=True, stderrpipe=True, usesh=True, env=env) # calls to open a shell are aggregated in # cylc_subproc.procopen() else: command = shlex.split( job_runner.SUBMIT_CMD_TMPL % {"job": job_file_path}) try: proc = procopen( command, stdin=proc_stdin_arg, stdoutpipe=True, stderrpipe=True, env=env, # paths in directives should be interpreted relative to # $HOME # https://github.com/cylc/cylc-flow/issues/4247 cwd=Path('~').expanduser() ) except OSError as exc: # subprocess.Popen has a bad habit of not setting the # filename of the executable when it raises an OSError. if not exc.filename: exc.filename = command[0] return 1, "", str(exc), "" out, err = (f.decode() for f in proc.communicate(proc_stdin_value)) ret_code = proc.wait() with suppress(AttributeError, IOError): proc_stdin_arg.close() # Filter submit command output, if relevant # Get job ID, if possible job_id = None if out or err: try: out, err, job_id = self._filter_submit_output( f"{job_file_path}.status", job_runner, out, err) except OSError: ret_code = 1 self.job_kill(f"{job_file_path}.status") return ret_code, out, err, job_id def _jobs_submit_prep_by_args(self, job_log_root, job_log_dirs): """Prepare job files for submit by reading files in arguments. Job files are specified in the arguments in local mode. Extract job submission methods and job submission command templates from each job file. Return a list, where each element contains something like: (job_log_dir, job_runner_name, submit_opts) """ items = [] for job_log_dir in job_log_dirs: job_file_path = os.path.join(job_log_root, job_log_dir, "job") job_runner_name = None submit_opts = {} with open(job_file_path, 'r') as job_file: for line in job_file: if line.startswith(self.LINE_PREFIX_JOB_RUNNER_NAME): job_runner_name = line.replace( self.LINE_PREFIX_JOB_RUNNER_NAME, "").strip() elif line.startswith(self.LINE_PREFIX_JOB_RUNNER_CMD_TMPL): submit_opts["job_runner_cmd_tmpl"] = line.replace( self.LINE_PREFIX_JOB_RUNNER_CMD_TMPL, "").strip() elif line.startswith( self.LINE_PREFIX_EXECUTION_TIME_LIMIT ): submit_opts["execution_time_limit"] = float( line.replace( self.LINE_PREFIX_EXECUTION_TIME_LIMIT, "" ).strip() ) items.append((job_log_dir, job_runner_name, submit_opts)) return items def _jobs_submit_prep_by_stdin(self, job_log_root, job_log_dirs): """Prepare job files for submit by reading from STDIN. Job files are uploaded via STDIN in remote mode. Extract job submission methods and job submission command templates from each job file. Return a list, where each element contains something like: (job_log_dir, job_runner_name, submit_opts) """ items = [[job_log_dir, None, {}] for job_log_dir in job_log_dirs] items_map = {} for item in items: items_map[item[0]] = item handle = None job_runner_name = None submit_opts = {} job_log_dir = None lines = [] # Get job files from STDIN. # Get job runner name and job runner command template from each job # file. # Write job file in correct location. while True: # Note: "for cur_line in sys.stdin:" may hang cur_line = sys.stdin.readline() if not cur_line: if handle is not None: handle.close() break if cur_line.startswith(self.LINE_PREFIX_JOB_RUNNER_NAME): job_runner_name = cur_line.replace( self.LINE_PREFIX_JOB_RUNNER_NAME, "").strip() elif cur_line.startswith(self.LINE_PREFIX_JOB_RUNNER_CMD_TMPL): submit_opts["job_runner_cmd_tmpl"] = cur_line.replace( self.LINE_PREFIX_JOB_RUNNER_CMD_TMPL, "").strip() elif cur_line.startswith(self.LINE_PREFIX_EXECUTION_TIME_LIMIT): submit_opts["execution_time_limit"] = float(cur_line.replace( self.LINE_PREFIX_EXECUTION_TIME_LIMIT, "").strip()) elif cur_line.startswith(self.LINE_PREFIX_JOB_LOG_DIR): job_log_dir = cur_line.replace( self.LINE_PREFIX_JOB_LOG_DIR, "").strip() os.makedirs( os.path.join(job_log_root, job_log_dir), exist_ok=True) if handle is not None: handle.close() handle = open( # noqa: SIM115 (can't convert to with open) os.path.join(job_log_root, job_log_dir, "job.tmp"), "wb") if handle is None: lines.append(cur_line) else: for line in lines + [cur_line]: handle.write(line.encode()) lines = [] if cur_line.startswith(self.LINE_PREFIX_EOF + job_log_dir): handle.close() # Make it executable os.chmod(handle.name, ( os.stat(handle.name).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)) # Rename from "*/job.tmp" to "*/job" os.rename(handle.name, handle.name[:-4]) try: items_map[job_log_dir][1] = job_runner_name items_map[job_log_dir][2] = submit_opts except KeyError: pass handle = None job_log_dir = None job_runner_name = None submit_opts = {} return items cylc-flow-8.6.4/cylc/flow/cylc_subproc.py0000664000175000017500000000427615202510242020560 0ustar alastairalastair# 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 . """ A wrapper function to aggregate these calls in one file. Bandit B602: subprocess_popen_with_shell_equals_true https://docs.openstack.org/developer/bandit/plugins/subprocess_popen_with_shell_equals_true.html B605: start_process_with_a_shell https://docs.openstack.org/developer/bandit/plugins/start_process_with_a_shell.html """ from shlex import split from subprocess import PIPE, STDOUT, DEVNULL, Popen # nosec # pylint: disable=too-many-arguments # pylint: disable=too-many-locals def procopen(cmd, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, usesh=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, splitcmd=False, stdoutpipe=False, stdoutout=False, stderrpipe=False, stderrout=False, stdindevnull=DEVNULL): shell = usesh if stdoutpipe is True: stdout = PIPE elif stdoutout is True: stdout = STDOUT if stderrpipe is True: stderr = PIPE elif stderrout is True: stderr = STDOUT if stdindevnull is True: stdin = DEVNULL if splitcmd is True: command = split(cmd) else: command = cmd process = Popen(command, bufsize, executable, stdin, stdout, # nosec stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags) return process cylc-flow-8.6.4/cylc/flow/taskdef.py0000664000175000017500000004013715202510242017506 0ustar alastairalastair# 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 . """Task definition.""" from collections import deque from typing import ( TYPE_CHECKING, Dict, List, NamedTuple, Set, Tuple, ) from cylc.flow.exceptions import TaskDefError import cylc.flow.flags from cylc.flow.task_id import TaskID from cylc.flow.task_outputs import ( SORT_ORDERS, TASK_OUTPUT_FAILED, TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED, ) if TYPE_CHECKING: from cylc.flow.cycling import ( PointBase, SequenceBase, ) from cylc.flow.task_trigger import ( Dependency, TaskTrigger, ) class TaskTuple(NamedTuple): name: str point: 'PointBase' is_abs: bool def generate_graph_children( tdef: 'TaskDef', point: 'PointBase' ) -> Dict[str, List[TaskTuple]]: """Determine graph children of this task at point.""" graph_children: Dict[str, List[TaskTuple]] = {} for seq, dout in tdef.graph_children.items(): for output, downs in dout.items(): for name, trigger in downs: child_point = trigger.get_child_point(point, seq) is_abs = ( trigger.offset_is_absolute or trigger.offset_is_from_icp ) if is_abs and trigger.get_parent_point(point) != point: # If 'foo[^] => bar' only spawn off of '^'. continue if seq.is_valid(child_point): # E.g.: foo should trigger only on T06: # PT6H = "waz" # T06 = "waz[-PT6H] => foo" graph_children.setdefault(output, []).append( TaskTuple(name, child_point, is_abs) ) if tdef.sequential: # Add next-instance child. nexts = [] for seq in tdef.sequences: nxt = seq.get_next_point(point) if nxt is not None: # Within sequence bounds. nexts.append(nxt) if nexts: graph_children.setdefault(TASK_OUTPUT_SUCCEEDED, []).append( TaskTuple(tdef.name, min(nexts), False) ) return graph_children def generate_graph_parents( tdef: 'TaskDef', point: 'PointBase', taskdefs: Dict[str, 'TaskDef'] ) -> Dict['SequenceBase', List[TaskTuple]]: """Determine concrete graph parents of task tdef at point. Infer parents by reversing upstream triggers that lead to point/task. """ graph_parents: Dict['SequenceBase', List[TaskTuple]] = {} for seq, triggers in tdef.graph_parents.items(): if not seq.is_valid(point): # Don't infer parents if the trigger belongs to a sequence that # does not include the child point. E.g.: # T06 = "waz[-PT6H] => foo" # here waz[-PT6H] is a parent of T06/foo but not of T12/foo. continue graph_parents[seq] = [] for parent_name, trigger in triggers: parent_point = trigger.get_parent_point(point) if ( parent_point != point and not taskdefs[parent_name].is_valid_point(parent_point) ): # Don't infer inter-cycle parents if the upstream point is # not valid for the parent (which depends on its sequences). # NOTE this includes pre-initial dependence where the offset # extends back beyond the initial point AND erroneous offsets # when different tasks are involved, e.g.: # woo[-Px] => foo # where (point -Px) does not land on a valid point for woo. # TODO ideally validation would flag this as an error. continue is_abs = trigger.offset_is_absolute or trigger.offset_is_from_icp graph_parents[seq].append( TaskTuple(parent_name, parent_point, is_abs) ) if tdef.sequential: # Add implicit previous-instance parent. prevs = [] for seq in tdef.sequences: prev = seq.get_prev_point(point) if prev is not None: # Within sequence bounds. prevs.append(prev) if prevs: graph_parents.setdefault(seq, []).append( TaskTuple(tdef.name, min(prevs), False) ) return graph_parents class TaskDef: """Task definition.""" # Memory optimization - constrain possible attributes to this list. __slots__ = [ "rtconfig", "start_point", "initial_point", "sequences", "used_in_offset_trigger", "max_future_prereq_offset", "sequential", "is_coldstart", "workflow_polling_cfg", "expiration_offset", "namespace_hierarchy", "dependencies", "outputs", "param_var", "graph_children", "graph_parents", "has_abs_triggers", "external_triggers", "xtrig_labels", "name", "elapsed_times"] # Store the elapsed times for a maximum of 10 cycles MAX_LEN_ELAPSED_TIMES = 10 def __init__(self, name, rtcfg, start_point, initial_point): if not TaskID.is_valid_name(name): raise TaskDefError("Illegal task name: %s" % name) self.name: str = name self.rtconfig = rtcfg self.start_point = start_point self.initial_point = initial_point self.sequences: List[SequenceBase] = [] self.used_in_offset_trigger = False # some defaults self.max_future_prereq_offset = None self.sequential = False self.workflow_polling_cfg = {} self.expiration_offset = None self.namespace_hierarchy = [] self.dependencies: Dict[SequenceBase, List[Dependency]] = {} self.outputs = {} # {output: (message, is_required)} self.graph_children: Dict[ SequenceBase, Dict[str, Set[Tuple[str, TaskTrigger]]] ] = {} self.graph_parents: Dict[ SequenceBase, Set[Tuple[str, TaskTrigger]] ] = {} self.param_var = {} self.external_triggers = [] self.xtrig_labels = {} # {sequence: [labels]} self.elapsed_times = deque(maxlen=self.MAX_LEN_ELAPSED_TIMES) self._add_std_outputs() self.has_abs_triggers = False def add_output(self, output, message): """Add a new task output as defined under [runtime].""" # optional/required is None until defined by the graph self.outputs[output] = (message, None) def get_output(self, message): """Return output name corresponding to task message.""" for name, (msg, _) in self.outputs.items(): if msg == message: return name raise KeyError(f"Unknown task output message: {message}") def _add_std_outputs(self): """Add the standard outputs.""" # optional/required is None until defined by the graph for output in SORT_ORDERS: self.outputs[output] = (output, None) def set_required_output(self, output, required): """Set outputs to required or optional.""" # (Note outputs and associated messages are already defined.) message, _ = self.outputs[output] self.outputs[output] = (message, required) def tweak_outputs(self): """Output consistency checking and tweaking.""" # If :succeed or :fail not set, assume success is required. if ( self.outputs[TASK_OUTPUT_SUCCEEDED][1] is None and self.outputs[TASK_OUTPUT_FAILED][1] is None ): self.set_required_output(TASK_OUTPUT_SUCCEEDED, True) # In Cylc 7 back compat mode, make all success outputs required. if cylc.flow.flags.cylc7_back_compat: for output in [ TASK_OUTPUT_SUBMITTED, TASK_OUTPUT_SUCCEEDED ]: self.set_required_output(output, True) def add_graph_child( self, trigger: 'TaskTrigger', taskname: str, sequence: 'SequenceBase' ) -> None: """Record child task instances that depend on my outputs. {sequence: { output: [(a,t1), (b,t2), ...] # (task-name, trigger) } } """ self.graph_children.setdefault( sequence, {} ).setdefault( trigger.output, set() ).add((taskname, trigger)) def add_graph_parent( self, trigger: 'TaskTrigger', parent: str, sequence: 'SequenceBase' ) -> None: """Record task instances that I depend on. { sequence: set([(a,t1), (b,t2), ...]) # (task-name, trigger) } """ self.graph_parents.setdefault(sequence, set()).add((parent, trigger)) def add_dependency(self, dependency, sequence): """Add a dependency to a named sequence. Args: dependency (cylc.flow.task_trigger.Dependency): The dependency to add. sequence (cylc.flow.cycling.SequenceBase): The sequence for which this dependency applies. """ self.dependencies.setdefault(sequence, []).append(dependency) if any( trig.offset_is_from_icp or trig.offset_is_absolute for trig in dependency.task_triggers ): self.has_abs_triggers = True def add_xtrig_label(self, xtrig_label, sequence): """Add an xtrigger to a named sequence. Args: xtrig_label: The xtrigger label to add. sequence (cylc.cycling.SequenceBase): The sequence for which this xtrigger applies. """ self.xtrig_labels.setdefault(sequence, []).append(xtrig_label) def add_sequence(self, sequence): """Add a sequence.""" if sequence not in self.sequences: self.sequences.append(sequence) def describe(self): """Return title and description of the current task.""" return self.rtconfig['meta'] def check_for_explicit_cycling(self): """Check for explicitly somewhere. Must be called after all graph sequences added. """ if len(self.sequences) == 0 and self.used_in_offset_trigger: raise TaskDefError( "No cycling sequences defined for %s" % self.name) def get_parent_points(self, point): """Return the cycle points of my parents, at point.""" parent_points = set() for seq in self.sequences: if not seq.is_valid(point): continue if seq in self.dependencies: # task has prereqs in this sequence for dep in self.dependencies[seq]: if dep.suicide: continue for trig in dep.task_triggers: parent_points.add(trig.get_parent_point(point)) return parent_points def get_prereqs(self, point): """Return my prereqs, at point.""" prereqs = set() for seq in self.sequences: if not seq.is_valid(point): continue if seq in self.dependencies: # task has prereqs in this sequence for dep in self.dependencies[seq]: if dep.suicide: continue prereqs.add(dep.get_prerequisite(point, self)) return prereqs def get_xtrigs(self, point): """Return my xtrigger labels, at point.""" xlabels = set() for seq in self.sequences: if not seq.is_valid(point): continue if seq in self.xtrig_labels: # task has xtriggers in this sequence xlabels.update(self.xtrig_labels[seq]) return xlabels def get_triggers(self, point): """Return my triggers, at point.""" triggers = set() for seq in self.sequences: if not seq.is_valid(point): continue if seq in self.dependencies: # task has prereqs in this sequence for dep in self.dependencies[seq]: if dep.suicide: continue for trig in dep.task_triggers: triggers.add(trig) return triggers def has_only_abs_triggers(self, point): """Return whether I have only absolute triggers at point.""" if not self.has_abs_triggers: return False # Has abs triggers somewhere, but need to check at point. has_abs = False for seq in self.sequences: if not seq.is_valid(point) or seq not in self.dependencies: continue for dep in self.dependencies[seq]: for trig in dep.task_triggers: if ( trig.offset_is_absolute or trig.offset_is_from_icp or # Don't count suicide as a normal trigger: dep.suicide ): has_abs = True else: return False return has_abs def is_valid_point(self, point: 'PointBase') -> bool: """Return True if point is on-sequence and within bounds.""" return any( sequence.is_valid(point) for sequence in self.sequences ) def first_point(self, icp): """Return the first point for this task.""" point = None adjusted = [] for seq in self.sequences: pt = seq.get_first_point(icp) if pt: # may be None if beyond the sequence bounds adjusted.append(pt) if adjusted: point = min(adjusted) return point def next_point(self, point): """Return the next cycle point after point.""" p_next = None adjusted = [] for seq in self.sequences: nxt = seq.get_next_point(point) if nxt: # may be None if beyond the sequence bounds adjusted.append(nxt) if adjusted: p_next = min(adjusted) return p_next def is_parentless(self, point: 'PointBase', cutoff: 'PointBase') -> bool: """Return True if task has no parents at the given point. Tasks are considered parentless if they have: - no parents at all - all parents < cutoff cycle point - only absolute triggers Absolute-triggered tasks are auto-spawned like true parentless tasks, (once the trigger is satisfied they are effectively parentless) but with a prerequisite that gets satisfied when the absolute output is completed at runtime. Args: point: The cycle point to check. cutoff: This should be the start cycle point for the startup spawning, or the intial cycle point for manually triggered tasks. """ if not self.graph_parents: # No parents at any point return True if self.sequential: # Implicit parents return False parent_points = self.get_parent_points(point) return ( not parent_points or all(x < cutoff for x in parent_points) or self.has_only_abs_triggers(point) ) def __repr__(self) -> str: """ >>> TaskDef( ... name='oliver', rtcfg={}, start_point='1', ... initial_point='1' ... ) """ return f"" cylc-flow-8.6.4/cylc/flow/listify.py0000664000175000017500000000423315202510242017545 0ustar alastairalastair# 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 . import re REC_CONDITIONALS = re.compile("([&|()])") def listify(message): """Convert a string containing a logical expression to a list Examples: >>> listify('(foo)') ['foo'] >>> listify('foo & (bar | baz)') ['foo', '&', ['bar', '|', 'baz']] >>> listify('(a&b)|(c|d)&(e|f)') [['a', '&', 'b'], '|', ['c', '|', 'd'], '&', ['e', '|', 'f']] >>> listify('a & (b & c)') ['a', '&', ['b', '&', 'c']] >>> listify('a & b') ['a', '&', 'b'] >>> listify('a & (b)') ['a', '&', 'b'] >>> listify('((foo)') Traceback (most recent call last): ValueError: ((foo) >>> listify('(foo))') Traceback (most recent call last): ValueError: (foo)) """ message = message.replace("'", "\"") ret_list = [] stack = [ret_list] for item in REC_CONDITIONALS.split(message): item = item.strip() if item and item not in ["(", ")"]: stack[-1].append(item) elif item == "(": stack[-1].append([]) stack.append(stack[-1][-1]) elif item == ")": stack.pop() if not stack: raise ValueError(message) if isinstance(stack[-1][-1], list) and len(stack[-1][-1]) == 1: stack[-1][-1] = stack[-1][-1][0] if len(stack) > 1: raise ValueError(message) return ret_list cylc-flow-8.6.4/cylc/flow/install.py0000664000175000017500000005213415202510242017533 0ustar alastairalastair# 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 . """Functionality for (local) workflow installation.""" import logging import os import re import shlex from contextlib import suppress from pathlib import Path from subprocess import ( PIPE, Popen, ) from typing import ( Any, Dict, List, Optional, Tuple, Union, ) from cylc.flow import LOG from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.exceptions import ( InputError, WorkflowFilesError, ) from cylc.flow.loggingutil import ( CylcLogFormatter, close_log, get_next_log_number, get_sorted_logs_by_time, ) from cylc.flow.pathutil import ( expand_path, get_cylc_run_dir, get_next_rundir_number, get_workflow_run_dir, make_localhost_symlinks, ) from cylc.flow.remote import ( DEFAULT_RSYNC_OPTS, ) from cylc.flow.util import cli_format from cylc.flow.workflow_files import ( WorkflowFiles, abort_if_flow_file_in_path, check_deprecation, check_flow_file, get_cylc_run_abs_path, is_valid_run_dir, validate_workflow_name, ) NESTED_DIRS_MSG = ( "Nested {dir_type} directories not allowed - cannot install workflow" " in '{dest}' as '{existing}' is already a valid {dir_type} directory." ) def _get_logger(rund, log_name, open_file=True): """Get log and create and open if necessary. Args: rund: The workflow run directory of the associated workflow. log_name: The name of the log to open. open_file: Open the appropriate log file and add it as a file handler to the logger. I.E. Start writing the log to a file if not already doing so. """ logger = logging.getLogger(log_name) logger.setLevel(logging.INFO) if open_file and not logger.hasHandlers(): _open_install_log(rund, logger) return logger def _open_install_log(rund, logger): """Open Cylc log handlers for install/reinstall.""" rund = Path(rund).expanduser() log_type = logger.name[logger.name.startswith('cylc-') and len('cylc-'):] log_dir = Path( rund, WorkflowFiles.LogDir.DIRNAME, WorkflowFiles.LogDir.INSTALL) log_files = get_sorted_logs_by_time(log_dir, '*.log') log_num = get_next_log_number(log_files[-1]) if log_files else 1 log_path = Path(log_dir, f"{log_num:02d}-{log_type}.log") log_parent_dir = log_path.parent log_parent_dir.mkdir(exist_ok=True, parents=True) handler = logging.FileHandler(log_path) handler.setFormatter(CylcLogFormatter()) logger.addHandler(handler) def get_rsync_rund_cmd(src, dst, reinstall=False, dry_run=False): """Create and return the rsync command used for cylc install/re-install. Args: src (str): file path location of source directory dst (str): file path location of destination directory reinstall (bool): indicate reinstall (--delete option added) dry-run (bool): indicate dry-run, rsync will not take place but report output if a real run were to be executed Return: list: command to use for rsync. """ rsync_cmd = shlex.split( glbl_cfg().get(['platforms', 'localhost', 'rsync command']) ) rsync_cmd += DEFAULT_RSYNC_OPTS if dry_run: rsync_cmd.append("--dry-run") if reinstall: rsync_cmd.append('--delete') exclusions = [ '.git', '.svn', '.cylcignore', 'opt/rose-suite-cylc-install.conf', WorkflowFiles.LogDir.DIRNAME, WorkflowFiles.WORK_DIR, WorkflowFiles.SHARE_DIR, WorkflowFiles.Install.DIRNAME, WorkflowFiles.Service.DIRNAME ] # This is a hack to make sure that changes to rose-suite.conf # are considered when re-installing. # It should be removed after https://github.com/cylc/cylc-rose/issues/149 if not dry_run: exclusions.append('rose-suite.conf') for exclude in exclusions: if ( Path(src).joinpath(exclude).exists() or Path(dst).joinpath(exclude).exists() ): # Note '/' is the rsync "anchor" to the top level: rsync_cmd.append(f"--exclude=/{exclude}") cylcignore_file = Path(src).joinpath('.cylcignore') if cylcignore_file.exists(): rsync_cmd.append(f"--exclude-from={cylcignore_file}") rsync_cmd.append(f"{src}/") rsync_cmd.append(f"{dst}/") return rsync_cmd def reinstall_workflow( source: Path, named_run: str, rundir: Path, dry_run: bool = False ) -> str: """Reinstall workflow. Args: source: source directory named_run: name of the run e.g. my-flow/run1 rundir: run directory dry_run: if True, will not execute the file transfer but report what would be changed. Raises: WorkflowFilesError: If rsync returns non-zero. Returns: Stdout from the rsync command. """ validate_source_dir(source, named_run) check_nested_dirs(rundir) reinstall_log = _get_logger( rundir, 'cylc-reinstall', open_file=not dry_run, # don't open the log file for --dry-run ) reinstall_log.info( f'Reinstalling "{named_run}", from "{source}" to "{rundir}"' ) rsync_cmd = get_rsync_rund_cmd( source, rundir, reinstall=True, dry_run=dry_run, ) rsync_cmd.append('--out-format=%i %o %n%L') # %i: itemized changes - needed for rsync to report files with # changed permissions # Run rsync command: reinstall_log.info(cli_format(rsync_cmd)) LOG.debug(cli_format(rsync_cmd)) proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True) # nosec # * command is constructed via internal interface stdout, stderr = (i.strip() for i in proc.communicate()) reinstall_log.info( f"Copying files from {source} to {rundir}" f"\n{stdout}" ) if proc.returncode != 0: raise WorkflowFilesError( f'An error occurred reinstalling from {source} to {rundir}' f'\n{stderr}' ) check_flow_file(rundir) reinstall_log.info(f'REINSTALLED {named_run} from {source}') print( f'REINSTALL{"ED" if not dry_run else ""} {named_run} from {source}' ) close_log(reinstall_log) return stdout def install_workflow( source: Path, workflow_name: Optional[str] = None, run_name: Optional[str] = None, no_run_name: bool = False, cli_symlink_dirs: Optional[Dict[str, Dict[str, Any]]] = None ) -> Tuple[Path, Path, str, str]: """Install a workflow, or renew its installation. Install workflow into new run directory. Create symlink to workflow source location, creating any symlinks for run, work, log, share, share/cycle directories. Args: source: absolute path to workflow source directory. workflow_name: workflow name, default basename($PWD). run_name: name of the run, overrides run1, run2, run 3 etc... If specified, cylc install will not create runN symlink. rundir: for overriding the default cylc-run directory. no_run_name: Flag as True to install workflow into ~/cylc-run/ cli_symlink_dirs: Symlink dirs, if entered on the cli. Return: source: absolute path to source directory. rundir: absolute path to run directory, where the workflow has been installed into. workflow_name: installed workflow name (which may be computed here). named_run: Name of the run. Raise: WorkflowFilesError: No flow.cylc file found in source location. Illegal name (can look like a relative path, but not absolute). Another workflow already has this name. Trying to install a workflow that is nested inside of another. """ abort_if_flow_file_in_path(source) source = Path(expand_path(source)).resolve() if not workflow_name: workflow_name = get_source_workflow_name(source) if run_name is not None: if len(Path(run_name).parts) != 1: raise WorkflowFilesError( f'Run name cannot be a path. (You used {run_name})' ) validate_workflow_name( os.path.join(workflow_name, run_name), check_reserved_names=True ) else: validate_workflow_name(workflow_name, check_reserved_names=True) validate_source_dir(source, workflow_name) run_path_base = Path(get_workflow_run_dir(workflow_name)) relink, run_num, rundir = get_run_dir_info( run_path_base, run_name, no_run_name ) max_scan_depth = glbl_cfg().get(['install', 'max depth']) workflow_id = rundir.relative_to(get_cylc_run_dir()) if len(workflow_id.parts) > max_scan_depth: raise WorkflowFilesError( f"Cannot install: workflow ID '{workflow_id}' would exceed " f"global.cylc[install]max depth = {max_scan_depth}" ) check_nested_dirs(rundir, run_path_base) if rundir.exists(): raise WorkflowFilesError( f"'{rundir}' already exists\n" "To reinstall, use `cylc reinstall`" ) symlinks_created = {} named_run = workflow_name if run_name: named_run = os.path.join(named_run, run_name) elif run_num: named_run = os.path.join(named_run, f'run{run_num}') symlinks_created = make_localhost_symlinks( rundir, named_run, symlink_conf=cli_symlink_dirs) install_log = _get_logger(rundir, 'cylc-install') if symlinks_created: for target, symlink in symlinks_created.items(): install_log.info(f"Symlink created: {symlink} -> {target}") try: rundir.mkdir(exist_ok=True, parents=True) except FileExistsError: # This occurs when the file exists but is _not_ a directory. raise WorkflowFilesError( f"Cannot install as there is an existing file at {rundir}." ) from None if relink: link_runN(rundir) rsync_cmd = get_rsync_rund_cmd(source, rundir) proc = Popen(rsync_cmd, stdout=PIPE, stderr=PIPE, text=True) # nosec # * command is constructed via internal interface stdout, stderr = proc.communicate() install_log.info( f"Copying files from {source} to {rundir}" f"\n{stdout}" ) if proc.returncode != 0: install_log.warning( f"An error occurred when copying files from {source} to {rundir}") install_log.warning(f" Warning: {stderr}") cylc_install = Path(rundir.parent, WorkflowFiles.Install.DIRNAME) check_deprecation(check_flow_file(rundir)) if no_run_name: cylc_install = Path(rundir, WorkflowFiles.Install.DIRNAME) source_link = cylc_install.joinpath(WorkflowFiles.Install.SOURCE) # check source link matches the source symlink from workflow dir. cylc_install.mkdir(parents=True, exist_ok=True) if not source_link.exists(): if source_link.is_symlink(): # Condition represents a broken symlink. raise WorkflowFilesError( f'Symlink broken: {source_link} -> {source_link.resolve()}.' ) install_log.info(f"Creating symlink from {source_link}") source_link.symlink_to(source.resolve()) else: if source_link.resolve() != source.resolve(): raise WorkflowFilesError( f"Failed to install from {source.resolve()}: " f"previous installations were from {source_link.resolve()}" ) install_log.info( f'Symlink from "{source_link}" to "{source}" in place.') install_log.info(f'INSTALLED {named_run} from {source}') close_log(install_log) return source, rundir, workflow_name, named_run def get_run_dir_info( run_path_base: Path, run_name: Optional[str], no_run_name: bool ) -> Tuple[bool, Optional[int], Path]: """Get (numbered, named or unnamed) run directory info for current install. Args: run_path_base: The workflow directory absolute path. run_name: Name of the run. no_run_name: Flag as True to indicate no run name - workflow installed into ~/cylc-run/. Returns: relink: True if runN symlink needs updating. run_num: Run number of the current install, if using numbered runs. rundir: Run directory absolute path. """ relink = False run_num = None if no_run_name: rundir = run_path_base elif run_name: rundir = run_path_base.joinpath(run_name) if run_path_base.exists() and detect_flow_exists(run_path_base, True): raise WorkflowFilesError( f"--run-name option not allowed as '{run_path_base}' contains " "installed numbered runs." ) else: run_num = get_next_rundir_number(run_path_base) rundir = Path(run_path_base, f'run{run_num}') if run_path_base.exists() and detect_flow_exists(run_path_base, False): raise WorkflowFilesError( f"Path: \"{run_path_base}\" contains an installed" " workflow. Use --run-name to create a new run." ) unlink_runN(run_path_base) relink = True return relink, run_num, rundir def get_source_dirs() -> List[str]: return glbl_cfg().get(['install', 'source dirs']) def search_install_source_dirs(workflow_name: Union[Path, str]) -> Path: """Return the path of a workflow source dir if it is present in the 'global.cylc[install]source dirs' search path.""" abort_if_flow_file_in_path(Path(workflow_name)) search_path: List[str] = get_source_dirs() if not search_path: raise WorkflowFilesError( "Cannot find workflow as 'global.cylc[install]source dirs' " "does not contain any paths") for path in search_path: try: return check_flow_file(Path(path, workflow_name)).parent except WorkflowFilesError: continue raise WorkflowFilesError( f"Could not find workflow '{workflow_name}' in: " f"{', '.join(search_path)}") def get_source_workflow_name(source: Path) -> str: """Return workflow name relative to configured source dirs if possible, else the basename of the given path. Note the source path provided should be fully expanded (user and env vars) and normalised. """ for dir_ in get_source_dirs(): try: return str(source.relative_to(Path(expand_path(dir_)).resolve())) except ValueError: continue return source.name def unlink_runN(path: Union[Path, str]) -> bool: """Remove symlink runN if it exists. Args: path: Absolute path to workflow dir containing runN. """ try: Path(expand_path(path, WorkflowFiles.RUN_N)).unlink() except OSError: return False return True def link_runN(latest_run: Union[Path, str]): """Create symlink runN, pointing at the latest run""" latest_run = Path(latest_run) run_n = Path(latest_run.parent, WorkflowFiles.RUN_N) with suppress(OSError): run_n.symlink_to(latest_run.name) def validate_source_dir( source: Union[Path, str], workflow_name: str ) -> None: """Ensure the source directory is valid: - has flow file - does not contain reserved dir names Args: source: Path to source directory Raises: WorkflowFilesError: If log, share, work or _cylc-install directories exist in the source directory. """ # Source dir must not contain reserved run dir names (as file or dir). for dir_ in WorkflowFiles.RESERVED_DIRNAMES: if Path(source, dir_).exists(): raise WorkflowFilesError( f"{workflow_name} installation failed " f"- {dir_} exists in source directory." ) check_flow_file(source) def parse_cli_sym_dirs(symlink_dirs: str) -> Dict[str, Dict[str, Any]]: """Converts command line entered symlink dirs to a dictionary. Args: symlink_dirs: As entered by user on cli, e.g. "log=$DIR, share=$DIR2". Raises: WorkflowFilesError: If directory to be symlinked is not in permitted dirs: run, log, share, work, share/cycle Returns: dict: In the same form as would be returned by global config. e.g. {'localhost': {'log': '$DIR', 'share': '$DIR2' } } """ # Ensures the same nested dict format which is returned by the glb cfg symdict: Dict[str, Dict[str, Any]] = {'localhost': {'run': None}} if symlink_dirs == "": return symdict symlist = symlink_dirs.strip(',').split(',') possible_symlink_dirs = set(WorkflowFiles.SYMLINK_DIRS.union( {WorkflowFiles.RUN_DIR}) ) possible_symlink_dirs.remove('') for pair in symlist: try: key, val = pair.split("=") key = key.strip() except ValueError: raise InputError( 'There is an error in --symlink-dirs option:' f' {pair}. Try entering option in the form ' '--symlink-dirs=\'log=$DIR, share=$DIR2, ...\'' ) from None if key not in possible_symlink_dirs: dirs = ', '.join(possible_symlink_dirs) raise InputError( f"{key} not a valid entry for --symlink-dirs. " f"Configurable symlink dirs are: {dirs}" ) symdict['localhost'][key] = val.strip() or None return symdict def detect_flow_exists( run_path_base: Union[Path, str], numbered: bool ) -> bool: """Returns True if installed flow already exists. Args: run_path_base: Absolute path of the parent of the workflow's run dir, i.e ~/cylc-run/ numbered: If True, will detect if numbered runs exist. If False, will detect if non-numbered runs exist, i.e. runs installed by --run-name. """ for entry in Path(run_path_base).iterdir(): is_numbered = bool(re.search(r'^run\d+$', entry.name)) if ( entry.is_dir() and entry.name not in { WorkflowFiles.Install.DIRNAME, WorkflowFiles.RUN_N } and Path(entry, WorkflowFiles.FLOW_FILE).exists() and is_numbered == numbered ): return True return False def check_nested_dirs( run_dir: Path, install_dir: Optional[Path] = None ) -> None: """Disallow nested dirs: - Nested installed run dirs - Nested installed workflow dirs Args: run_dir: Absolute workflow run directory path. install_dir: Absolute workflow install directory path (contains _cylc-install). If None, will not check for nested install dirs. Raises: WorkflowFilesError if run_dir is nested inside an existing run dir, or install dirs are nested. """ if install_dir is not None: install_dir = Path(os.path.normpath(install_dir)) # Check parents: for parent_dir in run_dir.parents: # Stop searching at ~/cylc-run if parent_dir == Path(get_cylc_run_dir()): break # check for run directories: if is_valid_run_dir(parent_dir): raise WorkflowFilesError( NESTED_DIRS_MSG.format( dir_type='run', dest=run_dir, existing=get_cylc_run_abs_path(parent_dir) ) ) # Check for install directories: if ( install_dir and parent_dir in install_dir.parents and (parent_dir / WorkflowFiles.Install.DIRNAME).is_dir() ): raise WorkflowFilesError( NESTED_DIRS_MSG.format( dir_type='install', dest=run_dir, existing=get_cylc_run_abs_path(parent_dir) ) ) if install_dir: # Search child tree for install directories: for depth in range(glbl_cfg().get(['install', 'max depth'])): search_pattern = f'*/{"*/" * depth}{WorkflowFiles.Install.DIRNAME}' for result in install_dir.glob(search_pattern): raise WorkflowFilesError( NESTED_DIRS_MSG.format( dir_type='install', dest=run_dir, existing=get_cylc_run_abs_path(result.parent) ) ) cylc-flow-8.6.4/cylc/flow/command_validation.py0000664000175000017500000002612215202510242021713 0ustar alastairalastair# 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 . """Cylc command argument validation logic.""" from typing import ( TYPE_CHECKING, Dict, Iterable, List, Optional, Set, cast, ) from cylc.flow.cycling.loader import standardise_point_string from cylc.flow.exceptions import InputError, PointParsingError from cylc.flow.flow_mgr import ( FLOW_NEW, FLOW_NONE, ) from cylc.flow.id import ( IDTokens, Tokens, ) from cylc.flow.id_cli import contains_fnmatch from cylc.flow.scripts.set import XTRIGGER_PREREQ_PREFIX from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED if TYPE_CHECKING: from cylc.flow.id import TaskTokens ERR_OPT_FLOW_VAL_INT_NEW_NONE = ( # for set and trigger commands f"Flow values must be integers, or '{FLOW_NEW}', or '{FLOW_NONE}'" ) ERR_OPT_FLOW_VAL_INT = "Flow values must be integers" # for remove command ERR_OPT_FLOW_COMBINE = "Cannot combine --flow={0} with other flow values" ERR_OPT_FLOW_WAIT = ( f"--wait is not compatible with --flow={FLOW_NEW} or --flow={FLOW_NONE}" ) def flow_opts( flows: List[str], flow_wait: bool, allow_new_or_none: bool = True, ) -> None: """Check validity of flow-related CLI options. Note the schema defaults flows to []. Examples: Good: >>> flow_opts([], False) >>> flow_opts(["new"], False) >>> flow_opts(["1", "2"], False) >>> flow_opts(["1", "2"], True) Bad: >>> flow_opts(["none", "1"], False) Traceback (most recent call last): cylc.flow.exceptions.InputError: Cannot combine --flow=none with other flow values >>> flow_opts(["cheese", "2"], True) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... or 'new', or 'none' >>> flow_opts(["new"], True) Traceback (most recent call last): cylc.flow.exceptions.InputError: --wait is not compatible with --flow=new or --flow=none >>> flow_opts(["new"], False, allow_new_or_none=False) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... must be integers >>> flow_opts([''], False, allow_new_or_none=False) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... must be integers """ if not flows: return flows = [val.strip() for val in flows] for val in flows: val = val.strip() if val in {FLOW_NONE, FLOW_NEW}: if len(flows) != 1: raise InputError(ERR_OPT_FLOW_COMBINE.format(val)) if not allow_new_or_none and val in {FLOW_NEW, FLOW_NONE}: raise InputError(ERR_OPT_FLOW_VAL_INT) else: try: int(val) except ValueError: if allow_new_or_none: raise InputError(ERR_OPT_FLOW_VAL_INT_NEW_NONE) from None raise InputError(ERR_OPT_FLOW_VAL_INT) from None if flow_wait and flows[0] in {FLOW_NEW, FLOW_NONE}: raise InputError(ERR_OPT_FLOW_WAIT) def prereqs(prereqs: Optional[List[str]]): """Validate prerequisites, add implicit ":succeeded". Comma-separated lists should be split already, client-side. Examples: # Set multiple at once, prereq and xtriggers: >>> prereqs(['1/foo:bar', '2/foo:baz', 'xtrigger/x1']) ['1/foo:bar', '2/foo:baz', 'xtrigger/x1:succeeded'] # --pre=all >>> prereqs(["all"]) ['all'] # implicit ":succeeded" >>> prereqs(["1/foo"]) ['1/foo:succeeded'] # implicit ":satisifed" >>> prereqs(["xtrigger/foo"]) ['xtrigger/foo:succeeded'] # Error: invalid format: >>> prereqs(["fish", "dog"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... * fish * dog # Error: invalid format: >>> prereqs(["1/foo::bar"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... * 1/foo::bar # Error: invalid format: >>> prereqs(["xtrigger/x1::bar"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... * xtrigger/x1::bar # Error: "all" must be used alone: >>> prereqs(["all", "2/foo:baz"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: --pre=all must be used alone """ if prereqs is None: return [] prereqs2 = [] bad: List[str] = [] for pre in prereqs: p = prereq(pre) if p is not None: prereqs2.append(p) else: bad.append(pre) if bad: raise InputError( "Bad prerequisite format, see command help:\n * " + "\n * ".join(bad) ) if len(prereqs2) > 1: # noqa SIM102 (anticipates "cylc set --pre=cycle") if "all" in prereqs: raise InputError("--pre=all must be used alone") return prereqs2 def prereq(prereq: str) -> Optional[str]: """Return standardised task and xtrigger prerequisites if valid, else None. Default to suffix ":succeeded" (task and xtrigger prerequisites). (Standardisation of "start" -> "started" etc. is done later). Format: cycle/task[:output] (xtriggers: cycle is "xtrigger", task is xtrigger label) Examples: >>> prereq('1/foo:succeeded') '1/foo:succeeded' >>> prereq('1/foo:succeed') '1/foo:succeed' >>> prereq('1/foo') '1/foo:succeeded' >>> prereq('1/foo:other_output') '1/foo:other_output' >>> prereq('all') 'all' >>> prereq('xtrigger/wall_clock') 'xtrigger/wall_clock:succeeded' >>> prereq('xtrigger/wall_clock:succeeded') 'xtrigger/wall_clock:succeeded' >>> prereq('xtrigger/all') 'xtrigger/all:succeeded' >>> prereq('xtrigger/all:succeeded') 'xtrigger/all:succeeded' # Error, xtrigger state must be succeeded or waiting: >>> prereq('xtrigger/wall_clock:other') # Error, just a task name: >>> prereq('fish') """ try: tokens = Tokens(prereq, relative=True) except ValueError: return None if tokens["cycle"] == prereq and prereq != "all": # Error: --pre= other than "all" return None if tokens["cycle"] == XTRIGGER_PREREQ_PREFIX: if tokens["task_sel"] not in {None, TASK_OUTPUT_SUCCEEDED}: # Error: xtrigger status must be default or succeeded. return None if tokens["task_sel"] is None: # Default to succeeded prereq += f":{TASK_OUTPUT_SUCCEEDED}" else: if prereq != "all" and tokens["task_sel"] is None: # Default to succeeded prereq += f":{TASK_OUTPUT_SUCCEEDED}" return prereq def outputs(outputs: Optional[List[str]]): """Validate outputs. Comma-separated lists should be split already, client-side. Examples: Good: >>> outputs(['a', 'b']) ['a', 'b'] >>> outputs(["required"]) # "required" is explicit default [] Bad: >>> outputs(["required", "a"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: --out=required must be used alone >>> outputs(["waiting"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: Tasks cannot be set to waiting... """ # If "required" is explicit just ditch it (same as the default) if not outputs or outputs == ["required"]: return [] if "required" in outputs: raise InputError("--out=required must be used alone") if "waiting" in outputs: raise InputError( "Tasks cannot be set to waiting. Use trigger to re-run tasks." ) return outputs def consistency( outputs: Optional[List[str]], prereqs: Optional[List[str]], ) -> None: """Check global option consistency Examples: >>> consistency(["a"], None) # OK >>> consistency(None, ["1/a:failed"]) #OK >>> consistency(["a"], ["1/a:failed"]) Traceback (most recent call last): cylc.flow.exceptions.InputError: ... """ if outputs and prereqs: raise InputError("Use --prerequisite or --output, not both.") def is_tasks(ids: Iterable[str]) -> 'Set[TaskTokens]': """Ensure all IDs are task IDs and standardise them. * Parses IDs. * Filters out job ids and ensures at least the cycle point is provided. * Standardises the cycle point format. * Defaults the namespace to "root" unless provided. Args: ids: The strings to parse. Returns: The parsed IDs as TaskTokens objects. Raises: InputError: If any of the IDs cannot be parsed or formatted. """ if not ids: raise InputError("No tasks specified") ret: 'Set[TaskTokens]' = set() errors: Dict[str, List[str]] = {} for id_ in ids: # parse id try: tokens = Tokens(id_, relative=True) except ValueError: errors.setdefault('Invalid ID', []).append(id_) continue # filter out job IDs if tokens.lowest_token == IDTokens.Job.value: errors.setdefault('This command does not take job IDs', []).append( id_ ) continue # if the task is not specified, default to "root" if tokens['task'] is None: tokens = tokens.duplicate(task='root') # if the cycle is not a glob or reference, standardise it if ( # cycle point is a glob not contains_fnmatch(cast('str', tokens['cycle'])) # cycle point is a reference to the ICP/FCP and tokens['cycle'] not in {'^', '$'} ): try: cycle = standardise_point_string(tokens['cycle']) except PointParsingError: errors.setdefault('Invalid cycle point', []).append(id_) continue else: if cycle != tokens['cycle']: tokens = tokens.duplicate(cycle=cycle) # we have confirmed that both cycle and task have been provided ret.add(cast('TaskTokens', tokens)) if errors: raise InputError( '\n'.join( f'{message}: {", ".join(sorted(_ids))}' for message, _ids in sorted(errors.items()) ) ) return ret cylc-flow-8.6.4/cylc/flow/id.py0000664000175000017500000006363215202510242016466 0ustar alastairalastair# 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 . """Cylc univeral identifier system for referencing Cylc "objects". This module contains the abstract ID tokenising/detokenising code. """ from enum import Enum import re from typing import ( TYPE_CHECKING, Any, Iterable, List, Literal, Optional, Set, Tuple, Union, cast, overload, ) from cylc.flow import LOG if TYPE_CHECKING: from cylc.flow.cycling import PointBase class IDTokens(Enum): """Cylc object identifier tokens.""" User = 'user' Workflow = 'workflow' Cycle = 'cycle' Task = 'task' Job = 'job' class Tokens(dict): """A parsed representation of a Cylc universal identifier (UID). Examples: Parse tokens: >>> tokens = Tokens('~u/w//c/t/01') >>> tokens Parse back to a string ID: >>> tokens.id '~u/w//c/t/01' >>> tokens.workflow_id '~u/w' >>> tokens.relative_id 'c/t/01' Inspect the tokens: >>> tokens['user'] 'u' >>> tokens['task'] 't' >>> tokens['task_sel'] # task selector >>> list(tokens.values()) # Note the None values are selectors ['u', 'w', None, 'c', None, 't', None, '01', None] Construct tokens: >>> Tokens(workflow='w', cycle='c') >>> Tokens(workflow='w', cycle='c')['job'] # Make a copy (note Tokens are immutable): >>> tokens.duplicate() >>> tokens.duplicate(job='02') # make changes at the same time """ _REGULAR_KEYS: Set[str] = {token.value for token in IDTokens} _SELECTOR_KEYS = { f'{token.value}_sel' for token in IDTokens if token != IDTokens.User } # all valid dictionary keys _KEYS = _REGULAR_KEYS | _SELECTOR_KEYS _TASK_LIKE_KEYS = { key for key in _KEYS if not ( key.startswith(IDTokens.User.value) or key.startswith(IDTokens.Workflow.value) ) } def __init__( self, *args: 'Union[str, Tokens]', relative: bool = False, **kwargs: Optional[str] ): if args: if len(args) > 1: raise ValueError() if isinstance(args[0], str): kwargs = tokenise(args[0], relative) else: kwargs = dict(args[0]) else: for key in kwargs: if key not in self._KEYS: raise ValueError(f'Invalid token: {key}') dict.__init__(self, **kwargs) def __setitem__(self, key, value): raise Exception('Tokens objects are not mutable') def update(self, other): raise Exception('Tokens objects are not mutable') def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: if key not in self._KEYS: raise ValueError(f'Invalid token: {key}') from None return None def __str__(self): return self.id def __repr__(self): """Python internal representation. Examples: >>> Tokens('a//1') >>> Tokens('//1', relative=True) >>> Tokens() """ if self.is_null: id_ = '' else: id_ = self.id return f'' def __hash__(self): # generate the __hash__ from the non None items in the dictionary return hash(tuple(sorted(((k, v) for k, v in self.items() if v)))) def __eq__(self, other): if not isinstance(other, Tokens): return False return all( self[key] == other[key] for key in self._KEYS ) def __lt__(self, other): return self.id < other.id def __gt__(self, other): return self.id > other.id def __ne__(self, other): if not isinstance(other, Tokens): return True return any( self[key] != other[key] for key in self._KEYS ) @property # noqa A003 (not shadowing id built-in) def id(self) -> str: # noqa A003 (not shadowing id built-in) """The full ID these tokens represent. Examples: >>> Tokens('~u/w//c/t/01').id '~u/w//c/t/01' >>> Tokens().id Traceback (most recent call last): ValueError: No tokens provided """ return detokenise(self) @property def relative_id(self) -> str: """The relative ID (without the workflow part). Examples: >>> Tokens('~u/w//c/t/01').relative_id 'c/t/01' >>> Tokens('~u/w').relative_id Traceback (most recent call last): ValueError: No tokens provided """ return detokenise(self.task, relative=True) @property def relative_id_with_selectors(self) -> str: """The relative ID (without the workflow part), with selectors. Examples: >>> Tokens('~u/w//c/t:failed/01').relative_id 'c/t/01' >>> Tokens('~u/w//c/t:failed/01').relative_id_with_selectors 'c/t:failed/01' """ return detokenise(self.task, relative=True, selectors=True) @property def workflow_id(self) -> str: """The workflow id (without the relative part). Examples: >>> Tokens('~u/w//c/t/01').workflow_id '~u/w' >>> Tokens('c/t/01', relative=True).workflow_id Traceback (most recent call last): ValueError: No tokens provided """ return detokenise(self.workflow) @property def lowest_token(self) -> str: """Return the lowest token present in a tokens dictionary. Examples: >>> Tokens('~u/w//c/t/01').lowest_token 'job' >>> Tokens('~u/w//c/t').lowest_token 'task' >>> Tokens('~u/w//c').lowest_token 'cycle' >>> Tokens('~u/w//').lowest_token 'workflow' >>> Tokens('~u').lowest_token 'user' >>> Tokens().lowest_token({}) Traceback (most recent call last): ValueError: No tokens defined """ for token in reversed(IDTokens): if token.value in self and self[token.value]: return token.value raise ValueError('No tokens defined') def pop_token(self) -> Tuple[str, str]: """Pop the lowest token. Examples: >>> tokens = Tokens('~u/w//c/t/01') >>> tokens.pop_token() ('job', '01') >>> tokens.pop_token() ('task', 't') >>> tokens.pop_token() ('cycle', 'c') >>> tokens.pop_token() ('workflow', 'w') >>> tokens.pop_token() ('user', 'u') >>> tokens >>> tokens.pop_token() Traceback (most recent call last): KeyError: 'No defined tokens.' """ for token in reversed(IDTokens): token_name = token.value value = self[token_name] if value: self.pop(token_name) return (token_name, value) raise KeyError('No defined tokens.') @property def is_task_like(self) -> bool: """Returns True if any task-like objects are present in the ID. Task like == cycles or tasks or jobs. Examples: >>> Tokens('workflow//').is_task_like False >>> Tokens('workflow//1').is_task_like True """ return any( self[key] for key in self._TASK_LIKE_KEYS ) @property def task(self) -> 'Tokens': """The task portion of the tokens. Examples: >>> Tokens('~user/workflow//cycle/task/01').task """ return Tokens( **{ key: value for key, value in self.items() if key in self._TASK_LIKE_KEYS } ) @property def workflow(self) -> 'Tokens': """The workflow portion of the tokens. Examples: >>> Tokens('~user/workflow//cycle/task/01').workflow """ return Tokens( **{ key: value for key, value in self.items() if key not in self._TASK_LIKE_KEYS } ) @property def is_null(self) -> bool: """Returns True if no tokens are set. Examples: >>> tokens = Tokens() >>> tokens.is_null True >>> tokens.duplicate(job_sel='x').is_null True >>> tokens.duplicate(job='01').is_null False """ return not any( self[key] for key in self._REGULAR_KEYS ) @property def submit_num(self) -> int | None: """The job submit number as an integer, or None if not set. Examples: >>> Tokens('//c/t/01').submit_num 1 >>> Tokens('//c/t').submit_num is None True """ return int(self['job']) if self['job'] else None @overload def duplicate( self, *, cycle: str, task: str, **kwargs, ) -> 'TaskTokens': ... @overload def duplicate( self, *tokens_list: 'Tokens', **kwargs, ) -> 'Tokens': ... def duplicate( self, *tokens_list: 'Tokens', **kwargs, ) -> 'Tokens': """Duplicate a tokens object. Can be used to change the values of the new object at the same time. Examples: Duplicate tokens: >>> tokens1 = Tokens('~u/w') >>> tokens2 = tokens1.duplicate() The copy is equal but a different object: >>> tokens1 == tokens2 True >>> id(tokens1) == id(tokens2) False Make a copy with a modification: >>> tokens1.duplicate(cycle='1').id '~u/w//1' The Original is not changed: >>> tokens1.id '~u/w' Arguments override in definition order: >>> Tokens.duplicate( ... tokens1, ... Tokens(cycle='c', task='a', job='01'), ... task='b' ... ).id '~u/w//c/b/01' """ _kwargs: dict[str, Any] = {} for tokens in (self, *tokens_list): _kwargs.update(tokens) _kwargs.update(kwargs) return Tokens(**_kwargs) class TaskTokens(Tokens): """A Tokens object where the cycle and task are compulsory.""" def __init__(self, cycle: str, task: str, **kwargs): Tokens.__init__(self, cycle=cycle, task=task, **kwargs) @overload def __getitem__(self, key: "Literal['cycle']") -> str: ... @overload def __getitem__(self, key: "Literal['task']") -> str: ... def __getitem__(self, key: str) -> Optional[str]: return Tokens.__getitem__(self, key) def duplicate(self, *tokens_list, **kwargs) -> 'TaskTokens': return cast( 'TaskTokens', Tokens.duplicate(self, *tokens_list, **kwargs) ) @property def task(self) -> 'TaskTokens': return cast('TaskTokens', Tokens.task.fget(self)) # type: ignore @property def is_task_like(self) -> 'Literal[True]': return True # //cycle[:sel][/task[:sel][/job[:sel]]] RELATIVE_PATTERN = rf''' // (?P<{IDTokens.Cycle.value}>[^~\/:\n][^~\/\n]*?) (?: : (?P<{IDTokens.Cycle.value}_sel>[^\/:\n]+) )? (?: / (?: (?P<{IDTokens.Task.value}>[^\/:\n]+) (?: : (?P<{IDTokens.Task.value}_sel>[^\/:\n]+) )? (?: / (?: (?P<{IDTokens.Job.value}>[^\/:\n]+) (?: : (?P<{IDTokens.Job.value}_sel>[^\/:\n]+) )? )? )? )? )? ''' RELATIVE_ID = re.compile( r'^' + RELATIVE_PATTERN + r'$', re.X ) # ~user[/workflow[:sel][//cycle[:sel][/task[:sel][/job[:sel]]]]] UNIVERSAL_ID = re.compile( rf''' # don't match an empty string (?=.) # either match a user or the start of the line (?: (?: ~ (?P<{IDTokens.User.value}>[^\/:\n~]+) # allow the match to end here (\/|$) ) |^ ) (?: (?P<{IDTokens.Workflow.value}> # can't begin with // (?!//) # workflow ID (flat) [^:~\n\/]+ # workflow ID (hierarchical) (?: (?: \/ [^:~\n\/]+ )+ )? ) (?: : (?P<{IDTokens.Workflow.value}_sel>[^\/:\n]+) )? (?: (?: # can't end /// //(?!/) )? (?: # cycle/task/job {RELATIVE_PATTERN} )? )? )? $ ''', re.X ) # task.cycle[:sel] LEGACY_TASK_DOT_CYCLE = re.compile( rf''' ^ # NOTE: task names can contain "." (?P<{IDTokens.Task.value}>[^~\:\/\n]+) \. # NOTE: legacy cycles always start with a number (?P<{IDTokens.Cycle.value}>\d[^~\.\:\/\n]*) # NOTE: the task selector applied to the cycle in this legacy format # (not a mistake) (?: : (?P<{IDTokens.Task.value}_sel>[^\:\/\n]+) )? $ ''', re.X ) # cycle/task[:sel] LEGACY_CYCLE_SLASH_TASK = re.compile( rf''' ^ # NOTE: legacy cycles always start with a number (?P<{IDTokens.Cycle.value}>\d[^~\.\:\/\n]+) \/ # NOTE: task names can contain "." (?P<{IDTokens.Task.value}>[^~\:\/\n]+) (?: : (?P<{IDTokens.Task.value}_sel>[^\:\/\n]+) )? $ ''', re.X ) def quick_relative_id( cycle: Union[str, int, 'PointBase'], task: str, task_sel: Optional[str] = None ) -> str: """Generate a relative ID for a task, with optional selector. This is a more efficient solution to `Tokens` for cases where you only want the ID string and don't have any use for a Tokens object. Example: >>> q = quick_relative_id >>> q('1', 'a') == Tokens(cycle='1', task='a').relative_id True >>> q('1', 'a', 'succeeded') '1/a:succeeded' >>> q('1', 'a', 'succeeded') == ( ... detokenise( ... Tokens(cycle='1', task='a', task_sel="succeeded"), ... selectors=True, ... relative=True ... ) ... ) True """ if task_sel is None: return f'{cycle}/{task}' else: return f'{cycle}/{task}:{task_sel}' def _dict_strip(dictionary): """Run str.strip against dictionary values. Examples: >>> _dict_strip({'a': ' x ', 'b': 'x', 'c': None}) {'a': 'x', 'b': 'x', 'c': None} """ return { key: value.strip() if value else None for key, value in dictionary.items() } def legacy_tokenise(identifier: str) -> Tokens: """Convert a legacy string identifier into Cylc tokens. Supports the two legacy Cylc7 formats: * task.cycle[:task_status] * cycle/task[:task_status] Args: identifier (str): The namespace to tokenise. Returns: dict - {token: value} Warning: The tokenise() function will parse a legacy token as a Workflow. Raises: ValueError: For invalid identifiers. Examples: # task.cycle[:task_status] >>> legacy_tokenise('task.123') {'task': 'task', 'cycle': '123', 'task_sel': None} >>> legacy_tokenise('task.123:task_sel') {'task': 'task', 'cycle': '123', 'task_sel': 'task_sel'} # cylc/task[:task_status] >>> legacy_tokenise('123/task') {'cycle': '123', 'task': 'task', 'task_sel': None} >>> legacy_tokenise('123/task:task_sel') {'cycle': '123', 'task': 'task', 'task_sel': 'task_sel'} """ for pattern in ( LEGACY_TASK_DOT_CYCLE, LEGACY_CYCLE_SLASH_TASK ): match = pattern.match(identifier) if match: return _dict_strip(match.groupdict()) raise ValueError(f'Invalid legacy Cylc identifier: {identifier}') def tokenise( identifier: str, relative: bool = False, ) -> Tokens: """Convert a string identifier into Cylc tokens. Args: identifier (str): The namespace to tokenise. relative (bool): If True the prefix // is implicit if omitted. Returns: dict - {token: value} Warning: Will parse a legacy (task and or cycle) token as a Workflow. Raises: ValueError: For invalid identifiers. Examples: # absolute identifiers >>> tokenise( ... '~user/workflow:workflow_sel//' ... 'cycle:cycle_sel/task:task_sel/01:job_sel' ... ) # doctest: +NORMALIZE_WHITESPACE >>> def _(tokens): ... return { ... token: value for token, value in tokens.items() if value} # "full" identifiers >>> _(tokenise('workflow//cycle')) {'workflow': 'workflow', 'cycle': 'cycle'} # "partial" identifiers: >>> _(tokenise('~user')) {'user': 'user'} >>> _(tokenise('~user/workflow')) {'user': 'user', 'workflow': 'workflow'} >>> _(tokenise('workflow')) {'workflow': 'workflow'} # "relative" identifiers (new syntax): >>> _(tokenise('//cycle')) {'cycle': 'cycle'} >>> _(tokenise('cycle', relative=True)) {'cycle': 'cycle'} >>> _(tokenise('//cycle/task/job')) {'cycle': 'cycle', 'task': 'task', 'job': 'job'} # whitespace stripping is employed on all values: >>> _(tokenise(' workflow // cycle ')) {'workflow': 'workflow', 'cycle': 'cycle'} # illegal identifiers: >>> tokenise('a///') Traceback (most recent call last): ValueError: Invalid Cylc identifier: a/// """ patterns = [UNIVERSAL_ID, RELATIVE_ID] if relative and not identifier.startswith('//'): identifier = f'//{identifier}' for pattern in patterns: match = pattern.match(identifier) if match: return Tokens(**_dict_strip(match.groupdict())) raise ValueError(f'Invalid Cylc identifier: {identifier}') def detokenise( tokens: Tokens, selectors: bool = False, relative: bool = False, ) -> str: """Convert Cylc tokens into a string identifier. Args: tokens (dict): IDTokens as returned by tokenise. selectors (bool): If true selectors (i.e. :sel) will be included in the output. relative (bool): If true relative references are not given the `//` prefix. Returns: str - Identifier i.e. ~user/workflow//cycle/task/job Raises: ValueError: For invalid or empty tokens. Examples: # absolute references: >>> detokenise(tokenise('~user')) '~user' >>> detokenise(tokenise('~user/workflow')) '~user/workflow' >>> detokenise(tokenise('~user/workflow//cycle')) '~user/workflow//cycle' >>> detokenise(tokenise('~user/workflow//cycle/task')) '~user/workflow//cycle/task' >>> detokenise(tokenise('~user/workflow//cycle/task/4')) '~user/workflow//cycle/task/04' # relative references: >>> detokenise(tokenise('//cycle/task/4')) '//cycle/task/04' >>> detokenise(tokenise('//cycle/task/4'), relative=True) 'cycle/task/04' # selectors are enabled using the selectors kwarg: >>> detokenise(tokenise('workflow:a//cycle:b/task:c/01:d')) 'workflow//cycle/task/01' >>> detokenise(tokenise('workflow:a//cycle:b/task:c/01:d'), True) 'workflow:a//cycle:b/task:c/01:d' # missing tokens expand to '*' (absolute): >>> tokens = tokenise('~user/workflow//cycle/task/01') >>> tokens.pop('task') 'task' >>> detokenise(tokens) '~user/workflow//cycle/*/01' # missing tokens expand to '*' (relative): >>> tokens = tokenise('//cycle/task/01') >>> tokens.pop('task') 'task' >>> detokenise(tokens) '//cycle/*/01' # empty tokens result in traceback: >>> detokenise({}) Traceback (most recent call last): ValueError: No tokens provided """ keys = { key for key in Tokens._REGULAR_KEYS if tokens.get(key) } is_relative = keys.isdisjoint(('user', 'workflow')) is_partial = keys.isdisjoint(('cycle', 'task', 'job')) if is_relative and is_partial: raise ValueError('No tokens provided') # determine the lowest token for lowest_token in reversed(IDTokens): if lowest_token.value in keys: break highest_token: Optional[IDTokens] identifier = [] if is_relative: highest_token = IDTokens.Cycle if not relative: identifier = ['/'] else: highest_token = IDTokens.User for token in IDTokens: if highest_token: if token != highest_token: continue highest_token = None value: Optional[str] = tokens.get(token.value) if not value and token == IDTokens.User: continue elif token == IDTokens.User: value = f'~{value}' elif token == IDTokens.Job and value != 'NN': value = f'{int(value):02}' # type: ignore[arg-type] value = value or '*' if selectors and tokens.get(token.value + '_sel'): # include selectors value = f'{value}:{tokens[token.value + "_sel"]}' if token == IDTokens.Workflow and not is_partial: value += '/' identifier.append(value) if token == lowest_token: break return '/'.join(identifier) def upgrade_legacy_ids(*ids: str, relative=False) -> List[str]: """Reformat IDs from legacy to contemporary format: If no upgrading is required it returns the identifiers unchanged. Args: *ids: Identifier list. relative: If `False` then `ids` must describe absolute ID(s) e.g: workflow task1.cycle1 task2.cycle2 If `True` then `ids` should be relative e.g: task1.cycle1 task2.cycle2 Returns: tuple/list - Identifier list. # do nothing to contemporary ids: >>> upgrade_legacy_ids('workflow') ['workflow'] >>> upgrade_legacy_ids('workflow', '//cycle') ['workflow', '//cycle'] # upgrade legacy task.cycle ids: >>> upgrade_legacy_ids('workflow', 'task.123', 'task.234') ['workflow', '//123/task', '//234/task'] # upgrade legacy cycle/task ids: >>> upgrade_legacy_ids('workflow', '123/task', '234/task') ['workflow', '//123/task', '//234/task'] # upgrade mixed legacy ids: >>> upgrade_legacy_ids('workflow', 'task.123', '234/task') ['workflow', '//123/task', '//234/task'] # upgrade legacy task states: >>> upgrade_legacy_ids('workflow', 'task.123:abc', '234/task:def') ['workflow', '//123/task:abc', '//234/task:def'] # upgrade relative IDs: >>> upgrade_legacy_ids('x.1', relative=True) ['1/x'] >>> upgrade_legacy_ids('x.1', 'x.2', 'x.3:s', relative=True) ['1/x', '2/x', '3/x:s'] """ if not relative and len(ids) < 2: # only legacy relative references require upgrade => abort return list(ids) legacy_ids: List[str] _ids: Iterable[str] if relative: legacy_ids = [] _ids = ids else: legacy_ids = [ids[0]] _ids = ids[1:] for id_ in _ids: try: tokens = legacy_tokenise(id_) except ValueError: # not a valid legacy token => abort return list(ids) else: # upgrade this token legacy_ids.append( detokenise(tokens, selectors=True, relative=relative) ) LOG.warning( f'Cylc7 format is deprecated, using: {" ".join(legacy_ids)}' ' (see "cylc help id")' ) return legacy_ids def contains_multiple_workflows(tokens_list: List[Tokens]) -> bool: """Returns True if multiple workflows are contained in the tokens list. Examples: >>> a_1 = tokenise('a//1') >>> a_2 = tokenise('a//2') >>> b_1 = tokenise('b//1') >>> contains_multiple_workflows([a_1]) False >>> contains_multiple_workflows([a_1, a_2]) False >>> contains_multiple_workflows([a_1, b_1]) True """ return len({ (tokens['user'], tokens['workflow']) for tokens in tokens_list }) > 1 cylc-flow-8.6.4/cylc/flow/profiler.py0000664000175000017500000000443515202510242017710 0ustar alastairalastair# 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 . """Cylc memory and performance profiling.""" import os import cProfile import io from pathlib import Path import pstats import psutil class Profiler: """Wrap cProfile, pstats, and memory logging, for performance profiling.""" def __init__(self, schd, enabled=False): """Initialize cProfile.""" self.schd = schd self.enabled = enabled if enabled: self.prof = cProfile.Profile() else: self.prof = None def start(self): """Start profiling.""" if not self.enabled: return self.prof.enable() def stop(self): """Stop profiling and print stats.""" if not self.enabled: return self.prof.disable() string_stream = io.StringIO() stats = pstats.Stats(self.prof, stream=string_stream) stats.sort_stats('cumulative') stats.print_stats() # dump to stdout print(string_stream.getvalue()) # write data file to workflow log dir if not self.schd: # if no scheduler present (e.g. validate) dump to PWD loc = Path() else: loc = Path(self.schd.workflow_log_dir) self.prof.dump_stats( Path(loc, 'profile.prof') ) def log_memory(self, message): """Print a message to standard out with the current memory usage.""" if not self.enabled: return memory = psutil.Process(os.getpid()).memory_info().rss / 1024 print("PROFILE: Memory: %d KiB: %s" % (memory, message)) cylc-flow-8.6.4/cylc/flow/pipe_poller.py0000664000175000017500000000544515202510242020402 0ustar alastairalastair# 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 . """Utility for preventing pipes from getting clogged up. If you're reading files from Popen (i.e. to extract command output) where the command output has the potential to be long-ish, then you should use this function to protect against the buffer filling up. Note, there is a more advanced version of this baked into the subprocpool. """ import selectors from typing import ( IO, TYPE_CHECKING, TypeVar, cast, ) if TYPE_CHECKING: from subprocess import Popen T = TypeVar('T', str, bytes) def pipe_poller( proc: 'Popen[T]', *files: IO[T], chunk_size=4096 ) -> tuple[T, ...]: """Read from a process without hitting buffer issues. Standin for subprocess.Popen.communicate. When PIPE'ing from subprocesses, the output goes into a buffer. If the buffer gets full, the subprocess will hang trying to write to it. This function polls the process, reading output from the buffers into memory to prevent them from filling up. Args: proc: The process to poll. files: The files you want to read from, likely anything you've directed to PIPE. chunk_size: The amount of text to read from the buffer on each pass. Returns: tuple - The text read from each of the files in the order they were specified. """ selector = selectors.DefaultSelector() file_to_output: dict[IO[T], T] = {} for file in files: file_to_output[file] = file.read(0) # empty str | bytes selector.register(file, selectors.EVENT_READ) def _read(timeout=1.0): # read any data from files for key, _events in selector.select(timeout): file = cast('IO[T]', key.fileobj) buffer = file.read(chunk_size) if len(buffer) > 0: file_to_output[file] += buffer while proc.poll() is None: # read from the buffers _read() # double check the buffers now that the process has finished _read(timeout=0.01) selector.close() return tuple(file_to_output.values()) cylc-flow-8.6.4/cylc/flow/option_parsers.py0000664000175000017500000007254315202510242021142 0ustar alastairalastair# 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 . """Common options for all cylc commands.""" from contextlib import suppress import logging from itertools import product from optparse import ( OptionParser, Values, Option, IndentedHelpFormatter, ) import os import re import sys from textwrap import dedent from typing import Any, Dict, Iterable, Optional, List, Set, Tuple from ansimarkup import ( parse as cparse, strip as cstrip ) from cylc.flow import LOG from cylc.flow.terminal import should_use_color, DIM import cylc.flow.flags from cylc.flow.loggingutil import ( CylcLogFormatter, setup_segregated_log_streams, ) from cylc.flow.log_level import ( env_to_verbosity, verbosity_to_log_level ) WORKFLOW_ID_ARG_DOC = ('WORKFLOW', 'Workflow ID') OPT_WORKFLOW_ID_ARG_DOC = ('[WORKFLOW]', 'Workflow ID') WORKFLOW_ID_MULTI_ARG_DOC = ('WORKFLOW ...', 'Workflow ID(s)') WORKFLOW_ID_OR_PATH_ARG_DOC = ('WORKFLOW | PATH', 'Workflow ID or path') ID_SEL_ARG_DOC = ('ID[:sel]', 'WORKFLOW-ID[[//CYCLE[/TASK]]:selector]') ID_MULTI_ARG_DOC = ('ID ...', 'Workflow/Cycle/Family/Task ID(s)') FULL_ID_MULTI_ARG_DOC = ('ID ...', 'Cycle/Family/Task ID(s)') SHORTLINK_TO_ICP_DOCS = "https://bit.ly/3MYHqVh" DOUBLEDASH = '--' class OptionSettings(): """Container for info about a command line option Despite some similarities this is not to be confused with optparse.Option: This a container for information which may or may not be passed to optparse depending on the results of cylc.flow.option_parsers(thismodule).combine_options_pair. """ def __init__( self, argslist: List[str], sources: Optional[Set[str]] = None, useif: str = '', **kwargs ): """Init function: Args: arglist: list of arguments for optparse.Option. sources: set of CLI scripts which use this option. useif: badge for use by Cylc optionparser. **kwargs: kwargs for optparse.option. """ self.args: List[str] = argslist self.kwargs: Dict[str, Any] = kwargs self.sources: Set[str] = sources if sources is not None else set() self.useif: str = useif def __eq__(self, other): """Args and Kwargs, but not other props equal. (Also make an exception for kwargs['help'] to allow lists of sources prepended to 'help' to be passed through.) """ return ( ( {k: v for k, v in self.kwargs.items() if k != 'help'} == {k: v for k, v in other.kwargs.items() if k != 'help'} ) and self.args == other.args ) def __and__(self, other): """Is there a set intersection between arguments.""" return list(set(self.args).intersection(set(other.args))) def __sub__(self, other): """Set difference on args.""" return list(set(self.args) - set(other.args)) def _in_list(self, others): """CLI arguments for this option found in any of a list of other options.""" return any(self & other for other in others) def _update_sources(self, other): """Update the sources from this and 1 other OptionSettings object""" self.sources = {*self.sources, *other.sources} def __repr__(self) -> str: return f"<{type(self).__name__} {self.args}>" ICP_OPTION = OptionSettings( ["--initial-cycle-point", "--icp"], help=( "Set the initial cycle point. " "Required if not defined in flow.cylc.\n" "Can be an absolute point or an offset relative to the " "current time - see " f"{SHORTLINK_TO_ICP_DOCS} (Cylc documentation link)." ), metavar="CYCLE_POINT or OFFSET", action='store', dest="icp" ) AGAINST_SOURCE_OPTION = OptionSettings( ['--against-source'], help=( "Load the workflow configuration from the source directory it was" " installed from using any options (e.g. template variables) which" " have been set in the installation." " This is useful if you want to see how changes made to the workflow" " source would affect the installation if reinstalled." " Note if this option is used the provided workflow must have been" " installed by `cylc install`." ), dest='against_source', action='store_true', default=False ) icp_option = Option( *ICP_OPTION.args, **ICP_OPTION.kwargs) # type: ignore[arg-type] def format_shell_examples(string): """Put comments in the terminal "diminished" colour.""" return cparse( re.sub( r'^(\s*(?:\$[^#]+)?)(#.*)$', rf'\1<{DIM}>\2', string, flags=re.M, ) ) def format_help_headings(string): """Put "headings" in bold. Where "headings" are lines with no indentation which are followed by a colon. """ return cparse( re.sub( r'^(\w.*:)$', r'\1', string, flags=re.M, ) ) class CylcOption(Option): """Optparse option which adds a decrement action.""" ACTIONS = Option.ACTIONS + ('decrement',) STORE_ACTIONS = Option.STORE_ACTIONS + ('decrement',) def take_action(self, action, dest, opt, value, values, parser): if action == 'decrement': setattr(values, dest, values.ensure_value(dest, 0) - 1) else: Option.take_action(self, action, dest, opt, value, values, parser) class CylcHelpFormatter(IndentedHelpFormatter): """This formatter handles colour in help text, and automatically colourises headings & shell examples.""" def _format(self, text: str) -> str: """Format help (usage) text on the fly to handle coloring. Help is printed to the terminal before color initialization for general command output. If coloring is wanted: - Add color tags to shell examples Else: - Strip any hardwired color tags """ if should_use_color(self.parser.values): # Add color formatting to examples text. return format_shell_examples( format_help_headings(text) ) # Else strip any hardwired formatting return cstrip(text) def format_usage(self, usage: str) -> str: return super().format_usage(self._format(usage)) # If we start using "description" as well as "usage" (also epilog): # def format_description(self, description): # return super().format_description(self._format(description)) def format_option(self, option: Option) -> str: """Format help text for options.""" if option.help: if should_use_color(self.parser.values): option.help = cparse(option.help) else: option.help = cstrip(option.help) return super().format_option(option) class CylcOptionParser(OptionParser): """Common options for all cylc CLI commands.""" MULTITASK_USAGE = dedent(''' This command can operate on multiple tasks: Multiple Tasks: # Operate on two tasks //cycle-1/task-1 //cycle-2/task-2 # Operate on all members of a family //cycle-1/FAMILY-NAME # Operate on all tasks in a cycle //cycle # shorthand for '//cycle/root' Globs (note: quote globs or they might expand in your shell): # Match all tasks in cycle "1" with names beginning with "foo" ''//1/foo*' # Match the task "foo" in all active (i.e. n=0) cycles '//*/foo' # Match the tasks "foo-1" and "foo-2" in all active cycles. '//*/foo-[12]' Selectors (note: selectors only match active tasks): # match all failed tasks in cycle "1" //1:failed See `cylc help id` for more details. ''') MULTIWORKFLOW_USAGE = dedent(''' This command can operate on multiple workflows. Globs may be used: Multiple Workflows: # Operate on two workflows workflow-1 workflow-2 Globs (note: globs should be quoted): # Match all workflows '*' # Match the workflows foo-1, foo-2 'foo-[12]' See `cylc help id` for more details. ''') CAN_BE_USED_MULTIPLE = ( " This option can be used multiple times on the command line.") NOTE_PERSIST_ACROSS_RESTARTS = ( " NOTE: these settings persist across workflow restarts," " but can be set again on the \"cylc play\"" " command line if they need to be overridden." ) STD_OPTIONS = [ OptionSettings( ['-q', '--quiet'], help='Decrease verbosity.', action='decrement', dest='verbosity', useif='all'), OptionSettings( ['-v', '--verbose'], help='Increase Verbosity', dest='verbosity', action='count', default=env_to_verbosity(os.environ), useif='all'), OptionSettings( ['--debug'], help='Equivalent to -v -v', dest='verbosity', action='store_const', const=2, useif='all'), OptionSettings( ['--timestamp'], help='Add a timestamp to messages logged to the terminal.', action='store_true', dest='log_timestamp', default=False, useif='all'), OptionSettings( ['--no-timestamp'], help="Don't add a timestamp to messages logged" " to the terminal (this does nothing - it is now the default.", action='store_false', dest='_noop', default=False, useif='all'), OptionSettings( ['--color', '--colour'], metavar='WHEN', action='store', default='auto', choices=['never', 'auto', 'always'], help=( "When to use color/bold text in terminal output." " Options are 'never', 'auto' and 'always'." ), useif='color'), OptionSettings( ['--comms-timeout'], metavar='SEC', help=( "Set the timeout for communication with the running workflow." " The default is determined by the setup, 5 seconds for" " TCP comms and 300 for SSH." " If connections timeout, it likely means either, a complex" " request has been issued (e.g. cylc tui); there is a network" " issue; or a problem with the scheduler. Increasing the" " timeout will help with the first case." ), action='store', default=None, dest='comms_timeout', useif='comms'), OptionSettings( ['-s', '--set'], metavar='NAME=VALUE', help=( "Set the value of a Jinja2 template variable in the" " workflow definition." " Values should be valid Python literals so strings" " must be quoted" " e.g. 'STR=\"string\"', INT=43, BOOL=True." + CAN_BE_USED_MULTIPLE + NOTE_PERSIST_ACROSS_RESTARTS ), action='append', default=[], dest='templatevars', useif='jset' ), OptionSettings( ['-z', '--set-list', '--template-list'], metavar='NAME=VALUE1,VALUE2,...', # NOTE: deliberate non-breaking spaces in help text: help=( 'A more convenient alternative to --set for defining a list' ' of strings. E.G.' ' "-z FOO=a,b,c" is shorthand for' ' "-s FOO=[\'a\',\'b\',\'c\']".' ' Commas can be present in values if quoted, e.g.' ' "-z FOO=a,\'b,c\'" is shorthand for' ' "-s FOO=[\'a\',\'b,c\']".' + CAN_BE_USED_MULTIPLE + NOTE_PERSIST_ACROSS_RESTARTS ), action='append', default=[], dest='templatevars_lists', useif='jset' ), OptionSettings( ['--set-file'], metavar='FILE', help=( "Set the value of Jinja2 template variables in the" " workflow definition from a file containing NAME=VALUE" " pairs (one per line)." " As with --set values should be valid Python literals " " so strings must be quoted e.g. STR='string'." + NOTE_PERSIST_ACROSS_RESTARTS ), action='store', default=None, dest='templatevars_file', useif='jset' ) ] def __init__( self, usage: str, argdoc: Optional[List[Tuple[str, str]]] = None, comms: bool = False, jset: bool = False, multitask: bool = False, multiworkflow: bool = False, auto_add: bool = True, color: bool = True, segregated_log: bool = False ) -> None: """ Args: usage: Usage instructions. Typically this will be the __doc__ of the script module. argdoc: The args for the command, to be inserted into the usage instructions. Optional list of tuples of (name, description). comms: If True, allow the --comms-timeout option. jset: If True, allow the Jinja2 --set option. multitask: If True, insert the multitask text into the usage instructions. multiworkflow: If True, insert the multiworkflow text into the usage instructions. auto_add: If True, allow the standard options. color: If True, allow the --color option. segregated_log: If False, write all logging entries to stderr. If True, write entries at level < WARNING to stdout and entries at level >= WARNING to stderr. """ self.auto_add = auto_add if multiworkflow: usage += self.MULTIWORKFLOW_USAGE if multitask: usage += self.MULTITASK_USAGE args = "" self.n_compulsory_args = 0 self.n_optional_args = 0 self.unlimited_args = False self.comms = comms self.jset = jset self.color = color # Whether to log messages that are below warning level to stdout # instead of stderr: self.segregated_log = segregated_log if argdoc: maxlen = max(len(arg) for arg, _ in argdoc) usage += "\n\nArguments:" for arg, descr in argdoc: if arg.startswith('['): self.n_optional_args += 1 else: self.n_compulsory_args += 1 if arg.rstrip(']').endswith('...'): self.unlimited_args = True args += arg + " " pad = (maxlen - len(arg)) * ' ' + ' ' usage += "\n " + arg + pad + descr usage = usage.replace('ARGS', args) OptionParser.__init__( self, usage, option_class=CylcOption, formatter=CylcHelpFormatter() ) def get_std_options(self): """Get a data-structure of standard options""" opts = [] for opt in self.STD_OPTIONS: if ( opt.useif == 'all' or hasattr(self, opt.useif) and getattr(self, opt.useif) ): opts.append(opt) return opts def add_std_options(self): """Add standard options if they have not been overridden.""" for option in self.get_std_options(): if not any(self.has_option(i) for i in option.args): self.add_option(*option.args, **option.kwargs) @staticmethod def get_cylc_rose_options(): """Returns a list of option dictionaries if Cylc Rose exists.""" try: __import__('cylc.rose') except ImportError: return [] return [ OptionSettings( ["--opt-conf-key", "-O"], help=( "Use optional Rose Config Setting" " (If Cylc-Rose is installed)"), action="append", default=[], dest="opt_conf_keys", sources={'cylc-rose'}, ), OptionSettings( ["--define", '-D'], help=( "Each of these overrides the `[SECTION]KEY` setting" " in a `rose-suite.conf` file." " Can be used to disable a setting using the syntax" " `--define=[SECTION]!KEY` or" " even `--define=[!SECTION]`."), action="append", default=[], dest="defines", sources={'cylc-rose'}), OptionSettings( ["--rose-template-variable", '-S', '--define-suite'], help=( "As `--define`, but with an implicit `[SECTION]` for" " workflow variables."), action="append", default=[], dest="rose_template_vars", sources={'cylc-rose'}, ) ] def add_cylc_rose_options(self) -> None: """Add extra options for cylc-rose plugin if it is installed. Now a vestigal interface for get_cylc_rose_options. """ for option in self.get_cylc_rose_options(): self.add_option(*option.args, **option.kwargs) def parse_args(self, api_args, remove_opts=None): """Parse options and arguments, overrides OptionParser.parse_args. Args: api_args (list): Command line options if passed via Python as opposed to sys.argv remove_opts (list): List of standard options to remove before parsing. """ if self.auto_add: # Add common options after command-specific options. self.add_std_options() if remove_opts: for opt in remove_opts: with suppress(ValueError): self.remove_option(opt) (options, args) = OptionParser.parse_args(self, api_args) if len(args) < self.n_compulsory_args: self.error("Wrong number of arguments (too few)") elif ( not self.unlimited_args and len(args) > self.n_compulsory_args + self.n_optional_args ): self.error("Wrong number of arguments (too many)") if self.jset and options.templatevars_file: options.templatevars_file = os.path.abspath(os.path.expanduser( options.templatevars_file) ) cylc.flow.flags.verbosity = options.verbosity # Set up stream logging for CLI. Note: # 1. On choosing STDERR: Log messages are diagnostics, so STDERR is the # better choice for the logging stream. This allows us to use STDOUT # for verbosity agnostic outputs. # 2. Scheduler will remove this handler when it becomes a daemon. LOG.setLevel(verbosity_to_log_level(options.verbosity)) # Remove NullHandler before add the StreamHandler while LOG.handlers: LOG.handlers[0].close() LOG.removeHandler(LOG.handlers[0]) log_handler = logging.StreamHandler(sys.stderr) log_handler.setFormatter(CylcLogFormatter( timestamp=options.log_timestamp, dev_info=(options.verbosity > 2) )) LOG.addHandler(log_handler) if self.segregated_log: setup_segregated_log_streams(LOG, log_handler) return (options, args) @staticmethod def optional(arg: Tuple[str, str]) -> Tuple[str, str]: """Make an argdoc tuple display as an optional arg with square brackets.""" name, doc = arg return (f'[{name}]', doc) class Options: """Wrapper to allow Python API access to optparse CLI functionality. Example: Create an optparse parser as normal: >>> import optparse >>> parser = optparse.OptionParser() >>> _ = parser.add_option('-a', default=1) >>> _ = parser.add_option('-b', default=2) Create an Options object from the parser: >>> PythonOptions = Options(parser, overrides={'c': 3}) "Parse" options via Python API: >>> opts = PythonOptions(a=4) Access options as normal: >>> opts.a 4 >>> opts.b 2 >>> opts.c 3 Optparse allows you to create new options on the fly: >>> opts.d = 5 >>> opts.d 5 But you can't create new options at initiation, this gives us basic input validation: >>> PythonOptions(e=6) Traceback (most recent call last): ValueError: e You can reuse the object multiple times >>> opts2 = PythonOptions(a=2) >>> id(opts) == id(opts2) False """ def __init__( self, parser: OptionParser, overrides: Optional[Dict[str, Any]] = None ) -> None: if overrides is None: overrides = {} if isinstance(parser, CylcOptionParser) and parser.auto_add: parser.add_std_options() self.defaults = {**parser.defaults, **overrides} def __call__(self, **kwargs) -> Values: opts = Values(self.defaults) for key, value in kwargs.items(): if not hasattr(opts, key): raise ValueError(key) setattr(opts, key, value) return opts def appendif(list_, item): """Avoid duplicating items in output list""" if item not in list_: list_.append(item) return list_ def combine_options_pair(first_list, second_list): """Combine two option lists recording where each came from. Scenarios: - Arguments are identical - return this argument. - Arguments are not identical but have some common label strings, i.e. both arguments can be invoked using `-f`. - If there are non-shared label strings strip the shared ones. - Otherwise raise an error. E.g: If `command-A` has an option `-f` or `--file` and `command-B has an option `-f` or `--fortran`` then `command-A+B` will have options `--fortran` and `--file` but _not_ `-f`, which would be confusing. - Arguments only apply to a single component of the compound CLI script. """ output = [] if not first_list: output = second_list elif not second_list: output = first_list else: for first, second in product(first_list, second_list): # Two options are identical in both args and kwargs: if first == second: first._update_sources(second) output = appendif(output, first) # If any of the argument names identical we must remove # overlapping names (if we can) # e.g. [-a, --aleph], [-a, --alpha-centuri] -> keep both options # but neither should have the `-a` short version: elif ( first != second and first & second ): # if any of the args are different: if first.args == second.args: raise Exception( f'Clashing Options \n{first.args}\n{second.args}') else: first_args = first - second second.args = second - first first.args = first_args output = appendif(output, first) output = appendif(output, second) else: # Neither option appears in the other list, so it can be # appended: if not first._in_list(second_list): output = appendif(output, first) if not second._in_list(first_list): output = appendif(output, second) return output def add_sources_to_helps( options: Iterable[OptionSettings], modify: Optional[dict] = None ) -> None: """Get list of CLI commands this option applies to and prepend that list to the start of help. Arguments: options: List of OptionSettings to modify help upon. modify: Dict of items to substitute: Intended to allow one to replace cylc-rose with the names of the sub-commands cylc rose options apply to. """ modify = {} if modify is None else modify for option in options: if hasattr(option, 'sources'): sources = list(option.sources) for match, sub in modify.items(): if match in option.sources: sources.append(sub) sources.remove(match) option.kwargs['help'] = ( f'[{", ".join(sources)}]' f' {option.kwargs["help"]}' ) def combine_options( *args: List[OptionSettings], modify: Optional[dict] = None ) -> List[OptionSettings]: """Combine lists of Cylc options. Ordering should be irrelevant because combine_options_pair should be commutative, and the overall order of args is not relevant. """ output = args[0] for arg in args[1:]: output = combine_options_pair(arg, output) add_sources_to_helps(output, modify) return output def cleanup_sysargv( script_name: str, workflow_id: str, options: 'Values', compound_script_opts: Iterable['OptionSettings'], script_opts: Iterable['OptionSettings'], source: str, ) -> None: """Remove unwanted options from sys.argv Some cylc scripts (notably Cylc Play when it is re-invoked on a scheduler server) require the correct content in sys.argv: This function subtracts the unwanted options from sys.argv. Args: script_name: Name of the target script. For example if we are using this for the play step of cylc vip then this will be "play". workflow_id: options: Actual options provided to the compound script. compound_script_options: Options available in compound script. script_options: Options available in target script. source: Source directory. """ # Organize Options by dest. script_opts_by_dest = { x.kwargs.get('dest', x.args[0].strip(DOUBLEDASH)): x for x in script_opts } compound_opts_by_dest = { x.kwargs.get('dest', x.args[0].strip(DOUBLEDASH)): x for x in compound_script_opts } # Get a list of unwanted args: unwanted_compound: List[str] = [] unwanted_simple: List[str] = [] for unwanted_dest in set(options.__dict__) - set(script_opts_by_dest): for unwanted_arg in compound_opts_by_dest[unwanted_dest].args: if ( compound_opts_by_dest[unwanted_dest].kwargs.get('action', None) in ['store_true', 'store_false'] ): unwanted_simple.append(unwanted_arg) else: unwanted_compound.append(unwanted_arg) new_args = filter_sysargv(sys.argv, unwanted_simple, unwanted_compound) # replace compound script name: new_args[1] = script_name # replace source path with workflow ID. if str(source) in new_args: new_args.remove(str(source)) if workflow_id not in new_args: new_args.append(workflow_id) sys.argv = new_args def filter_sysargv( sysargs, unwanted_simple: List, unwanted_compound: List ) -> List: """Create a copy of sys.argv without unwanted arguments: Cases: >>> this = filter_sysargv >>> this(['--foo', 'expects-a-value', '--bar'], [], ['--foo']) ['--bar'] >>> this(['--foo=expects-a-value', '--bar'], [], ['--foo']) ['--bar'] >>> this(['--foo', '--bar'], ['--foo'], []) ['--bar'] """ pop_next: bool = False new_args: List = [] for this_arg in sysargs: parts = this_arg.split('=', 1) if pop_next: pop_next = False continue elif parts[0] in unwanted_compound: # Case --foo=value or --foo value if len(parts) == 1: # --foo value pop_next = True continue elif parts[0] in unwanted_simple: # Case --foo does not expect a value: continue else: new_args.append(this_arg) return new_args def log_subcommand(*args): """Log a command run as part of a sequence. Example: >>> log_subcommand('ruin', 'my_workflow') \x1b[1m\x1b[36m$ cylc ruin my_workflow\x1b[0m\x1b[1m\x1b[0m\n """ # Args might be posixpath or similar. args = [str(a) for a in args] print(cparse( f'$ cylc {" ".join(args)}' )) cylc-flow-8.6.4/cylc/flow/remote.py0000664000175000017500000003511415202510242017357 0ustar alastairalastair# 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 . """Run command on a remote, (i.e. a remote [user@]host).""" import os from pathlib import Path from posix import WIFSIGNALED import shlex from shlex import quote import signal # CODACY ISSUE: # Consider possible security implications associated with Popen module. # REASON IGNORED: # Subprocess is needed, but we use it with security in mind. from subprocess import ( DEVNULL, PIPE, Popen, ) import sys from time import ( sleep, time, ) from typing import ( Any, Dict, List, Literal, Optional, Tuple, Union, overload, ) from cylc.flow import ( LOG, __version__ as CYLC_VERSION, ) import cylc.flow.flags from cylc.flow.log_level import verbosity_to_opts from cylc.flow.platforms import ( get_host_from_platform, get_platform, ) from cylc.flow.util import format_cmd def get_proc_ancestors(): """Return list of parent PIDs back to init.""" pid = os.getpid() ancestors = [] while True: p = Popen( # nosec ["ps", "-p", str(pid), "-oppid="], stdout=PIPE, stderr=PIPE, text=True ) # * there is no untrusted output ppid = p.communicate()[0].strip() if not ppid: return ancestors ancestors.append(ppid) pid = ppid def watch_and_kill(proc): """Kill proc if my PPID (etc.) changed - e.g. ssh connection dropped.""" gpa = get_proc_ancestors() ps_interval = 60 # secs last_ps_time = time() while True: if proc.poll() is not None: break if time() - last_ps_time > ps_interval: # Run ps command if get_proc_ancestors() != gpa: sleep(1) os.kill(proc.pid, signal.SIGTERM) break last_ps_time = time() sleep(1) def run_cmd( command, stdin=None, stdin_str=None, capture_process=False, capture_status=False, manage=False, text=True, ): """Run a given cylc command on another host. Arguments: command (list): command inclusive of all opts and args required to run via ssh. stdin (file): If specified, it should be a readable file object. If None, DEVNULL is set if output is to be captured. stdin_str (str): A string to be passed to stdin. Implies `stdin=PIPE`. capture_process (boolean): If True, set stdout=PIPE and return the Popen object. capture_status (boolean): If True, and the remote command is unsuccessful, return the associated exit code instead of exiting with an error. manage (boolean): If True, watch ancestor processes and kill command if they change (e.g. kill tail-follow commands when parent ssh connection dies). text (boolean): If True, use string mode instead of bytes for communicating with subprocess. Return: * If capture_process=True: the Popen[str|bytes] object if created successfully. * Else if capture_status=True: the remote command exit code. * Else if the remote command is executed successfully: 0. * Else exits with an error message instead of returning. Exits with code 1 in the event of certain command errors. """ # CODACY ISSUE: # subprocess call - check for execution of untrusted input. # REASON IGNORED: # The command is read from the site/user global config file, but we check # above that it ends in 'cylc', and in any case the user could execute # any such command directly via ssh. stdout = None stderr = None if capture_process: stdout = PIPE stderr = PIPE if stdin is None: stdin = DEVNULL if stdin_str: read, write = os.pipe() os.write(write, stdin_str.encode()) os.close(write) stdin = read try: LOG.debug(f'running command:\n$ {format_cmd(command)}') proc = Popen( # nosec command, stdin=stdin, stdout=stdout, stderr=stderr, text=text ) # * this see CODACY ISSUE comment above except OSError as exc: sys.exit(r'ERROR: %s: %s' % ( exc, ' '.join(quote(item) for item in command))) if capture_process: return proc if manage: watch_and_kill(proc) res = proc.wait() if WIFSIGNALED(res): sys.exit( r'ERROR: command terminated by signal %d: %s' % (res, ' '.join(quote(item) for item in command)) ) if capture_status or not res: return res sys.exit( r'ERROR: command returns %d: %s' % (res, ' '.join(quote(item) for item in command)) ) def get_includes_to_rsync(rsync_includes=None): """Returns list of configured dirs/files for remote file installation.""" configured_includes = [] if rsync_includes is not None: for include in rsync_includes: if include.endswith("/"): # item is a directory configured_includes.append("/" + include + "***") else: # item is a file configured_includes.append("/" + include) return configured_includes DEFAULT_RSYNC_OPTS = [ '-a', '--checksum', '--out-format=%o %n%L', '--no-t' ] # %o: the operation (send or del.) # %n: filename # %L: "-> symlink_target" if applicable DEFAULT_INCLUDES = [ '/ana/***', # Rose ana analysis modules '/app/***', # Rose applications '/bin/***', # Cylc bin directory (added to PATH) '/etc/***', # Miscellaneous resources '/lib/***', # Cylc lib directory (lib/python added to PYTHONPATH for # workflow config) ] def construct_rsync_over_ssh_cmd( src_path: str, dst_path: str, platform: Dict[str, Any], rsync_includes=None, bad_hosts=None ) -> Tuple[List[str], str]: """Constructs the rsync command used for remote file installation. Includes as standard the directories: app, bin, etc, lib; and the server key, used for ZMQ authentication. Args: src_path: source path dst_path: path of target platform: contains info relating to platform rsync_includes: files and directories to be included in the rsync Raises: NoHostsError: If there are no hosts available for the requested platform. Developer Warning: The Cylc Subprocess Pool method ``rsync_255_fail`` relies on ``rsync_cmd[0] == 'rsync'``. Please check that changes to this function do not break ``rsync_255_fail``. """ dst_path = dst_path.replace('$HOME/', '') dst_host = get_host_from_platform(platform, bad_hosts=bad_hosts) ssh_cmd = platform['ssh command'] command = platform['rsync command'] rsync_cmd = shlex.split(command) rsync_options = [ "--delete", "--rsh=" + ssh_cmd, "--include=/.service/", "--include=/.service/server.key" ] + DEFAULT_RSYNC_OPTS # Note to future devs - be wary of changing the order of the following # rsync options, rsync is very particular about order of in/ex-cludes. rsync_cmd.extend(rsync_options) for exclude in ['/log', '/share', '/work']: rsync_cmd.append(f"--exclude={exclude}") for include in DEFAULT_INCLUDES: rsync_cmd.append(f"--include={include}") for include in get_includes_to_rsync(rsync_includes): rsync_cmd.append(f"--include={include}") # The following excludes are required in case these are added to the rsync_cmd.append("--exclude=*") # exclude everything else rsync_cmd.append(f"{src_path}/") rsync_cmd.append(f"{dst_host}:{dst_path}/") return rsync_cmd, dst_host def construct_ssh_cmd( raw_cmd, platform, host, forward_x11=False, stdin=False, set_UTC=False, set_verbosity=False, timeout=None, ): """Build an SSH command for execution on a remote platform hosts. Arguments: raw_cmd (list): primitive command to run remotely. platform (dict): The Cylc job "platform" to run the command on. This is used to determine the settings used e.g. "ssh command". host (string): remote host name. Use 'localhost' if not specified. forward_x11 (boolean): If True, use 'ssh -Y' to enable X11 forwarding, else just 'ssh'. stdin: If None, the `-n` option will be added to the SSH command line. set_UTC (boolean): If True, check UTC mode and specify if set to True (non-default). set_verbosity (boolean): If True apply -q, -v opts to match cylc.flow.flags.verbosity. timeout (str): String for bash timeout command. Returns: list - A list containing a chosen command including all arguments and options necessary to directly execute the bare command on a given host via ssh. """ command = shlex.split(platform['ssh command']) if forward_x11: command.append('-Y') if stdin is None: command.append('-n') command.append(host) # Pass CYLC_VERSION and optionally, CYLC_CONF_PATH & CYLC_UTC through. command += ['env', quote(r'CYLC_VERSION=%s' % CYLC_VERSION)] for envvar in [ 'CYLC_CONF_PATH', 'CYLC_COVERAGE', 'CLIENT_COMMS_METH', 'CYLC_ENV_NAME', *platform['ssh forward environment variables'], ]: if envvar in os.environ: command.append( quote(f'{envvar}={os.environ[envvar]}') ) if set_UTC and os.getenv('CYLC_UTC') in ["True", "true"]: command.append(quote(r'CYLC_UTC=True')) command.append(quote(r'TZ=UTC')) # Use bash -l? ssh_login_shell = platform['use login shell'] if ssh_login_shell: # A login shell will always source /etc/profile and the user's bash # profile file. To avoid having to quote the entire remote command # it is passed as arguments to the bash script. command += ['bash', '--login', '-c', quote(r'exec "$0" "$@"')] if timeout: command += ['timeout', timeout] # 'cylc' on the remote host remote_cylc_path = platform['cylc path'] if remote_cylc_path: cylc_cmd = str(Path(remote_cylc_path) / 'cylc') else: cylc_cmd = 'cylc' command.append(cylc_cmd) # Insert core raw command after ssh, but before its own, command options. command += raw_cmd if set_verbosity: command.extend(verbosity_to_opts(cylc.flow.flags.verbosity)) return command def construct_cylc_server_ssh_cmd( cmd, host, **kwargs, ): """Convenience function to building SSH commands for remote Cylc servers. Build an SSH command that connects to the specified host using the localhost platform config. * To run commands on job platforms use construct_ssh_cmd. * Use this interface to connect to: * Cylc servers (i.e. `[scheduler][run hosts]available`). * The host `cylc play` was run on, use this interface. This assumes the host you are connecting to shares the $HOME filesystem with the localhost platform. For arguments and returns see construct_ssh_cmd. """ return construct_ssh_cmd( cmd, get_platform(), # use localhost settings host, **kwargs, ) def remote_cylc_cmd( cmd, platform, bad_hosts=None, host=None, stdin=None, stdin_str=None, ssh_login_shell=None, ssh_cmd=None, remote_cylc_path=None, capture_process=False, manage=False, text=True, ): """Execute a Cylc command on a remote platform. Uses the provided platform configuration to construct the command. For arguments and returns see construct_ssh_cmd and run_cmd. Raises: NoHostsError: If the platform is not contactable. Exits with code 1 in the event of certain command errors. """ if not host: # no host selected => perform host selection from platform config host = get_host_from_platform(platform, bad_hosts=bad_hosts) return run_cmd( construct_ssh_cmd( cmd, platform, host=host, stdin=True if stdin_str else stdin, ), stdin=stdin, stdin_str=stdin_str, capture_process=capture_process, capture_status=True, manage=manage, text=text ) @overload def cylc_server_cmd( cmd, host: Optional[str] = None, *, capture_process: 'Literal[False]' = False, **kwargs, ) -> int: ... @overload def cylc_server_cmd( cmd, host: Optional[str] = None, *, capture_process: 'Literal[True]', **kwargs, ) -> Popen: ... @overload def cylc_server_cmd( cmd, host: Optional[str] = None, *, capture_process: bool, **kwargs, ) -> Union[int, Popen]: ... def cylc_server_cmd( cmd, host: Optional[str] = None, *, capture_process: bool = False, **kwargs, ) -> Union[int, Popen]: """Convenience function for running commands on remote Cylc servers. Executes a Cylc command on the specified host using localhost platform config. * To run commands on job platforms use remote_cylc_cmd. * Use this interface to run commands on: * Cylc servers (i.e. `[scheduler][run hosts]available`). * The host `cylc play` was run on. Runs a command via SSH using the configuration for the localhost platform. This assumes the host you are connecting to shares the $HOME filesystem with the localhost platform. For arguments and returns see construct_ssh_cmd and run_cmd. Raises: NoHostsError: If the platform is not contactable. Exits with code 1 in the event of certain command errors. """ return remote_cylc_cmd( cmd, get_platform(), # use localhost settings host=host, capture_process=capture_process, **kwargs, ) cylc-flow-8.6.4/cylc/flow/parsec/0000775000175000017500000000000015202510242016763 5ustar alastairalastaircylc-flow-8.6.4/cylc/flow/parsec/__init__.py0000664000175000017500000000147115202510242021077 0ustar alastairalastair# 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 . """Parsec - library for parsing nested Cylc's INI-style configuration.""" cylc-flow-8.6.4/cylc/flow/parsec/OrderedDict.py0000664000175000017500000001476215202510242021537 0ustar alastairalastair# 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 . """Ordered Dictionary data structure used extensively in cylc.""" from collections import OrderedDict class OrderedDictWithDefaults(OrderedDict): """Subclass to provide defaults fetching capability.""" # Note that defining a '__missing__' method would work for foo[key], # but doesn't for foo.get(key). def __init__(self, *args, **kwargs): """Allow a defaults argument.""" self._allow_contains_default = True super().__init__(*args, **kwargs) def __getitem__(self, key): # Override to look in our special defaults attribute, if it exists. try: return OrderedDict.__getitem__(self, key) except KeyError: if hasattr(self, 'defaults_'): return self.defaults_[key] raise def __setitem__(self, *args, **kwargs): # Make sure that we don't set the default value! self._allow_contains_default = False return_value = OrderedDict.__setitem__(self, *args, **kwargs) self._allow_contains_default = True return return_value def keys(self): """Include the default keys, after the list of actually-set ones.""" keys = list(self) for key in getattr(self, 'defaults_', []): if key not in keys: keys.append(key) return keys def values(self): """Return a list of values, including default ones.""" return [self[key] for key in self.keys()] def items(self): """Return key-value pairs, including default ones.""" return [(key, self[key]) for key in self.keys()] def iterkeys(self): """Include default keys""" yield from OrderedDict.keys(self) for key in getattr(self, 'defaults_', []): if not OrderedDict.__contains__(self, key): yield key def itervalues(self): """Include default values.""" for k in self.keys(): yield self[k] def iteritems(self): """Include default key-value pairs.""" for k in self.keys(): yield (k, self[k]) def __contains__(self, key): if ( self._allow_contains_default and key in getattr(self, "defaults_", {}) ): return True return OrderedDict.__contains__(self, key) def __bool__(self): """Include any default keys in the nonzero calculation.""" return bool(list(self.keys())) def prepend(self, key, value): """Prepend new item in the ordered dict.""" # https://stackoverflow.com/questions/16664874/ # how-can-i-add-an-element-at-the-top-of-an-ordereddict-in-python self[key] = value self.move_to_end(key, last=False) class DictTree: """An object providing a single point of access to a tree of dicts. * Allows easy extraction of values from a collection of dictionaries. * Values from dictionaries earlier in the list will take priority over values from dictionaries later in the list. * If the dict objects provided provide a custom `get` interface this will take priority over the `__getitem__` interface. Args: tree (list): A list of dict-type objects. Examples: Regular usage: >>> tree = DictTree( ... {'a': 1, 'b': 2}, ... {'b': 3, 'c': 4} ... ) >>> tree['a'] 1 >>> tree['b'] # items from earlier entries are preferred 2 >>> tree['c'] 4 >>> tree['d'] Traceback (most recent call last): KeyError: 'd' Quirk, None values result in KeyErrors: >>> tree = DictTree({'a': None}) >>> tree['a'] Traceback (most recent call last): KeyError: 'a' """ def __init__(self, *tree): self._tree = tree def __getitem__(self, key): values = [] defaults = [] for branch in self._tree: # priority goes to the `get` method value = branch.get(key) if value is not None: values.append(value) defaults.append(None) else: try: # then falls back to the `__getitem__` method defaults.append(branch[key]) values.append(None) except KeyError: # okay, this key really isn't present here values.append(None) defaults.append(None) # handle nested dictionaries if any((isinstance(item, dict) for item in values + defaults)): return DictTree(*( item for item in values if item is not None )) # handle non-existent keys if all((item is None for item in values + defaults)): raise KeyError(key) # return first value or default encountered return next(( item for item in values + defaults if item is not None )) def __eq__(self, other): if not isinstance(other, DictTree): return False return self._tree == other._tree def __iter__(self): def inner(tree): # yield keys from all branches (but only once) yield from { key for branch in tree for key in branch } return inner(self._tree) def get(self, key, default=None): """Get an item from this tree or return `default` if not present. Note: Behaviour purposefully differs from OrderedDictWithDefaults, this `get` method *will* return default values if present. """ try: return self[key] except KeyError: return default cylc-flow-8.6.4/cylc/flow/parsec/util.py0000664000175000017500000003374415202510242020325 0ustar alastairalastair# 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 . """Utility functions for printing and manipulating PARSEC NESTED DICTS. The copy and override functions below assume values are either dicts (nesting) or shallow collections of simple types. """ from copy import copy from collections import deque import re import sys from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults def intlistjoin(lst): """Return dump string for int list. Attempt grouping on sequences with 3 or more numbers: * Consider sequential int values in list, group them together in `START..END[..STEP]` syntax where relevant. * Consider same numbers in list, group them together in `N*INT` syntax. Arguments: lst (list): a list of int numbers. Return (str): The (hopefully) nicely formatted dump string. Examples: >>> intlistjoin([]) '' >>> intlistjoin([10]) '10' >>> intlistjoin([10, 10]) '10, 10' >>> intlistjoin([10, 10, 10]) '3*10' >>> intlistjoin([10, 11]) '10, 11' >>> intlistjoin([10, 11, 12]) '10..12' >>> intlistjoin([-1, 1, 3, 5, 0, 0, 0, 8, 9, 11, -1, 0, 1, 2]) '-1..5..2, 3*0, 8, 9, 11, -1..2' >>> intlistjoin([-1, 1, 3, 5, 0, 0, 0, 8, 9, 11, 11, 12]) '-1..5..2, 3*0, 8, 9, 11, 11, 12' >>> intlistjoin( ... [-10, -10, -1, 1, 3, 5, 0, 0, 0, 8, 9, 11, -1, 0, 1, 2]) '-10, -10, -1..5..2, 3*0, 8, 9, 11, -1..2' >>> intlistjoin( ... [64747, -10, -10, -10, -1, 1, 3, 5, 0, 0, 0, 8, 9, 11, -1, 0, ... 1, 2, 3, 4, 19, 19, 19, 20, 20, 20, 21, 22, 23]) '64747, 3*-10, -1..5..2, 3*0, 8, 9, 11, -1..4, 3*19, 3*20, 21..23' """ rets = [] items = list(lst) while items: group = [items.pop(0)] while items: if (len(group) == 1 or items[0] - group[-1] == group[-1] - group[-2]): group.append(items.pop(0)) else: # If 2 numbers only, return 1 back if grouping still possible # in subsequent lots. if len(group) == 2 and len(items) >= 2: items.insert(0, group.pop()) break if len(group) <= 2: # Less than 2 numbers rets += [str(item) for item in group] elif group[1] - group[0] > 1: # Sequence of numbers with equal steps > 1 rets.append( '%d..%d..%d' % (group[0], group[-1], group[1] - group[0])) elif group[1] - group[0] == 1: # Sequence of incremental numbers rets.append('%d..%d' % (group[0], group[-1])) else: # Sequence of same number rets.append('%d*%d' % (len(group), group[0])) return ', '.join(rets) def listjoin(lst, none_str=''): """Return string from joined list. Quote all elements if any of them contain comment or list-delimiter characters (currently quoting must be consistent across all elements). Note: multi-line values in list is not handle. """ if not lst: # empty list return none_str if len(lst) > 2 and all(isinstance(item, int) for item in lst): return intlistjoin(lst) items = [] for item in lst: if item is None: items.append(none_str) elif any(char in str(item) for char in ',#"\''): items.append(repr(item)) # will be quoted else: items.append(str(item)) return ', '.join(items) def printcfg(cfg, level=0, indent=0, prefix='', none_str='', handle=None): """Pretty-print a parsec config item or section (nested dict). Args: cfg: The config to be printed. level: Each level of the hierarchy is printed with this many extra square brackets: E.g. if ``indent=2`` then ``[runtime]`` will be printed as ``[[[runtime]]]. indent (int): Indentation of top level sections - if set the whole document will have a left margin this many 4 space indents wide. prefix (str): Prefix each line with this. none_str (str): Value to insert instead of blank if no value is set in config. handle (stream handler): Where to write the output. As returned by parse.config.get(). """ # Note: default handle=sys.stdout, but explicitly setting this as a kwarg # interferes with pytest capsys: # https://github.com/pytest-dev/pytest/issues/1132#issuecomment-147506107 if handle is None: handle = sys.stdout stack = [("", cfg, level, indent)] while stack: key_i, cfg_i, level_i, indent_i = stack.pop() spacer = " " * 4 * (indent_i - 1) if isinstance(cfg_i, dict): if not cfg_i and none_str is None: # Don't print empty sections if none_str is None. This does not # handle sections with no items printed because the values of # all items are empty or None. continue if key_i and level_i: # Print heading msg = "%s%s%s%s%s\n" % ( prefix, spacer, '[' * level_i, str(key_i), ']' * level_i) if hasattr(handle, 'mode') and 'b' in handle.mode: msg = msg.encode() handle.write(msg) # Nested sections are printed after normal settings subsections = [] values = [] for key, item in cfg_i.items(): if isinstance(item, dict): subsections.append((key, item, level_i + 1, indent_i + 1)) else: values.append((key, item, level_i + 1, indent_i + 1)) stack += reversed(subsections) stack += reversed(values) else: key = "" if key_i: key = "%s = " % key_i if cfg_i is None: value = none_str elif isinstance(cfg_i, list): value = listjoin(cfg_i, none_str) elif "\n" in str(cfg_i) and key: value = '"""\n' for line in str(cfg_i).splitlines(True): value += spacer + " " * 4 + line value += '\n' + spacer + '"""' else: value = str(cfg_i) if value is not None: msg = "%s%s%s%s\n" % (prefix, spacer, key, value) if hasattr(handle, 'mode') and 'b' in handle.mode: msg = msg.encode() handle.write(msg) def replicate(target, source): """Replicate source *into* target. Source elements need not exist in target already, so source overrides common elements in target and otherwise adds elements to it. """ if not source: return if hasattr(source, "defaults_"): target.defaults_ = pdeepcopy(source.defaults_) for key, val in source.items(): if isinstance(val, dict): if key not in target: target[key] = OrderedDictWithDefaults() if hasattr(val, 'defaults_'): target[key].defaults_ = pdeepcopy(val.defaults_) replicate(target[key], val) elif isinstance(val, list): target[key] = val[:] else: target[key] = val def pdeepcopy(source): """Make a deep copy of a pdict source""" target = OrderedDictWithDefaults() replicate(target, source) return target def poverride(target, sparse, prepend=False): """Override or add items in a target pdict. Target sub-dicts must already exist. For keys that already exist in the target, the value is overridden in-place. New keys can be prepended in the target (Cylc use case: broadcast environment variables should be defined first in the user environment section, to allow use in subsequent variable definitions. """ if not sparse: return for key, val in sparse.items(): if isinstance(val, dict): poverride(target[key], val, prepend) else: if prepend and (key not in target): # Prepend new items in the target ordered dict. setitem = target.prepend else: # Override in-place in the target ordered dict. setitem = target.__setitem__ if isinstance(val, list): setitem(key, val[:]) else: setitem(key, val) def m_override(target, sparse): """Override items in a target pdict. Target keys must already exist unless there is a "__MANY__" placeholder in the right position. """ if not sparse: return stack = deque([(sparse, target, [], OrderedDictWithDefaults())]) defaults_list = [] while stack: source, dest, keylist, many_defaults = stack.popleft() if many_defaults: defaults_list.append((dest, many_defaults)) for key, val in source.items(): if isinstance(val, dict): child_many_defaults = many_defaults.get( key, OrderedDictWithDefaults()) if key not in dest: if '__MANY__' in dest: dest[key] = OrderedDictWithDefaults() child_many_defaults = dest['__MANY__'] elif '__MANY__' in many_defaults: # A 'sub-many' dict - would it ever exist in real life? dest[key] = OrderedDictWithDefaults() child_many_defaults = many_defaults['__MANY__'] elif key not in many_defaults: # TODO - validation prevents this, but handle properly # for completeness. raise Exception( "parsec dict override: no __MANY__ placeholder" + "%s" % (keylist + [key]) ) dest[key] = OrderedDictWithDefaults() stack.append( (val, dest[key], keylist + [key], child_many_defaults)) else: if key not in dest: if not ( '__MANY__' in dest or key in many_defaults or '__MANY__' in many_defaults ): # TODO - validation prevents this, but handle properly # for completeness. raise Exception( "parsec dict override: no __MANY__ placeholder" + "%s" % (keylist + [key]) ) if isinstance(val, list): dest[key] = val[:] else: dest[key] = val if isinstance(val, list): dest[key] = val[:] else: dest[key] = val for dest_dict, defaults in defaults_list: dest_dict.defaults_ = defaults def un_many(cfig): """Remove any '__MANY__' items from a nested dict, in-place.""" if not cfig: return for key, val in list(cfig.items()): if key == '__MANY__': try: del cfig[key] except KeyError: if ( not hasattr(cfig, 'defaults_') or key not in cfig.defaults_ ): raise del cfig.defaults_[key] elif isinstance(val, dict): un_many(cfig[key]) def itemstr(parents=None, item=None, value=None): """ Pretty-print an item from list of sections, item name, and value E.g.: ([sec1, sec2], item, value) to '[sec1][sec2]item = value'. """ if parents: keys = copy(parents) if value and not item: # last parent is the item item = keys[-1] keys.remove(item) text = '[' + ']['.join(keys) + ']' else: text = '' if item: text += str(item) if value: text += " = " + str(value) if not text: text = str(value) return text # pattern for picking out comma separated items which does not split commas # inside of quotes SECTION_EXPAND_PATTERN = re.compile( r''' (?: [^,"']+ | "[^"]*" | '[^']*' )+ ''', re.X ) def dequote(string: str, chars='"\'') -> str: """Simple approach to strip quotes from strings. Examples: >>> dequote('"foo"') 'foo' >>> dequote("'foo'") 'foo' >>> dequote('a"b"c') 'a"b"c' """ if len(string) < 2: return string for char in chars: if string[0] == char and string[-1] == char: return string[1:-1] return string def expand_many_section(config): """Expand comma separated entries. Intended for use in __MANY__ sections i.e. ones in which headings are user defined. Returns the expanded config i.e. does not modify it in place (this is necessary to preserve definition order). """ ret = {} for section_name, section in config.items(): for name in SECTION_EXPAND_PATTERN.findall(section_name): name = dequote(name.strip()).strip() replicate(ret.setdefault(name, {}), section) return ret cylc-flow-8.6.4/cylc/flow/parsec/jinja2support.py0000664000175000017500000002660015202510242022153 0ustar alastairalastair# 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 . """cylc support for the Jinja2 template processor Importing code should catch ImportError in case Jinja2 is not installed. """ from contextlib import suppress from glob import glob import importlib import os import pkgutil import re import sys import traceback import typing as t from jinja2 import ( BaseLoader, ChoiceLoader, Environment, FileSystemLoader, StrictUndefined, TemplateNotFound, TemplateSyntaxError) from cylc.flow import LOG from cylc.flow.exceptions import InputError import cylc.flow.flags from cylc.flow.parsec.exceptions import Jinja2Error from cylc.flow.parsec.fileparse import get_cylc_env_vars TRACEBACK_LINENO = re.compile( r'\s+?File "(?P.*)", line (?P\d+), in .*template' ) CONTEXT_LINES = 3 class PyModuleLoader(BaseLoader): """Load python module as Jinja2 template. This loader piggybacks on the jinja import mechanism and returns an empty template that exports module's namespace.""" # no source access for this loader has_source_access = False def __init__(self, prefix='__python__'): self._templates = {} # prefix that can be used to avoid name collisions with template files self._python_namespace_prefix = prefix + '.' def load( self, environment, name, globals=None # noqa: A002 (required to match underlying interface?) ): """Imports Python module and returns it as Jinja2 template.""" if name.startswith(self._python_namespace_prefix): name = name[len(self._python_namespace_prefix):] with suppress(KeyError): return self._templates[name] try: mdict = __import__(name, fromlist=['*']).__dict__ except ImportError: raise TemplateNotFound(name) from None # inject module dict into the context of an empty template def root_render_func(context, *args, **kwargs): """Template render function.""" if False: yield None # to make it a generator context.vars.update(mdict) context.exported_vars.update(mdict) templ = environment.from_string('') templ.root_render_func = root_render_func self._templates[name] = templ return templ class Jinja2AssertionError(Exception): """Exception raised by the Jinja2 "raise" and "assert" functions.""" def raise_helper(message): """Provides a Jinja2 function for raising exceptions.""" raise Jinja2AssertionError(message) def assert_helper(logical, message): """Provides a Jinja2 function for asserting logical expressions.""" if not logical: raise_helper(message) return '' # Prevent None return value polluting output. def _load_jinja2_extensions(): """ Load modules under the cylc.jinja package namespace. Filters provided by third-party packages (i.e. user created packages) will also be included if correctly put in the cylc.jinja.filters namespace. Global variables are expected to be found in cylc.jinja.globals, and jinja tests in cylc.jinja.tests. The dictionary returned contains the full module name (e.g. cylc.jinja.filters.pad), and the second value is the module object (same object as in __import__("module_name")__). :return: jinja2 filter modules :rtype: dict[string, object] """ jinja2_extensions = {} for module_name in [ "cylc.flow.jinja.filters", "cylc.flow.jinja.globals", "cylc.flow.jinja.tests" ]: try: module = importlib.import_module(module_name) jinja2_filters_modules = pkgutil.iter_modules( module.__path__, f"{module.__name__}.") if jinja2_filters_modules: namespace = module_name.split(".")[-1] jinja2_extensions[namespace] = { name.split(".")[-1]: importlib.import_module(name) for finder, name, ispkg in jinja2_filters_modules } except ModuleNotFoundError: # Nothing to do, we may start without any filters/globals/tests pass return jinja2_extensions def jinja2environment(dir_=None): """Set up and return Jinja2 environment.""" if dir_ is None: dir_ = os.getcwd() # Ignore bandit false positive: B701:jinja2_autoescape_false # This env is not used to render content that is vulnerable to XSS. env = Environment( # nosec loader=ChoiceLoader([FileSystemLoader(dir_), PyModuleLoader()]), undefined=StrictUndefined, extensions=['jinja2.ext.do']) # Load Jinja2 filters using setuptools for scope, extensions in _load_jinja2_extensions().items(): for fname, module in extensions.items(): getattr(env, scope)[fname] = getattr(module, fname) # Load any custom Jinja2 filters, tests or globals in the workflow # definition directory # Example: a filter to pad integer values some fill character: # |(file WORKFLOW_DEFINITION_DIRECTORY/Jinja2/foo.py) # | #!/usr/bin/env python3 # | def foo( value, length, fillchar ): # | return str(value).rjust( int(length), str(fillchar) ) for namespace in ['filters', 'tests', 'globals']: nspdir = 'Jinja2' + namespace.capitalize() fdirs = [os.path.join(dir_, nspdir)] try: fdirs.append(os.path.join(os.environ['HOME'], '.cylc', nspdir)) except KeyError: # (Needed for tests/f/cylc-get-site-config/04-homeless.t!) LOG.warning(f"$HOME undefined: can't load ~/.cylc/{nspdir}") for fdir in fdirs: if os.path.isdir(fdir): sys.path.insert(1, os.path.abspath(fdir)) for name in glob(os.path.join(fdir, '*.py')): fname = os.path.splitext(os.path.basename(name))[0] # TODO - EXCEPTION HANDLING FOR LOADING CUSTOM FILTERS module = __import__(fname) envnsp = getattr(env, namespace) envnsp[fname] = getattr(module, fname) # Import WORKFLOW HOST USER ENVIRONMENT into template: # (Usage e.g.: {{environ['HOME']}}). env.globals['environ'] = os.environ env.globals['raise'] = raise_helper env.globals['assert'] = assert_helper # Add `CYLC_` environment variables to the global namespace. env.globals.update( get_cylc_env_vars() ) return env def get_error_lines( base_template_file: str, template_lines: t.List[str], ) -> t.Dict[str, t.List[str]]: """Extract exception lines from Jinja2 tracebacks. Returns: {filename: [exception_line, ...]} There may be multiple entries due to {% include %} statements. """ ret = {} for line in reversed(traceback.format_exc().splitlines()): match = TRACEBACK_LINENO.match(line) lines: t.List[str] = [] if match: filename = match.groupdict()['file'] lineno = int(match.groupdict()['line']) start_line = max(lineno - CONTEXT_LINES, 0) if filename in {'