Skip to content

Commit 807e571

Browse files
authored
Merge pull request #45 from nextgenusfs/clean-memory-log-output
Quiet predict memory-monitoring log output
2 parents f3af773 + 73d4935 commit 807e571

4 files changed

Lines changed: 143 additions & 44 deletions

File tree

funannotate2/predict.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,19 +1386,6 @@ def abinitio_wrapper(
13861386

13871387
contig_length = get_contig_length(contig)
13881388

1389-
# Log contig processing message (debug only)
1390-
log_message = (
1391-
f"Processing contig {contig_name} (length: {contig_length:,} bp)"
1392-
)
1393-
try:
1394-
if hasattr(logger, "debug"):
1395-
logger.debug(log_message)
1396-
elif callable(logger):
1397-
logger(log_message + "\n")
1398-
else:
1399-
print(log_message)
1400-
except Exception:
1401-
print(log_message)
14021389
except ImportError:
14031390
warning_message = (
14041391
"Memory monitoring requested but memory module not available"
@@ -1483,18 +1470,6 @@ def run_tool_with_error_tracking(tool_name, run_func, *args, **kwargs):
14831470
)
14841471
logger.warning(warning_msg)
14851472

1486-
# Log prediction (debug only)
1487-
log_message = f"{tool_name} memory prediction for {contig_name}: {predicted_mb:.1f} MB"
1488-
try:
1489-
if hasattr(logger, "debug"):
1490-
logger.debug(log_message)
1491-
elif callable(logger):
1492-
logger(log_message + "\n")
1493-
else:
1494-
print(log_message)
1495-
except Exception:
1496-
print(log_message)
1497-
14981473
# Run the tool
14991474
run_func(*args, **kwargs)
15001475
return True, None

funannotate2/utilities.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -832,26 +832,9 @@ def logoutput(logname, process_result):
832832
elif callable(logfile):
833833
# For function-based loggers, skip detailed output logging
834834
pass
835-
# Log memory stats if available (debug only)
835+
# Keep detailed memory stats out of the normal logger path.
836+
# Structured records are still written to the dedicated JSONL file below.
836837
if memory_stats:
837-
from .memory import format_memory_report
838-
839-
memory_report = f"Memory usage for {process_name or 'subprocess'}:\n{format_memory_report(memory_stats)}"
840-
841-
# Try different logging methods with error handling - use debug level
842-
try:
843-
if hasattr(logfile, "debug"):
844-
logfile.debug(memory_report)
845-
elif callable(logfile):
846-
# Handle function-based loggers like sys.stderr.write
847-
logfile(memory_report + "\n")
848-
else:
849-
# Fallback to print
850-
print(memory_report)
851-
except Exception:
852-
# If all else fails, just print it
853-
print(memory_report)
854-
855838
# Also write to dedicated memory monitoring file
856839
try:
857840
import os

tests/unit/test_predict.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,50 @@ def mock_predict(args):
270270
finally:
271271
# Restore the original function
272272
funannotate2.predict.predict = original_predict
273+
274+
@patch("funannotate2.predict.run_snap")
275+
@patch("funannotate2.memory.get_system_memory_info")
276+
@patch("funannotate2.memory.predict_memory_usage")
277+
@patch("funannotate2.memory.get_contig_length")
278+
def test_abinitio_wrapper_suppresses_memory_chatter_but_keeps_tool_logging(
279+
self,
280+
mock_get_contig_length,
281+
mock_predict_memory_usage,
282+
mock_get_system_memory_info,
283+
mock_run_snap,
284+
tmp_path,
285+
):
286+
"""Per-contig memory details should not leak into ordinary predict logs."""
287+
contig = tmp_path / "scaffold_1.fasta"
288+
contig.write_text(">scaffold_1\nATGC\n")
289+
290+
mock_get_contig_length.return_value = 4
291+
mock_predict_memory_usage.return_value = {
292+
"predicted_peak_with_margin_mb": 64.0
293+
}
294+
mock_get_system_memory_info.return_value = {"available_gb": 8.0}
295+
296+
def fake_run_snap(*args, **kwargs):
297+
kwargs["log"]("snap tool output\n")
298+
return 0
299+
300+
mock_run_snap.side_effect = fake_run_snap
301+
302+
logger = MagicMock()
303+
params = {"abinitio": {"snap": {"location": "snap-trained.hmm"}}}
304+
305+
stats = funannotate2.predict.abinitio_wrapper(
306+
str(contig), params, logger, monitor_memory=True
307+
)
308+
309+
info_messages = [call.args[0] for call in logger.info.call_args_list]
310+
debug_messages = [call.args[0] for call in logger.debug.call_args_list]
311+
312+
assert "snap tool output" in info_messages
313+
assert "Successfully ran tools for scaffold_1.fasta: snap" in info_messages
314+
assert not any("Processing contig" in message for message in info_messages)
315+
assert not any("memory prediction for scaffold_1.fasta" in message for message in info_messages)
316+
assert not any("Processing contig" in message for message in debug_messages)
317+
assert not any("memory prediction for scaffold_1.fasta" in message for message in debug_messages)
318+
assert stats["tools_run"] == ["snap"]
319+
assert stats["contig_length"] == 4

tests/unit/test_utilities.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
Unit tests for the utilities module.
33
"""
44

5+
import json
56
import os
7+
from unittest.mock import MagicMock, patch
68

79
from funannotate2.utilities import (
810
create_tmpdir,
911
merge_coordinates,
1012
naming_slug,
1113
readBlocks,
14+
runSubprocess,
1215
)
1316

1417

@@ -134,3 +137,94 @@ def test_read_blocks_no_pattern(self):
134137
blocks = list(readBlocks(source, "#"))
135138
assert len(blocks) == 1
136139
assert blocks[0] == ["Line 1", "Line 2", "Line 3"]
140+
141+
142+
class TestRunSubprocess:
143+
"""Regression tests for subprocess logging behavior."""
144+
145+
@patch("funannotate2.utilities.subprocess.Popen")
146+
@patch("funannotate2.memory.MemoryMonitor")
147+
def test_monitoring_keeps_memory_report_out_of_callable_logs(
148+
self, mock_monitor_cls, mock_popen, tmp_path, monkeypatch
149+
):
150+
"""Memory monitoring should not emit formatted reports via callable logs."""
151+
process = MagicMock()
152+
process.pid = 12345
153+
process.returncode = 0
154+
process.stdout = ""
155+
process.stderr = ""
156+
mock_popen.return_value = process
157+
158+
mock_monitor = mock_monitor_cls.return_value
159+
mock_monitor.monitor_process.return_value = {
160+
"process_name": "snap-scaffold_1.fasta",
161+
"duration_seconds": 1.5,
162+
"peak_rss_mb": 10.0,
163+
"peak_vms_mb": 20.0,
164+
"avg_rss_mb": 9.0,
165+
"avg_vms_mb": 19.0,
166+
"sample_count": 2,
167+
"samples": [],
168+
}
169+
170+
messages = []
171+
monkeypatch.setenv("FUNANNOTATE2_OUTPUT_DIR", str(tmp_path))
172+
173+
runSubprocess(
174+
["snap", "input.fasta"],
175+
messages.append,
176+
cwd=str(tmp_path),
177+
monitor_memory=True,
178+
process_name="snap-scaffold_1.fasta",
179+
)
180+
181+
assert messages == []
182+
183+
memory_log = tmp_path / "logfiles" / "predict-abinitio-memory-monitoring.jsonl"
184+
assert memory_log.exists()
185+
186+
records = [json.loads(line) for line in memory_log.read_text().splitlines()]
187+
assert len(records) == 1
188+
assert records[0]["process_name"] == "snap-scaffold_1.fasta"
189+
assert records[0]["tool_name"] == "snap"
190+
assert records[0]["memory_stats"]["peak_rss_mb"] == 10.0
191+
192+
@patch("funannotate2.utilities.subprocess.Popen")
193+
@patch("funannotate2.memory.MemoryMonitor")
194+
def test_monitoring_does_not_debug_log_formatted_memory_report(
195+
self, mock_monitor_cls, mock_popen, tmp_path, monkeypatch
196+
):
197+
"""Detailed memory reports should stay out of the normal logfile path."""
198+
process = MagicMock()
199+
process.pid = 12345
200+
process.returncode = 0
201+
process.stdout = ""
202+
process.stderr = ""
203+
mock_popen.return_value = process
204+
205+
mock_monitor = mock_monitor_cls.return_value
206+
mock_monitor.monitor_process.return_value = {
207+
"process_name": "snap-scaffold_1.fasta",
208+
"duration_seconds": 1.5,
209+
"peak_rss_mb": 10.0,
210+
"peak_vms_mb": 20.0,
211+
"avg_rss_mb": 9.0,
212+
"avg_vms_mb": 19.0,
213+
"sample_count": 2,
214+
"samples": [],
215+
}
216+
217+
logger = MagicMock()
218+
monkeypatch.setenv("FUNANNOTATE2_OUTPUT_DIR", str(tmp_path))
219+
220+
runSubprocess(
221+
["snap", "input.fasta"],
222+
logger,
223+
cwd=str(tmp_path),
224+
monitor_memory=True,
225+
process_name="snap-scaffold_1.fasta",
226+
)
227+
228+
debug_messages = [call.args[0] for call in logger.debug.call_args_list]
229+
assert "snap input.fasta" in debug_messages
230+
assert not any("Memory usage for" in message for message in debug_messages)

0 commit comments

Comments
 (0)