Skip to content

Commit cf1c0e3

Browse files
committed
add pytest for nf-test linting
1 parent e4fb26c commit cf1c0e3

File tree

2 files changed

+108
-22
lines changed

2 files changed

+108
-22
lines changed

nf_core/pipelines/lint/nf_test_content.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ def nf_test_content(self) -> Dict[str, List[str]]:
9191
}
9292

9393
for test_fn in test_fns:
94-
if nf_test_content_conf is not None and (not nf_test_content_conf or str(test_fn) in nf_test_content_conf):
95-
ignored.append(f"'{test_fn.name}' checking ignored")
94+
if nf_test_content_conf is not None and (
95+
not nf_test_content_conf or str(test_fn.relative_to(self.wf_path)) in nf_test_content_conf
96+
):
97+
ignored.append(f"'{test_fn.relative_to(self.wf_path)}' checking ignored")
9698
continue
9799

98100
checks_passed = {check: False for check in test_checks}
@@ -103,16 +105,18 @@ def nf_test_content(self) -> Dict[str, List[str]]:
103105
while "}\n" not in line:
104106
line = next(fh)
105107
if re.search(str(check_info["pattern"]), line):
106-
passed.append(f"'{test_fn}' contains {check_info['description']}")
108+
passed.append(
109+
f"'{test_fn.relative_to(self.wf_path)}' contains {check_info['description']}"
110+
)
107111
checks_passed[check_name] = True
108112
break
109113
elif not check_info["when_block"] and re.search(str(check_info["pattern"]), line):
110-
passed.append(f"'{test_fn}' {check_info['description']}")
114+
passed.append(f"'{test_fn.relative_to(self.wf_path)}' {check_info['description']}")
111115
checks_passed[check_name] = True
112116

113117
for check_name, check_info in test_checks.items():
114118
if not checks_passed[check_name]:
115-
failed.append(f"'{test_fn}' {check_info['failure_msg']}")
119+
failed.append(f"'{test_fn.relative_to(self.wf_path)}' {check_info['failure_msg']}")
116120

117121
# Content of nextflow.config file
118122
conf_fn = Path(self.wf_path, "tests", "nextflow.config")
@@ -127,38 +131,32 @@ def nf_test_content(self) -> Dict[str, List[str]]:
127131
},
128132
"cpus": {
129133
"pattern": "cpus: *[\"']?4[\"']?",
130-
"description": "correct CPU resource limits",
131-
"failure_msg": "correct CPU resource limits. Should be 4",
134+
"description": "correct CPU resource limits. Should be 4",
132135
},
133136
"memory": {
134137
"pattern": "memory: *[\"']?15\.GB[\"']?",
135-
"description": "correct memory resource limits",
136-
"failure_msg": "correct memory resource limits. Should be 15.GB",
138+
"description": "correct memory resource limits. Should be 15.GB",
137139
},
138140
"time": {
139141
"pattern": "time: *[\"']?1\.h[\"']?",
140-
"description": "correct time resource limits",
141-
"failure_msg": "correct time resource limits. Should be 1.h",
142+
"description": "correct time resource limits. Should be 1.h",
142143
},
143144
}
144145

145-
if nf_test_content_conf is None or str(test_fn) not in nf_test_content_conf:
146+
if nf_test_content_conf is None or str(conf_fn.relative_to(self.wf_path)) not in nf_test_content_conf:
146147
checks_passed = {check: False for check in config_checks}
147148
with open(conf_fn) as fh:
148149
for line in fh:
149150
line = line.strip()
150151
for check_name, config_check_info in config_checks.items():
151152
if re.search(str(config_check_info["pattern"]), line):
152-
passed.append(f"'{conf_fn}' contains {config_check_info['description']}")
153+
passed.append(f"'{conf_fn.relative_to(self.wf_path)}' contains {config_check_info['description']}")
153154
checks_passed[check_name] = True
154155
for check_name, config_check_info in config_checks.items():
155156
if not checks_passed[check_name]:
156-
failure_msg = config_check_info.get(
157-
"failure_msg", f"does not contain {config_check_info['description']}"
158-
)
159-
failed.append(f"'{conf_fn}' {failure_msg}")
157+
failed.append(f"'{conf_fn.relative_to(self.wf_path)}' does not contain {config_check_info['description']}")
160158
else:
161-
ignored.append(f"'{conf_fn}' checking ignored")
159+
ignored.append(f"'{conf_fn.relative_to(self.wf_path)}' checking ignored")
162160

163161
# Content of nf-test.config file
164162
nf_test_conf_fn = Path(self.wf_path, "nf-test.config")
@@ -180,19 +178,19 @@ def nf_test_content(self) -> Dict[str, List[str]]:
180178
},
181179
}
182180

