diff --git a/.jscpd-report.md b/.jscpd-report.md new file mode 100644 index 00000000..61bb2eb3 --- /dev/null +++ b/.jscpd-report.md @@ -0,0 +1,659 @@ +# Duplication Analysis Report + +> Generated: 2026-04-07 | Tool: jscpd 4.0.8 | Threshold: 5.0% + +## Summary + +| Project | Files | Lines | Clones | Duplicated Lines | Percentage | +|---------|------:|------:|-------:|-----------------:|-----------:| +| babs | 77 | 12356 | 18 | 434 | 3.51% | + +> Duplication is within the 5.0% threshold for all projects. + +## Duplicate Clusters + +| C | Lines | %file | Difficulty | Strategy | Files | +|----|-------|-------|------------|----------------------------------------------------|------------------------------------------------------------------------| +| 1 | 86 | 50% | Moderate | Extract shared test fixture to conftest.py | test_generate_bidsapp_runscript.py / test_generate_submit_script.py | +| 2 | 41 | 100% | Moderate | Extract shared test fixture to conftest.py | eg_simbids_0-0-3_multiinput.yaml / config_simbids_multiinput.yaml | +| 3 | 34 | 20% | Moderate | Extract shared test fixture to conftest.py | test_generate_bidsapp_runscript.py / test_generate_submit_script.py | +| 4 | 34 | 7% | Easy | Extract local helper function | generate_bidsapp_runscript.py | +| 5 | 30 | 6% | Easy | Extract local helper function | generate_bidsapp_runscript.py | +| 6 | 28 | 14% | Easy | Consolidate repeated sections within this document | whats_new.md | +| 7 | 25 | 68% | Moderate | Extract shared test fixture to conftest.py | config_simbids.yaml / config_simbids_multiinput.yaml | +| 8 | 24 | 9% | Moderate | Extract test fixture or parametrize | test_check_setup.py | +| 9 | 23 | 58% | Moderate | Extract shared test fixture to conftest.py | config_simbids_multiinput.yaml / simbids_fmriprep-24-1-1_anatonly.yaml | +| 10 | 18 | 7% | Moderate | Extract shared test fixture to conftest.py | test_babs_workflow.py / test_update_input_data.py | +| 11 | 18 | 2% | Easy | Extract local helper function | cli.py | +| 12 | 18 | 2% | Easy | Extract local helper function | cli.py | +| 13 | 17 | 5% | Moderate | Extract shared test fixture to conftest.py | conftest.py / test_babs_workflow.py | +| 14 | 14 | 5% | Moderate | Extract test fixture or parametrize | test_input_datasets.py | +| 15 | 12 | 2% | Moderate | Extract test fixture or parametrize | test_utils.py | +| 16 | 11 | 6% | Moderate | Extract test fixture or parametrize | test_interaction.py | +| 17 | 10 | 6% | Easy | Extract test fixture or parametrize | test_interaction.py | +| 18 | 9 | 2% | Easy | Extract local helper function | check_setup.py | + +
+Cluster 1: [Moderate] `tests/test_generate_bidsapp_runscript.py` lines 13-98 ↔ `tests/test_generate_submit_script.py` lines 8-93 (86 lines 50% of file) + +**Files involved:** +- [`tests/test_generate_bidsapp_runscript.py` (lines 13-98)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_bidsapp_runscript.py#L13-L98) +- [`tests/test_generate_submit_script.py` (lines 8-93)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_submit_script.py#L8-L93) + +**Duplicated fragment:** +~~~python +read_yaml, +) + +input_datasets_prep = [ + { + 'name': 'bids', + 'path_in_babs': 'inputs/data/BIDS', + 'unzipped_path_containing_subject_dirs': 'inputs/data/BIDS', + 'is_zipped': False, + }, +] + +input_datasets_fmriprep_ingressed_anat = [ + { + 'name': 'freesurfer', + 'path_in_babs': 'inputs/data', + 'unzipped_path_containing_subject_dirs': 'inputs/data/freesurfer', + 'is_zipped': True, + }, + { + 'name': 'bids', + 'path_in_babs': 'inputs/data/BIDS', + 'unzipped_path_containing_subject_dirs': 'inputs/data/BIDS', + 'is_zipped': False, + }, +] + +input_datasets_xcpd = [ + { + 'name': 'fmriprep', +... (56 more lines) +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 2: [Moderate] `notebooks/eg_simbids_0-0-3_multiinput.yaml` lines 1-41 ↔ `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 1-45 (41 lines 100% of file) + +**Files involved:** +- [`notebooks/eg_simbids_0-0-3_multiinput.yaml` (lines 1-41)](https://github.com/PennLINC/babs/blob/main/notebooks/eg_simbids_0-0-3_multiinput.yaml#L1-L41) +- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 1-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L1-L45) + +**Duplicated fragment:** +~~~yaml +input_datasets: + BIDS: + required_files: + - "anat/*_T1w.nii*" + is_zipped: false + origin_url: "/test-temp/simbids" + path_in_babs: inputs/data/BIDS + fmriprep_anat: + is_zipped: true + origin_url: "ria+file:///test-temp/test_project/output_ria#~data" + path_in_babs: inputs/data/fmriprep_anat + +bids_app_args: + --bids-app: "fmriprep" + $SUBJECT_SELECTION_FLAG: "--participant-label" + --stop-on-first-crash: "" + -vv: "" + +all_results_in_one_zip: true +zip_foldernames: + fmriprep_anat: "24-1-1" + +singularity_args: + - --containall + - --writable-tmpfs + +cluster_resources: + interpreting_shell: "/bin/bash" + +script_preamble: | +... (11 more lines) +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 3: [Moderate] `tests/test_generate_bidsapp_runscript.py` lines 134-167 ↔ `tests/test_generate_submit_script.py` lines 111-144 (34 lines) + +**Files involved:** +- [`tests/test_generate_bidsapp_runscript.py` (lines 134-167)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_bidsapp_runscript.py#L134-L167) +- [`tests/test_generate_submit_script.py` (lines 111-144)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_submit_script.py#L111-L144) + +**Duplicated fragment:** +~~~python +with open(out_fn, 'w') as f: + f.write(script_content) + passed, status = run_shellcheck(str(out_fn)) + if not passed: + print(script_content) + assert passed, status + + +def run_shellcheck(script_path): + """Run shellcheck on a shell script string and return the result. + + Parameters + ---------- + script_path : str + The path to the shell script to check + + Returns + ------- + tuple + (bool, str) where bool indicates success (True) or failure (False), + and str contains shellcheck output + """ + + try: + # Run shellcheck on the temporary file + result = subprocess.run(['shellcheck', script_path], capture_output=True, text=True) + return result.returncode == 0, result.stdout + except subprocess.CalledProcessError as e: + return False, e.output + except Exception as e: +... (4 more lines) +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 4: [Easy] `babs/generate_bidsapp_runscript.py` lines 290-323 ↔ `babs/generate_bidsapp_runscript.py` lines 222-255 (34 lines) + +**Files involved:** +- [`babs/generate_bidsapp_runscript.py` (lines 290-323)](https://github.com/PennLINC/babs/blob/main/babs/generate_bidsapp_runscript.py#L290-L323) +- [`babs/generate_bidsapp_runscript.py` (lines 222-255)](https://github.com/PennLINC/babs/blob/main/babs/generate_bidsapp_runscript.py#L222-L255) + +**Duplicated fragment:** +~~~python +) + + else: # check on values: + if value in ('', None, 'Null', 'NULL'): # a flag, without value + cmds.append(str(key)) + else: # a flag with value + # check if it is a placeholder which needs to be replaced: + # e.g., `$BABS_TMPDIR` + if value.startswith('$BABS_'): + value = replace_placeholder_from_config(value) + + cmds.append(f'{key} {value}') + + # Ensure that subject_selection_flag is not None before returning + if subject_selection_flag is None: + subject_selection_flag = '--participant-label' + print( + "'$SUBJECT_SELECTION_FLAG' not found in `bids_app_args` section of the YAML file. " + 'Using `--participant-label` as the default subject selection flag.' + ) + + # The input dataset is always the first one in the list + bids_app_input_dir = input_datasets[0]['unzipped_path_containing_subject_dirs'] + + return ( + cmds, + subject_selection_flag, + flag_fs_license, + path_fs_license, + bids_app_input_dir, +... (4 more lines) +~~~ + +**Mediation** (Easy → **Moderate**): Refactor `bids_app_args_from_config` and `bids_app_args_from_config_pipeline` into a single function + +> These two functions (`bids_app_args_from_config` at line 155 and `bids_app_args_from_config_pipeline` at line 255) are nearly identical. The only difference is how `$SESSION_SELECTION_FLAG` is handled: the original always appends `{value} $sesid`, while the pipeline version conditionally checks `processing_level == 'session'`. The fix is to merge them into one function that accepts an optional `processing_level` parameter (defaulting to `'session'` for backward compatibility). This covers both Clusters 4 and 5. + +
+ +
+Cluster 6: [Easy] `docs/whats_new.md` lines 169-196 ↔ `docs/whats_new.md` lines 112-137 (28 lines) + +**Files involved:** +- [`docs/whats_new.md` (lines 169-196)](https://github.com/PennLINC/babs/blob/main/docs/whats_new.md#L169-L196) +- [`docs/whats_new.md` (lines 112-137)](https://github.com/PennLINC/babs/blob/main/docs/whats_new.md#L112-L137) + +**Duplicated fragment:** +~~~markdown +nnLINC/babs/pull/194) +* @singlesp made their first contribution in [#239](https://github.com/PennLINC/babs/pull/239) + +**Full Changelog**: [https://github.com/PennLINC/babs/compare/0.0.8...0.2.0](https://github.com/PennLINC/babs/compare/0.0.8...0.2.0) + + +## Version 0.1.0 + +### What's Changed + +#### Exciting New Features +* Convert CLIs to subcommands by @tsalo in [#210](https://github.com/PennLINC/babs/pull/210) +* Add a yml file for creating a mamba environment on an hpc by @mattcieslak in [#217](https://github.com/PennLINC/babs/pull/217) + +#### Bug Fixes +* [FIX] Output RIA path not found error by @tientong98 in [#178](https://github.com/PennLINC/babs/pull/178) +* Replace `pkg_resources` with `importlib.metadata` by @tientong98 in [#211](https://github.com/PennLINC/babs/pull/211) + +#### Other Changes +* add backoff strategy for job polling by @asmacdo in [#165](https://github.com/PennLINC/babs/pull/165) +* Introducing e2e slurm tests by @asmacdo in [#169](https://github.com/PennLINC/babs/pull/169) +* [DOCS] Add examples of --list_sub_file/--list-sub-file by @tientong98 in [#181](https://github.com/PennLINC/babs/pull/181) +* [DOCS] Add examples of --list_sub_file/--list-sub-file - Fixed rendering issues by @tientong98 in [#183](https://github.com/PennLINC/babs/pull/183) +* update installation instructions with method to provide OSF credentials by @B-Sevchik in [#186](https://github.com/PennLINC/babs/pull/186) +* Add participant selection flag to `babs-init` config yaml file by @tientong98 in [#187](https://github.com/PennLINC/babs/pull/187) +* Support SLURM array jobs in `babs-submit` by @tientong98 in [#188](https://github.com/PennLINC/babs/pull/188) +* Add a new docker build and fix CI tests by @mattcieslak in [#189](https://github.com/PennLINC/babs/pull/189) +* Restyle with ruff by @mattcieslak in [#190](https://github.com/PennLINC/babs/pull/190) +* Add back containerized slurm to the CI by @mattcieslak in [#191](https://github.com/PennLINC/babs/pull/191) +* Fix shellcheck by @mattcieslak in [#198](https://github.com/PennLINC/babs/pull/198) +... (5 more lines) +~~~ + +**Mediation** (Easy): Consolidate repeated sections within this document + +> Same content appears multiple times in one file. Merge into a single section and add internal cross-references. + +
+ +
+Cluster 7: [Moderate] `tests/e2e-slurm/container/config_simbids.yaml` lines 14-38 ↔ `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 17-45 (25 lines 68% of file) + +**Files involved:** +- [`tests/e2e-slurm/container/config_simbids.yaml` (lines 14-38)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids.yaml#L14-L38) +- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 17-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L17-L45) + +**Duplicated fragment:** +~~~yaml +: "" + +all_results_in_one_zip: true +zip_foldernames: + fmriprep_anat: "24-1-1" + +singularity_args: + - --containall + - --writable-tmpfs + +cluster_resources: + interpreting_shell: "/bin/bash" + +script_preamble: | + PATH=/opt/conda/envs/babs/bin:$PATH + +job_compute_space: "/tmp" + +alert_log_messages: + stdout: + - "Excessive topologic defect encountered" + - "Cannot allocate memory" + - "mris_curvature_stats: Could not open file" + - "Numerical result out of range" + - "fMRIPrep failed" +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 8: [Moderate] `tests/test_check_setup.py` lines 60-83 ↔ `tests/test_check_setup.py` lines 29-52 (24 lines) + +**Files involved:** +- [`tests/test_check_setup.py` (lines 60-83)](https://github.com/PennLINC/babs/blob/main/tests/test_check_setup.py#L60-L83) +- [`tests/test_check_setup.py` (lines 29-52)](https://github.com/PennLINC/babs/blob/main/tests/test_check_setup.py#L29-L52) + +**Duplicated fragment:** +~~~python +(tmp_path_factory, babs_project_sessionlevel, monkeypatch): + """Test that the input_shasum is correctly checked.""" + + project_root = babs_project_sessionlevel + + babs_proj = BABSCheckSetup(project_root) + # Make sure we have the dataset ID + babs_proj.wtf_key_info() + + # Mock read_yaml to avoid creating lock files + def mock_read_yaml(path, use_filelock=False): + import yaml + + with open(path) as file: + return yaml.safe_load(file) + + monkeypatch.setattr('babs.check_setup.read_yaml', mock_read_yaml) + + # Run check-setup without test job + babs_proj.babs_check_setup(submit_a_test_job=False) + + # make a change to the input ria + add_commit_to_ria( + f'ria+file://{babs_proj.output_ria_path}#{babs_proj.analysis_dataset_id}' +~~~ + +**Mediation** (Moderate): Extract test fixture or parametrize + +> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern. + +
+ +
+Cluster 9: [Moderate] `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 19-41 ↔ `tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml` lines 23-45 (23 lines 58% of file) + +**Files involved:** +- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 19-41)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L19-L41) +- [`tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml` (lines 23-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml#L23-L45) + +**Duplicated fragment:** +~~~yaml +all_results_in_one_zip: true +zip_foldernames: + fmriprep_anat: "24-1-1" + +singularity_args: + - --containall + - --writable-tmpfs + +cluster_resources: + interpreting_shell: "/bin/bash" + +script_preamble: | + PATH=/opt/conda/envs/babs/bin:$PATH + +job_compute_space: "/tmp" + +alert_log_messages: + stdout: + - "Excessive topologic defect encountered" + - "Cannot allocate memory" + - "mris_curvature_stats: Could not open file" + - "Numerical result out of range" + - "fMRIPrep failed" +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 10: [Moderate] `tests/test_babs_workflow.py` lines 45-62 ↔ `tests/test_update_input_data.py` lines 126-142 (18 lines) + +**Files involved:** +- [`tests/test_babs_workflow.py` (lines 45-62)](https://github.com/PennLINC/babs/blob/main/tests/test_babs_workflow.py#L45-L62) +- [`tests/test_update_input_data.py` (lines 126-142)](https://github.com/PennLINC/babs/blob/main/tests/test_update_input_data.py#L126-L142) + +**Duplicated fragment:** +~~~python +) + + # Preparation of env variable `TEMPLATEFLOW_HOME`: + os.environ['TEMPLATEFLOW_HOME'] = str(templateflow_home) + assert os.getenv('TEMPLATEFLOW_HOME') + + # Get the cli of `babs init`: + project_base = tmp_path_factory.mktemp('project') + project_root = project_base / 'my_babs_project' + container_name = 'simbids-0-0-3' + + # Use config_simbids.yaml instead of eg_fmriprep + config_simbids_path = get_config_simbids_path() + container_config = update_yaml_for_run( + project_base, + config_simbids_path.name, + { + 'BIDS': bids_data_singlesession +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 11: [Easy] `babs/cli.py` lines 309-326 ↔ `babs/cli.py` lines 228-244 (18 lines) + +**Files involved:** +- [`babs/cli.py` (lines 309-326)](https://github.com/PennLINC/babs/blob/main/babs/cli.py#L309-L326) +- [`babs/cli.py` (lines 228-244)](https://github.com/PennLINC/babs/blob/main/babs/cli.py#L228-L244) + +**Duplicated fragment:** +~~~python +, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + PathExists = partial(_path_exists, parser=parser) + parser.add_argument( + 'project_root', + metavar='PATH', + help=( + 'Absolute path to the root of BABS project. ' + "For example, '/path/to/my_BABS_project/' " + '(default is current working directory).' + ), + nargs='?', + default=Path.cwd(), + type=PathExists, + ) + + group +~~~ + +**Mediation** (Easy): Extract `_base_project_parser(description)` helper + +> Three subcommand parsers (`_parse_check_setup`, `_parse_submit`, `_parse_status`) all create a parser with `ArgumentDefaultsHelpFormatter`, `PathExists`, and the identical `project_root` positional argument. Extract a `_base_project_parser(description)` function that returns a parser pre-configured with `project_root`, then each `_parse_*` function calls it and adds its own extra arguments. This covers both Clusters 11 and 12. + +```python +def _base_project_parser(description): + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + PathExists = partial(_path_exists, parser=parser) + parser.add_argument( + 'project_root', metavar='PATH', + help=('Absolute path to the root of BABS project. ' + "For example, '/path/to/my_BABS_project/' " + '(default is current working directory).'), + nargs='?', default=Path.cwd(), type=PathExists, + ) + return parser +``` + +
+ +
+Cluster 13: [Moderate] `tests/conftest.py` lines 424-440 ↔ `tests/test_babs_workflow.py` lines 45-142 (17 lines) + +**Files involved:** +- [`tests/conftest.py` (lines 424-440)](https://github.com/PennLINC/babs/blob/main/tests/conftest.py#L424-L440) +- [`tests/test_babs_workflow.py` (lines 45-142)](https://github.com/PennLINC/babs/blob/main/tests/test_babs_workflow.py#L45-L142) + +**Duplicated fragment:** +~~~python +, '.datalad/config')) + + # Preparation of env variable `TEMPLATEFLOW_HOME`: + os.environ['TEMPLATEFLOW_HOME'] = str(templateflow_home) + assert os.getenv('TEMPLATEFLOW_HOME') + + # Get the cli of `babs init`: + project_base = tmp_path_factory.mktemp('project') + project_root = project_base / 'my_babs_project' + container_name = 'simbids-0-0-3' + + # Use config_simbids.yaml instead of eg_fmriprep + config_simbids_path = get_config_simbids_path() + container_config = update_yaml_for_run( + project_base, + config_simbids_path.name, + {'BIDS': bids_data +~~~ + +**Mediation** (Moderate): Extract shared test fixture to conftest.py + +> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module. + +
+ +
+Cluster 14: [Moderate] `tests/test_input_datasets.py` lines 250-263 ↔ `tests/test_input_datasets.py` lines 108-121 (14 lines) + +**Files involved:** +- [`tests/test_input_datasets.py` (lines 250-263)](https://github.com/PennLINC/babs/blob/main/tests/test_input_datasets.py#L250-L263) +- [`tests/test_input_datasets.py` (lines 108-121)](https://github.com/PennLINC/babs/blob/main/tests/test_input_datasets.py#L108-L121) + +**Duplicated fragment:** +~~~python +if session_type == 'nosessions': + origin_url = fmriprep_noses_derivative_files_zipped_at_subject + elif session_type == 'sessions': + if processing_level == 'session': + origin_url = fmriprep_multises_derivative_files_zipped_at_session + else: + origin_url = fmriprep_multises_derivative_files_zipped_at_subject + else: + raise ValueError(f'Invalid session type: {session_type}') + + clone_path = tmp_path_factory.mktemp('cloned_input_dataset') + dlapi.clone(origin_url, path=clone_path) + + input_dataset +~~~ + +**Mediation** (Moderate): Extract test fixture or parametrize + +> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern. + +
+ +
+Cluster 15: [Moderate] `tests/test_utils.py` lines 454-465 ↔ `tests/test_utils.py` lines 309-320 (12 lines) + +**Files involved:** +- [`tests/test_utils.py` (lines 454-465)](https://github.com/PennLINC/babs/blob/main/tests/test_utils.py#L454-L465) +- [`tests/test_utils.py` (lines 309-320)](https://github.com/PennLINC/babs/blob/main/tests/test_utils.py#L309-L320) + +**Duplicated fragment:** +~~~python +, 'first_run'], + 'has_results': [False, False, False, True], + # Fields for tracking: + 'needs_resubmit': [False, False, False, False], + 'is_failed': [pd.NA, pd.NA, pd.NA, False], + 'log_filename': [pd.NA, pd.NA, pd.NA, 'test_array_job.log'], + 'last_line_stdout_file': [pd.NA, pd.NA, pd.NA, 'SUCCESS'], + 'alert_message': [pd.NA, pd.NA, pd.NA, pd.NA], + } + ) + + new_status_df +~~~ + +**Mediation** (Moderate): Extract test fixture or parametrize + +> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern. + +
+ +
+Cluster 16: [Moderate] `tests/test_interaction.py` lines 77-87 ↔ `tests/test_interaction.py` lines 56-67 (11 lines) + +**Files involved:** +- [`tests/test_interaction.py` (lines 77-87)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L77-L87) +- [`tests/test_interaction.py` (lines 56-67)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L56-L67) + +**Duplicated fragment:** +~~~python +], + 'time_used': ['0:01'], + 'time_limit': ['5-00:00:00'], + 'nodes': [1], + 'cpus': [1], + 'partition': ['normal'], + 'name': ['test_array_job'], + } + ) + monkeypatch.setattr(babs_proj, 'get_currently_running_jobs_df', lambda: running_df) + monkeypatch +~~~ + +**Mediation** (Moderate): Extract test fixture or parametrize + +> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern. + +
+ +
+Cluster 17: [Easy] `tests/test_interaction.py` lines 106-115 ↔ `tests/test_interaction.py` lines 54-63 (10 lines) + +**Files involved:** +- [`tests/test_interaction.py` (lines 106-115)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L106-L115) +- [`tests/test_interaction.py` (lines 54-63)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L54-L63) + +**Duplicated fragment:** +~~~python +], + 'task_id': [1], + 'state': ['R'], + 'time_used': ['0:01'], + 'time_limit': ['5-00:00:00'], + 'nodes': [1], + 'cpus': [1], + 'partition': ['normal'], + 'name': ['test_array_job'], + 'sub_id' +~~~ + +**Mediation** (Easy): Extract test fixture or parametrize + +> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern. + +
+ +
+Cluster 18: [Easy] `babs/check_setup.py` lines 343-351 ↔ `babs/check_setup.py` lines 293-302 (9 lines) + +**Files involved:** +- [`babs/check_setup.py` (lines 343-351)](https://github.com/PennLINC/babs/blob/main/babs/check_setup.py#L343-L351) +- [`babs/check_setup.py` (lines 293-302)](https://github.com/PennLINC/babs/blob/main/babs/check_setup.py#L293-L302) + +**Duplicated fragment:** +~~~python +sleeptime = 0 + while not new_job_status.empty: + sleeptime += 1 + time.sleep(sleeptime) + job_status = new_job_status.copy() + new_job_status = request_all_job_status(self.queue, job_id) + + if not job_status.shape[0] == 1: + raise Exception(f'Expected 1 job, got {job_status.shape[0]}' +~~~ + +**Mediation** (Easy): Extract `_wait_for_job_completion(queue, job_id)` helper + +> The "submit job → poll until done → validate single row" pattern at lines 292-302 and 340-351 is identical. Extract a helper like `_wait_for_job_completion(queue, job_id)` that returns the final `job_status` DataFrame. Both the pipeline-step loop and single-container branch would call it. + +