|
| 1 | +"""Clone engine placement contracts; native startup is a separate offline audit. |
| 2 | +
|
| 3 | +No inference, live Job, image copies, or installed runtime is needed by these |
| 4 | +regressions. The compiler tests exercise its real selected-dependency producer. |
| 5 | +""" |
| 6 | +from pathlib import Path |
| 7 | + |
| 8 | +import pytest |
| 9 | + |
| 10 | +from test_ngs_native_components import native |
| 11 | + |
| 12 | + |
| 13 | +HOME_KEYS = ('BMS_WF_CLONE_NXF_HOME', 'NXF_HOME', 'BMS_NEXTFLOW_HOME', 'BMS_DATA') |
| 14 | + |
| 15 | + |
| 16 | +@pytest.fixture |
| 17 | +def homes(monkeypatch, tmp_path): |
| 18 | + monkeypatch.setenv('HOME', str(tmp_path / 'home')) |
| 19 | + for key in HOME_KEYS: |
| 20 | + monkeypatch.delenv(key, raising=False) |
| 21 | + return tmp_path |
| 22 | + |
| 23 | + |
| 24 | +@pytest.mark.parametrize('winner', range(6)) |
| 25 | +def test_engine_home_precedence_and_dependency_binding(native, homes, monkeypatch, winner): |
| 26 | + from scripts.validate_wf_clone_runtime import resolve_nextflow_home |
| 27 | + |
| 28 | + values = [homes / name for name in ('explicit', 'clone', 'launcher', 'managed', 'data')] |
| 29 | + for index, key in enumerate(HOME_KEYS, 1): |
| 30 | + if index >= winner: |
| 31 | + monkeypatch.setenv(key, str(values[index])) |
| 32 | + selected = str(values[0]) if winner == 0 else None |
| 33 | + expected = (values[winner] if winner < 4 else |
| 34 | + values[4] / 'nextflow' if winner == 4 else homes / 'home/.nextflow') |
| 35 | + assert resolve_nextflow_home(selected) == expected |
| 36 | + params = {'wf_clone_nxf_home': selected} if selected else {} |
| 37 | + _, paths = native.ngs_clone_runtime_dependencies(params) |
| 38 | + jars = [path for key, path in paths.items() if key.endswith('-one.jar')] |
| 39 | + assert jars == [expected / 'framework/25.10.0/nextflow-25.10.0-one.jar'] |
| 40 | + |
| 41 | + |
| 42 | +def test_explicit_relocated_home_beats_inherited_engine(native, homes, monkeypatch): |
| 43 | + from scripts.validate_wf_clone_runtime import resolve_nextflow_home |
| 44 | + |
| 45 | + monkeypatch.setenv('NXF_HOME', str(homes / 'outer')) |
| 46 | + monkeypatch.setenv('NXF_VER', '25.10.1') |
| 47 | + selected = homes / 'relocated runtime/nxf-home' |
| 48 | + assert resolve_nextflow_home(str(selected)) == selected |
| 49 | + _, paths = native.ngs_clone_runtime_dependencies({'wf_clone_nxf_home': str(selected)}) |
| 50 | + jar = next(path for key, path in paths.items() if key.endswith('-one.jar')) |
| 51 | + assert jar == selected / 'framework/25.10.0/nextflow-25.10.0-one.jar' |
| 52 | + assert not selected.exists() # Resolution neither installs nor creates a cache. |
| 53 | + |
| 54 | + |
| 55 | +@pytest.mark.parametrize('assembly', [True, False]) |
| 56 | +@pytest.mark.parametrize('explicit', [True, False]) |
| 57 | +def test_normal_compiler_binds_only_selected_clone_engine(native, homes, monkeypatch, assembly, explicit): |
| 58 | + from services.nextflow import compile_nextflow_invocation |
| 59 | + |
| 60 | + monkeypatch.setenv('NXF_HOME', str(homes / 'launcher')) |
| 61 | + monkeypatch.setenv('NXF_VER', '25.10.1') |
| 62 | + monkeypatch.setenv('NXF_DIST', str(homes / 'outer-only-framework')) |
| 63 | + params = {'ont_workflow_id': 'ont_construct_screening', 'ont_input_mode': 'fastq', |
| 64 | + 'fastq_path': str(homes / 'reads.fastq'), |
| 65 | + 'reference_fasta': str(homes / 'reference.fa'), |
| 66 | + 'run_assembly': assembly, 'run_fastq_qc': True} |
| 67 | + if explicit: |
| 68 | + params['wf_clone_nxf_home'] = str(homes / 'relocated') |
| 69 | + invocation = compile_nextflow_invocation('nanopore', 'construct_screening', params, |
| 70 | + str(homes / 'results'), job_id='engine-selection') |
| 71 | + assert invocation.execution_plan.complete, invocation.execution_plan.blockers |
| 72 | + bound = invocation.native_parameters |
| 73 | + command = list(invocation.command) |
| 74 | + dependencies = invocation.execution_plan.dependencies |
| 75 | + jars = [row for row in dependencies if (row.relative_path or '').endswith('-one.jar')] |
| 76 | + if assembly: |
| 77 | + expected = str(homes / ('relocated' if explicit else 'launcher')) |
| 78 | + assert bound['wf_clone_nxf_home'] == expected |
| 79 | + assert command.count('--wf_clone_nxf_home') == 1 |
| 80 | + assert command[command.index('--wf_clone_nxf_home') + 1] == expected |
| 81 | + assert len(jars) == 1 |
| 82 | + _, paths = native.ngs_clone_runtime_dependencies(bound) |
| 83 | + assert paths[jars[0].logical_id] == Path(expected) / 'framework/25.10.0/nextflow-25.10.0-one.jar' |
| 84 | + else: |
| 85 | + assert not jars |
| 86 | + assert not any('wf-clone-validation' in row.logical_id for row in dependencies) |
| 87 | + if not explicit: |
| 88 | + assert 'wf_clone_nxf_home' not in bound |
| 89 | + assert '--wf_clone_nxf_home' not in command |
| 90 | + |
| 91 | + |
| 92 | +def test_clone_default_compiler_uses_managed_data_home(native, homes, monkeypatch): |
| 93 | + from services.nextflow import compile_nextflow_invocation |
| 94 | + |
| 95 | + monkeypatch.setenv('BMS_DATA', str(homes / 'data')) |
| 96 | + params = {'ont_workflow_id': 'wf_clone_validation', 'ont_input_mode': 'fastq', |
| 97 | + 'fastq_path': str(homes / 'reads.fastq'), |
| 98 | + 'reference_fasta': str(homes / 'reference.fa')} |
| 99 | + invocation = compile_nextflow_invocation('nanopore', 'clone_validation', params, |
| 100 | + str(homes / 'results'), job_id='clone-default') |
| 101 | + assert invocation.execution_plan.complete, invocation.execution_plan.blockers |
| 102 | + assert invocation.native_parameters['wf_clone_nxf_home'] == str(homes / 'data/nextflow') |
| 103 | + assert 'wf_clone_nxf_home' not in params # No caller-supplied workaround. |
| 104 | + _, paths = native.ngs_clone_runtime_dependencies(invocation.native_parameters) |
| 105 | + jar = next(path for key, path in paths.items() if key.endswith('-one.jar')) |
| 106 | + assert jar == homes / 'data/nextflow/framework/25.10.0/nextflow-25.10.0-one.jar' |
0 commit comments