diff --git a/CHANGES.md b/CHANGES.md index 92c6642ff11..c4847b84a78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -78,6 +78,10 @@ Remove obsolete Cylc 7 `[scheduling]spawn to max active cycle points` config. safer by preventing cleaning of dirs that contain more than one workflow run dir (use `--force` to override this safeguard). +[#4362](https://github.com/cylc/cylc-flow/pull/4362) - +When using `cylc clean` on a sequential run directory, remove the `runN` symlink +if it points to the removed directory. + ------------------------------------------------------------------------------- ## __cylc-8.0b2 (Released 2021-07-28)__ diff --git a/cylc/flow/workflow_files.py b/cylc/flow/workflow_files.py index f1874e8ab0a..27c33698df1 100644 --- a/cylc/flow/workflow_files.py +++ b/cylc/flow/workflow_files.py @@ -747,6 +747,15 @@ def clean(reg: str, run_dir: Path, rm_dirs: Optional[Set[str]] = None) -> None: # Remove empty parents of symlink target up to /cylc-run/ remove_empty_parents(target, Path(reg, symlink)) + # Remove `runN` symlink if it's now broken + runN = run_dir.parent / WorkflowFiles.RUN_N + if ( + runN.is_symlink() and + not run_dir.exists() and + os.readlink(str(runN)) == run_dir.name + ): + runN.unlink() + def get_symlink_dirs(reg: str, run_dir: Union[Path, str]) -> Dict[str, Path]: """Return the standard symlink dirs and their targets if they exist in diff --git a/tests/unit/test_workflow_files.py b/tests/unit/test_workflow_files.py index 07accb518b2..0e34c10a133 100644 --- a/tests/unit/test_workflow_files.py +++ b/tests/unit/test_workflow_files.py @@ -19,6 +19,7 @@ import os from pathlib import Path import pytest +import re import shutil from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union from unittest import mock @@ -40,6 +41,7 @@ _remote_clean_cmd, check_flow_file, check_nested_run_dirs, + clean, get_rsync_rund_cmd, get_symlink_dirs, is_installed, @@ -1523,3 +1525,32 @@ def test_get_rsync_rund_cmd(tmp_run_dir: Callable): '--exclude=log', '--exclude=work', '--exclude=share', '--exclude=_cylc-install', '--exclude=.service', 'blah/', f'{cylc_run_dir}/'] + + +@pytest.mark.parametrize( + 'expect, dirs', + [ + (['run1'], ['run1', 'run2']), + (['run1', 'run11'], ['run1', 'run11', 'run2']), + (['run1200'], ['run1200', 'run1201']), + (['foo'], ['foo', 'bar']), + ] +) +def test_delete_runN(tmp_path, expect, dirs): + """It deletes the runN symlink. + """ + for dir_ in dirs: + (tmp_path / dir_).mkdir() + if re.findall('run\d*', dirs[-1]): + (Path(tmp_path / 'runN')).symlink_to(dirs[-1]) + clean(str(tmp_path.name) + '/' + dirs[-1], tmp_path / dirs[-1]) + assert sorted([i.stem for i in tmp_path.glob('*')]) == sorted(expect) + + +def test_delete_runN_skipif_cleanedrun_not_runN(tmp_path): + """It doesn't delete the symlink dir to be cleaned is not runN""" + for folder in ['run1', 'run2']: + (tmp_path / folder).mkdir() + (tmp_path / 'runN').symlink_to(tmp_path / 'run2') + clean(str(tmp_path.name) + '/' + 'run1', tmp_path / 'run1') + assert sorted([i.stem for i in tmp_path.glob('*')]) == ['run2', 'runN']