def main(parser, options, reg): suite, suiterc = parse_suite_arg(options, reg) if options.markup: prefix = '!cylc!' else: prefix = '' config = SuiteConfig( suite, suiterc, options, load_template_vars(options.templatevars, options.templatevars_file)) if options.tasks: for task in config.get_task_name_list(): print(prefix + task) elif options.alltasks: for task in config.get_task_name_list(): items = ['[runtime][' + task + ']' + i for i in options.item] print(prefix + task, end=' ') config.pcfg.idump(items, options.sparse, options.pnative, prefix, options.oneline, none_str=options.none_str) else: config.pcfg.idump(options.item, options.sparse, options.pnative, prefix, options.oneline, none_str=options.none_str)
def _test(cp_tz, utc_mode, expected, expected_warnings=0): set_utc_mode(utc_mode) mock_config = Mock() mock_config.cfg = {'cylc': {'cycle point time zone': cp_tz['suite']}} mock_config.options.cycle_point_tz = cp_tz['stored'] SuiteConfig.process_cycle_point_tz(mock_config) assert mock_config.cfg['cylc']['cycle point time zone'] == expected assert len(caplog.record_tuples) == expected_warnings caplog.clear()
def main(_, options, *args): # suite name or file path suite, flow_file = parse_suite_arg(options, args[0]) # extract task host platforms from the suite config = SuiteConfig( suite, flow_file, options, load_template_vars(options.templatevars, options.templatevars_file)) platforms = { config.get_config(['runtime', name, 'platform']) for name in config.get_namespace_list('all tasks') } - {None, 'localhost'} # When "suite run hosts" are formalised as "flow platforms" # we can substitute `localhost` for this, in the mean time # we will have to assume that flow hosts are configured correctly. if not platforms: sys.exit(0) verbose = cylc.flow.flags.verbose # get the cylc version on each platform versions = {} for platform_name in sorted(platforms): platform = get_platform(platform_name) cmd = construct_platform_ssh_cmd(['version'], platform) if verbose: print(cmd) proc = procopen(cmd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() out = out.decode() err = err.decode() if proc.wait() == 0: if verbose: print(" %s" % out) versions[platform_name] = out.strip() else: versions[platform_name] = f'ERROR: {err.strip()}' # report results max_len = max((len(platform_name) for platform_name in platforms)) print(f'{"platform".rjust(max_len)}: cylc version') print('-' * (max_len + 14)) for platform_name, result in versions.items(): print(f'{platform_name.rjust(max_len)}: {result}') if all((version == CYLC_VERSION for version in versions.values())): exit = 0 elif options.error: exit = 1 else: exit = 0 sys.exit(exit)
def test_process_runahead_limit(cfg_scheduling, valid, cycling_mode): is_integer_mode = cfg_scheduling['cycling mode'] == 'integer' mock_config = Mock() mock_config.cycling_type = cycling_mode(integer=is_integer_mode) mock_config.cfg = {'scheduling': cfg_scheduling} if valid: SuiteConfig.process_runahead_limit(mock_config) else: with pytest.raises(SuiteConfigError) as exc: SuiteConfig.process_runahead_limit(mock_config) assert "bad runahead limit" in str(exc.value).lower()
def test_missing_initial_cycle_point(): """Test that validation fails when the initial cycle point is missing for datetime cycling""" mocked_config = Mock() mocked_config.cfg = { 'scheduling': { 'cycling mode': None, 'initial cycle point': None } } with pytest.raises(SuiteConfigError) as exc: SuiteConfig.process_initial_cycle_point(mocked_config) assert "This suite requires an initial cycle point" in str(exc.value)
def test_integer_cycling_default_initial_point(cycling_mode): """Test that the initial cycle point defaults to 1 for integer cycling mode.""" cycling_mode(integer=True) # This is a pytest fixture; sets cycling mode mocked_config = Mock() mocked_config.cfg = { 'scheduling': { 'cycling mode': 'integer', 'initial cycle point': None } } SuiteConfig.process_initial_cycle_point(mocked_config) assert mocked_config.cfg['scheduling']['initial cycle point'] == '1' assert mocked_config.initial_point == loader.get_point(1)
def _test(utc_mode, expected, expected_warnings=0): mock_glbl_cfg( 'cylc.flow.config.glbl_cfg', f''' [cylc] UTC mode = {utc_mode['glbl']} ''') mock_config = Mock() mock_config.cfg = {'cylc': {'UTC mode': utc_mode['suite']}} mock_config.options.utc_mode = utc_mode['stored'] SuiteConfig.process_utc_mode(mock_config) assert mock_config.cfg['cylc']['UTC mode'] is expected assert get_utc_mode() is expected assert len(caplog.record_tuples) == expected_warnings caplog.clear()
def test_xfunction_attribute_error(self, mock_glbl_cfg): """Test for error when a xtrigger function cannot be imported.""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''') with TemporaryDirectory() as temp_dir: python_dir = Path(os.path.join(temp_dir, "lib", "python")) python_dir.mkdir(parents=True) capybara_file = python_dir / "capybara.py" with capybara_file.open(mode="w") as f: # NB: we are not returning a lambda, instead we have a scalar f.write("""toucan = lambda: True""") f.flush() flow_file = Path(temp_dir, "flow.cylc") with flow_file.open(mode="w") as f: f.write(""" [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] oopsie = capybara() [[graph]] R1 = '@oopsie => qux' """) f.flush() with pytest.raises(AttributeError) as excinfo: SuiteConfig(suite="capybara_suite", fpath=f.name) assert "not found" in str(excinfo.value)
def test_xfunction_imports( self, mock_glbl_cfg: Fixture, tmp_path: Path, xtrigger_mgr: XtriggerManager): """Test for a suite 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 / SuiteFiles.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) suite_config = SuiteConfig( suite="name_a_tree", fpath=flow_file, options=Mock(spec=[]), xtrigger_mgr=xtrigger_mgr ) assert 'tree' in suite_config.xtrigger_mgr.functx_map
def test_family_inheritance_and_quotes(self, mock_glbl_cfg): """Test that inheritance does not ignore items, if not all quoted. For example: inherit = 'MAINFAM<major, minor>', SOMEFAM inherit = 'BIGFAM', SOMEFAM See bug #2700 for more/ """ mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''') template_vars = {} for content in get_test_inheritance_quotes(): with NamedTemporaryFile() as tf: tf.write(content) tf.flush() config = SuiteConfig('test', tf.name, template_vars=template_vars) assert 'goodbye_0_major1_minor10' in \ (config.runtime['descendants'] ['MAINFAM_major1_minor10']) assert 'goodbye_0_major1_minor10' in \ config.runtime['descendants']['SOMEFAM']
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 / SuiteFiles.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(ValueError) as excinfo: SuiteConfig(suite="suite_with_not_callable", fpath=flow_file, options=Mock(spec=[])) assert "callable" 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 / SuiteFiles.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(AttributeError) as excinfo: SuiteConfig(suite="capybara_suite", fpath=flow_file, options=Mock(spec=[])) assert "not found" in str(excinfo.value)
def test_xfunction_imports(self, mock_glbl_cfg): """Test for a suite configuration with valid xtriggers""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''') with TemporaryDirectory() as temp_dir: python_dir = Path(os.path.join(temp_dir, "lib", "python")) python_dir.mkdir(parents=True) name_a_tree_file = python_dir / "name_a_tree.py" with name_a_tree_file.open(mode="w") as f: # NB: we are not returning a lambda, instead we have a scalar f.write("""name_a_tree = lambda: 'jacaranda'""") f.flush() flow_file = Path(temp_dir, "flow.cylc") with flow_file.open(mode="w") as f: f.write(""" [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] tree = name_a_tree() [[graph]] R1 = '@tree => qux' """) f.flush() suite_config = SuiteConfig(suite="name_a_tree", fpath=f.name) config = suite_config assert 'tree' in config.xtrigger_mgr.functx_map
def test_xfunction_not_callable(self, mock_glbl_cfg): """Test for error when a xtrigger function is not callable.""" mock_glbl_cfg( 'cylc.flow.platforms.glbl_cfg', ''' [platforms] [[localhost]] hosts = localhost ''') with TemporaryDirectory() as temp_dir: python_dir = Path(os.path.join(temp_dir, "lib", "python")) python_dir.mkdir(parents=True) not_callable_file = python_dir / "not_callable.py" with not_callable_file.open(mode="w") as f: # NB: we are not returning a lambda, instead we have a scalar f.write("""not_callable = 42""") f.flush() flow_file = Path(temp_dir, "flow.cylc") with flow_file.open(mode="w") as f: f.write(""" [scheduling] initial cycle point = 2018-01-01 [[xtriggers]] oopsie = not_callable() [[graph]] R1 = '@oopsie => qux' """) f.flush() with pytest.raises(ValueError) as excinfo: SuiteConfig(suite="suite_with_not_callable", fpath=f.name) assert "callable" in str(excinfo.value)
def test_queue_config_not_used_not_defined(caplog, tmp_path): """Test task not defined vs no used, in queue config.""" flow_file_content = """ [scheduling] [[queues]] [[[q1]]] members = foo [[[q2]]] members = bar [[dependencies]] # foo and bar not used graph = "beef => wellington" [runtime] [[beef]] [[wellington]] [[foo]] # bar not even defined """ flow_file = tmp_path / "flow.cylc" flow_file.write_text(flow_file_content) SuiteConfig(suite="qtest", fpath=flow_file.absolute()) log = caplog.messages[0].split('\n') assert log[0] == "Queue configuration warnings:" assert log[1] == "+ q1: ignoring foo (task not used in the graph)" assert log[2] == "+ q2: ignoring bar (task not defined)"
def get_config(suite, opts, template_vars=None): """Return a SuiteConfig object for the provided reg / path.""" try: suiterc = get_suite_rc(suite) except SuiteServiceFileError: # could not find suite, assume we have been given a path instead suiterc = suite suite = 'test' return SuiteConfig(suite, suiterc, opts, template_vars=template_vars)
def main(parser, options, *args): suite1, suite1rc = parse_suite_arg(options, args[0]) suite2, suite2rc = parse_suite_arg(options, args[1]) if suite1 == suite2: parser.error("You can't diff a single suite.") print("Parsing %s (%s)" % (suite1, suite1rc)) template_vars = load_template_vars( options.templatevars, options.templatevars_file) config1 = SuiteConfig(suite1, suite1rc, options, template_vars).cfg print("Parsing %s (%s)" % (suite2, suite2rc)) config2 = SuiteConfig( suite2, suite2rc, options, template_vars, is_reload=True).cfg if config1 == config2: print("Suite definitions %s and %s are identical" % (suite1, suite2)) sys.exit(0) print("Suite definitions %s and %s differ" % (suite1, suite2)) suite1_only = {} suite2_only = {} diff_1_2 = {} diffdict(config1, config2, suite1_only, suite2_only, diff_1_2) if n_oone > 0: print() msg = str(n_oone) + ' items only in ' + suite1 + ' (<)' print(msg) prdict(suite1_only, '<', nested=options.nested) if n_otwo > 0: print() msg = str(n_otwo) + ' items only in ' + suite2 + ' (>)' print(msg) prdict(suite2_only, '>', nested=options.nested) if n_diff > 0: print() msg = (str(n_diff) + ' common items differ ' + suite1 + '(<) ' + suite2 + '(>)') print(msg) prdict(diff_1_2, '', diff=True, nested=options.nested)
def test_process_icp( scheduling_cfg: Dict[str, Any], expected_icp: Optional[str], expected_opt_icp: Optional[str], expected_err: Optional[Tuple[Type[Exception], str]], monkeypatch: Fixture, cycling_mode: Fixture): """Test SuiteConfig.process_initial_cycle_point(). "now" is assumed to be 2005-01-02T06:15+0530 Params: scheduling_cfg: 'scheduling' section of workflow config. expected_icp: The expected icp value that gets set. expected_opt_icp: The expected value of options.icp that gets set (this gets stored in the workflow DB). expected_err: Exception class expected to be raised plus the message. """ int_cycling_mode = True if scheduling_cfg['cycling mode'] == loader.ISO8601_CYCLING_TYPE: int_cycling_mode = False iso8601.init(time_zone="+0530") cycling_mode(integer=int_cycling_mode) mocked_config = Mock() mocked_config.cfg = { 'scheduling': scheduling_cfg } mocked_config.options.icp = None 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: SuiteConfig.process_initial_cycle_point(mocked_config) assert msg in str(exc.value) else: SuiteConfig.process_initial_cycle_point(mocked_config) assert mocked_config.cfg[ 'scheduling']['initial cycle point'] == expected_icp assert str(mocked_config.initial_point) == expected_icp opt_icp = mocked_config.options.icp if opt_icp is not None: opt_icp = str(loader.get_point(opt_icp).standardise()) assert opt_icp == expected_opt_icp
def main(parser, options, *args): suite1_name, suite1_config = parse_suite_arg(options, args[0]) suite2_name, suite2_config = parse_suite_arg(options, args[1]) if suite1_name == suite2_name: parser.error("You can't diff a single suite.") print(f"Parsing {suite1_name} ({suite1_config})") template_vars = load_template_vars(options.templatevars, options.templatevars_file) config1 = SuiteConfig(suite1_name, suite1_config, options, template_vars).cfg print(f"Parsing {suite2_name} ({suite2_config})") config2 = SuiteConfig(suite2_name, suite2_config, options, template_vars, is_reload=True).cfg if config1 == config2: print(f"Suite definitions {suite1_name} and {suite2_name} are " f"identical") sys.exit(0) print(f"Suite definitions {suite1_name} and {suite2_name} differ") suite1_only = {} suite2_only = {} diff_1_2 = {} diffdict(config1, config2, suite1_only, suite2_only, diff_1_2) if n_oone > 0: print(f'\n{n_oone} items only in {suite1_name} (<)') prdict(suite1_only, '<', nested=options.nested) if n_otwo > 0: print(f'\n{n_otwo} items only in {suite2_name} (>)') prdict(suite2_only, '>', nested=options.nested) if n_diff > 0: print(f'\n{n_diff} common items differ {suite1_name}(<) ' f'{suite2_name}(>)') prdict(diff_1_2, '', diff=True, nested=options.nested)
def test_valid_rsync_includes_returns_correct_list(tmp_path): """Test that the rsync includes in the correct """ flow_cylc_content = """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/, file1, file2 allow implicit tasks = True """ flow_cylc = tmp_path.joinpath(SuiteFiles.FLOW_FILE) flow_cylc.write_text(flow_cylc_content) config = SuiteConfig(suite="rsynctest", fpath=flow_cylc, options=Mock(spec=[])) rsync_includes = SuiteConfig.get_validated_rsync_includes(config) assert rsync_includes == ['dir/', 'dir2/', 'file1', 'file2']
def test_process_startcp(startcp: Optional[str], expected: str, monkeypatch: Fixture, cycling_mode: Fixture): """Test SuiteConfig.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: startcp: The start cycle point given by cli option. expected: The expected startcp value that gets set. """ iso8601.init(time_zone="+0530") cycling_mode(integer=False) mocked_config = Mock(initial_point='18990501T0000+0530') mocked_config.options.startcp = startcp monkeypatch.setattr('cylc.flow.config.get_current_time_string', lambda: '20050102T0615+0530') SuiteConfig.process_start_cycle_point(mocked_config) assert str(mocked_config.start_point) == expected
def test_valid_rsync_includes_returns_correct_list(): """Test that the rsync includes in the correct """ flow_cylc_content = """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/, file1, file2 """ with TemporaryDirectory() as temp_dir: flow_cylc = Path(temp_dir, "flow.cylc") with flow_cylc.open(mode="w") as f: f.write(flow_cylc_content) f.flush() config = SuiteConfig(suite="rsynctest", fpath=flow_cylc) rsync_includes = SuiteConfig.get_validated_rsync_includes(config) assert rsync_includes == ['dir/', 'dir2/', 'file1', 'file2']
def create_suite_config(workflow_directory: Path, suite_name: str, suiterc_content: str) -> SuiteConfig: """Create a SuiteConfig object from a suiterc content. Args: workflow_directory (Path): workflow base directory suite_name (str): suite name suiterc_content (str): suiterc content """ suite_rc = Path(workflow_directory, "suite.rc") with suite_rc.open(mode="w") as f: f.write(suiterc_content) f.flush() return SuiteConfig(suite=suite_name, fpath=f.name)
def test_process_fcp(scheduling_cfg: dict, options_fcp: Optional[str], expected_fcp: Optional[str], expected_err: Optional[Tuple[Type[Exception], str]], cycling_mode: Fixture): """Test SuiteConfig.process_final_cycle_point(). Params: 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. """ if scheduling_cfg['cycling mode'] == loader.ISO8601_CYCLING_TYPE: iso8601.init(time_zone="+0530") cycling_mode(integer=False) else: cycling_mode(integer=True) mocked_config = Mock(cycling_type=scheduling_cfg['cycling mode']) mocked_config.cfg = { 'scheduling': scheduling_cfg } mocked_config.initial_point = loader.get_point( scheduling_cfg['initial cycle point']).standardise() mocked_config.final_point = None mocked_config.options.fcp = options_fcp if expected_err: err, msg = expected_err with pytest.raises(err) as exc: SuiteConfig.process_final_cycle_point(mocked_config) assert msg in str(exc.value) else: SuiteConfig.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)
def create_task_proxy(task_name: str, suite_config: SuiteConfig, is_startup=False) -> TaskProxy: """Create a Task Proxy based on a TaskDef loaded from the SuiteConfig. Args: task_name (str): task name suite_config (SuiteConfig): SuiteConfig object that holds task definitions is_startup (bool): whether we are starting the workflow or not """ task_def = suite_config.get_taskdef(task_name) return TaskProxy(tdef=task_def, start_point=suite_config.start_point, is_startup=is_startup)
def test_prelim_process_graph( scheduling_cfg: Dict[str, Any], scheduling_expected: Optional[Dict[str, Any]], expected_err: Optional[Tuple[Type[Exception], str]]): """Test SuiteConfig.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 = Mock(cfg={ 'scheduling': scheduling_cfg }) if expected_err: err, msg = expected_err with pytest.raises(err) as exc: SuiteConfig.prelim_process_graph(mock_config) assert msg in str(exc.value) else: SuiteConfig.prelim_process_graph(mock_config) assert mock_config.cfg['scheduling'] == scheduling_expected
def test_rsync_includes_will_not_accept_sub_directories(tmp_path): flow_cylc_content = """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/subdir2/, file1, file2 """ flow_cylc = tmp_path.joinpath(SuiteFiles.FLOW_FILE) flow_cylc.write_text(flow_cylc_content) with pytest.raises(SuiteConfigError) as exc: SuiteConfig(suite="rsynctest", fpath=flow_cylc, options=Mock(spec=[])) assert "Directories can only be from the top level" in str(exc.value)
def test_rsync_includes_will_not_accept_sub_directories(): flow_cylc_content = """ [scheduling] initial cycle point = 2020-01-01 [[dependencies]] graph = "blah => deeblah" [scheduler] install = dir/, dir2/subdir2/, file1, file2 """ with TemporaryDirectory() as temp_dir: flow_cylc = Path(temp_dir, "flow.cylc") with flow_cylc.open(mode="w") as f: f.write(flow_cylc_content) f.flush() with pytest.raises(SuiteConfigError) as exc: SuiteConfig(suite="rsynctest", fpath=flow_cylc) assert "Directories can only be from the top level" in str(exc.value)
def main(parser, options, reg=None): if options.print_hierarchy: print("\n".join(get_config_file_hierarchy(reg))) return if reg is None: glbl_cfg().idump(options.item, sparse=options.sparse, oneline=options.oneline, none_str=options.none_str) return suite, flow_file = parse_suite_arg(options, reg) config = SuiteConfig( suite, flow_file, options, load_template_vars(options.templatevars, options.templatevars_file)) config.pcfg.idump(options.item, options.sparse, oneline=options.oneline, none_str=options.none_str)
def test_check_circular(opt, monkeypatch, caplog, tmp_path): """Test SuiteConfig._check_circular().""" # ----- Setup ----- caplog.set_level(logging.WARNING, CYLC_LOG) options = Mock(spec=[], is_validate=True) if opt: setattr(options, opt, True) flow_config = """ [scheduling] cycling mode = integer [[graph]] R1 = "a => b => c => d => e => a" [runtime] [[a, b, c, d, e]] script = True """ flow_file = tmp_path.joinpath(SuiteFiles.FLOW_FILE) flow_file.write_text(flow_config) def SuiteConfig__assert_err_raised(): with pytest.raises(SuiteConfigError) as exc: SuiteConfig(suite='circular', fpath=flow_file, options=options) assert "circular edges detected" in str(exc.value) # ----- The actual test ----- SuiteConfig__assert_err_raised() # Now artificially lower the limit and re-test: monkeypatch.setattr('cylc.flow.config.SuiteConfig.CHECK_CIRCULAR_LIMIT', 4) if opt != 'check_circular': # Will no longer raise SuiteConfig(suite='circular', fpath=flow_file, options=options) msg = "will not check graph for circular dependencies" assert msg in caplog.text else: SuiteConfig__assert_err_raised()