183-
if nf_test_content_conf is None or str(nf_test_conf_fn) not in nf_test_content_conf:
181+
if nf_test_content_conf is None or str(nf_test_conf_fn.relative_to(self.wf_path)) not in nf_test_content_conf:
184182
checks_passed = {check: False for check in nf_test_checks}
185183
with open(nf_test_conf_fn) as fh:
186184
for line in fh:
187185
line = line.strip()
188186
for check_name, nf_test_check_info in nf_test_checks.items():
189187
if re.search(str(nf_test_check_info["pattern"]), line):
190-
passed.append(f"'{nf_test_conf_fn}' {nf_test_check_info['description']}")
188+
passed.append(f"'{nf_test_conf_fn.relative_to(self.wf_path)}' {nf_test_check_info['description']}")
191189
checks_passed[check_name] = True
192190
for check_name, nf_test_check_info in nf_test_checks.items():
193191
if not checks_passed[check_name]:
194-
failed.append(f"'{nf_test_conf_fn}' {nf_test_check_info['failure_msg']}")
192+
failed.append(f"'{nf_test_conf_fn.relative_to(self.wf_path)}' {nf_test_check_info['failure_msg']}")
195193
else:
196-
ignored.append(f"'{nf_test_conf_fn}' checking ignored")
194+
ignored.append(f"'{nf_test_conf_fn.relative_to(self.wf_path)}' checking ignored")
197195

198196
return {"passed": passed, "failed": failed, "ignored": ignored}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from pathlib import Path
2+
3+
import yaml
4+
5+
import nf_core.pipelines.lint
6+
7+
from ..test_lint import TestLint
8+
9+
10+
class TestLintNfTestContent(TestLint):
11+
def setUp(self) -> None:
12+
super().setUp()
13+
self.new_pipeline = self._make_pipeline_copy()
14+
15+
def test_nf_test_content_nf_test_files(self):
16+
"""Test failure if nf-test file does not contain outdir parameter."""
17+
# Create a test file without outdir parameter or versions.yml snapshot
18+
test_file = Path(self.new_pipeline) / "tests" / "test.nf.test"
19+
with open(test_file, "w") as f:
20+
f.write("// No outdir parameter or versions YAML snapshot")
21+
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
22+
result = lint_obj.nf_test_content()
23+
assert len(result["failed"]) > 0
24+
assert (
25+
"'tests/test.nf.test' does not contain `outdir` parameter, it should contain `outdir = \"$outputDir\"`"
26+
in result["failed"]
27+
)
28+
assert "'tests/test.nf.test' does not snapshot a 'versions.yml' file" in result["failed"]
29+
30+
def test_nf_test_content_nextflow_config_file(self):
31+
"""Test failure if nextflow.config does not contain correct resource limits."""
32+
# Create nextflow.config without resource limits or required parameters
33+
config_file = Path(self.new_pipeline) / "tests" / "nextflow.config"
34+
with open(config_file, "w") as f:
35+
f.write("// Missing resource limits and parameters")
36+
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
37+
result = lint_obj.nf_test_content()
38+
assert len(result["failed"]) > 0
39+
assert "'tests/nextflow.config' does not contain `modules_testdata_base_path`" in result["failed"]
40+
assert "'tests/nextflow.config' does not contain `pipelines_testdata_base_path`" in result["failed"]
41+
assert "'tests/nextflow.config' does not contain correct CPU resource limits. Should be 4" in result["failed"]
42+
assert (
43+
"'tests/nextflow.config' does not contain correct memory resource limits. Should be 15.GB"
44+
in result["failed"]
45+
)
46+
assert (
47+
"'tests/nextflow.config' does not contain correct time resource limits. Should be 1.h" in result["failed"]
48+
)
49+
50+
def test_nf_test_content_missing_nf_test_config_file(self):
51+
"""Test failure if nf-test.config does not contain required settings."""
52+
# Create nf-test.config without required settings
53+
config_file = Path(self.new_pipeline) / "nf-test.config"
54+
with open(config_file, "w") as f:
55+
f.write("// Missing required settings")
56+
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
57+
result = lint_obj.nf_test_content()
58+
assert len(result["failed"]) > 0
59+
assert "'nf-test.config' does not set a `testsDir`, it should contain `testsDir \".\"`" in result["failed"]
60+
assert (
61+
'\'nf-test.config\' does not set a `workDir`, it should contain `workDir System.getenv("NFT_WORKDIR") ?: ".nf-test"`'
62+
in result["failed"]
63+
)
64+
assert (
65+
"'nf-test.config' does not set a `configFile`, it should contain `configFile \"tests/nextflow.config\"`"
66+
in result["failed"]
67+
)
68+
69+
def test_nf_test_content_ignored(self):
70+
"""Test that nf-test content checks can be ignored via .nf-core.yml."""
71+
# Create .nf-core.yml to ignore checks
72+
nf_core_yml = Path(self.new_pipeline) / ".nf-core.yml"
73+
with open(nf_core_yml) as f:
74+
yml_content = yaml.safe_load(f)
75+
76+
yml_content["lint"] = {"nf_test_content": ["tests/default.nf.test", "tests/nextflow.config", "nf-test.config"]}
77+
78+
with open(nf_core_yml, "w") as f:
79+
yaml.dump(yml_content, f)
80+
81+
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
82+
lint_obj._load()
83+
result = lint_obj.nf_test_content()
84+
print(result)
85+
assert len(result["ignored"]) == 3
86+
assert "'tests/default.nf.test' checking ignored" in result["ignored"]
87+
assert "'tests/nextflow.config' checking ignored" in result["ignored"]
88+
assert "'nf-test.config' checking ignored" in result["ignored"]

0 commit comments

Comments
 (0)