Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/workflow/CommandExecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> b
# Add non-default TOPP tool parameters
if tool in params.keys():
for k, v in params[tool].items():
# Boolean flag handling (from registerFlag_ params):
# True -> emit flag only, False -> skip entirely
if isinstance(v, bool):
if v:
command += [f"-{k}"]
continue
Comment on lines +287 to +292
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Boolean flag handling is incomplete for custom_params.

Line 287-Line 292 correctly applies flag semantics for params[tool], but Line 302-Line 310 still serializes booleans in custom_params as values (-flag True / -flag False). This creates inconsistent CLI behavior for the same option source.

🔧 Suggested fix
@@
             # Add custom parameters
             for k, v in custom_params.items():
+                # Keep boolean handling consistent with params[tool]
+                if isinstance(v, bool):
+                    if v:
+                        command += [f"-{k}"]
+                    continue
                 command += [f"-{k}"]
                 # Skip only empty strings (pass flag with no value)
                 # Note: 0 and 0.0 are valid values, so use explicit check
                 if v != "" and v is not None:
                     if isinstance(v, list):
                         command += [str(x) for x in v]
                     else:
                         command += [str(v)]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/workflow/CommandExecutor.py` around lines 287 - 292, The boolean flag
handling for custom_params is inconsistent: update the loop that serializes
custom_params so booleans follow the same semantics as the earlier params[tool]
block — if a value is a bool and True, append only the short flag token (e.g.,
command += [f"-{k}"]); if False, skip adding anything; otherwise serialize
non-bool values as before. Locate the custom_params serialization loop (the
block that iterates over custom_params, referencing variables like k, v, and
command) and apply the same isinstance(v, bool) check and conditional logic used
in the params[tool] boolean handling to ensure consistent CLI output.

command += [f"-{k}"]
# Skip only empty strings (pass flag with no value)
# Note: 0 and 0.0 are valid values, so use explicit check
Expand Down
173 changes: 173 additions & 0 deletions tests/test_boolean_flag_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""
Tests for boolean flag parameter handling in CommandExecutor.run_topp().

Verifies that:
- Python bool True (flag-style) emits only the flag, no value
- Python bool False (flag-style) omits the flag entirely
- String "true"/"false" (string-style) emits -param true / -param false
- Other parameter types (int, float, str, empty, multiline) are unchanged
"""
import os
import sys
import json
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock

# Add project root to path
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PROJECT_ROOT)

# Mock pyopenms and streamlit before importing project modules
mock_pyopenms = MagicMock()
mock_pyopenms.__version__ = "3.0.0"
sys.modules['pyopenms'] = mock_pyopenms

mock_streamlit = MagicMock()
mock_streamlit.session_state = {"settings": {"max_threads": {"local": 4, "online": 2}}}
sys.modules['streamlit'] = mock_streamlit

from src.workflow.ParameterManager import ParameterManager
from src.workflow.CommandExecutor import CommandExecutor
from src.workflow.Logger import Logger


@pytest.fixture
def workflow_env(tmp_path):
"""
Set up a realistic workflow environment with a fake ini file and params.json.

Creates:
- tmp_path/ini/FakeTool.ini (empty file, run_topp only checks existence)
- tmp_path/params.json with mixed parameter types
"""
# Create ini directory and fake ini file
ini_dir = tmp_path / "ini"
ini_dir.mkdir()
ini_file = ini_dir / "FakeTool.ini"
ini_file.write_text("<PARAMETERS></PARAMETERS>")

# Write params.json with all parameter type variants
params = {
"FakeTool": {
"enable_feature": True, # bool True -> flag only
"disable_feature": False, # bool False -> omit entirely
"string_bool_on": "true", # str "true" -> -param true
"string_bool_off": "false", # str "false" -> -param false
"threshold": 1000.0, # float
"mode": "fast", # regular string
"count": 5, # int
"empty_flag": "", # empty string -> flag only
"multi_value": "val1\nval2", # multiline string -> split
}
}
params_file = tmp_path / "params.json"
params_file.write_text(json.dumps(params, indent=4))

return tmp_path


@pytest.fixture
def captured_command(workflow_env):
"""
Create a CommandExecutor with mocked dependencies, call run_topp(),
and return the command list that would have been executed.
"""
captured = {}

pm = ParameterManager(workflow_env)
logger = MagicMock(spec=Logger)
executor = CommandExecutor(workflow_env, logger, pm)

# Capture the command instead of executing it
def fake_run_command(cmd):
captured["command"] = cmd
return True

executor.run_command = fake_run_command

executor.run_topp("FakeTool", {"in": ["input.mzML"]})

return captured["command"]


class TestBooleanFlagParams:
"""Test boolean flag parameter handling in run_topp()."""

def test_bool_true_emits_flag_only(self, captured_command):
"""Python bool True should emit -flag with no following value."""
idx = captured_command.index("-enable_feature")
# Next element should NOT be "True" — it should be another flag or -threads
next_elem = captured_command[idx + 1]
assert next_elem.startswith("-"), (
f"Expected flag-only for bool True, but got value '{next_elem}' after -enable_feature"
)

def test_bool_false_omits_flag(self, captured_command):
"""Python bool False should omit the flag entirely."""
assert "-disable_feature" not in captured_command, (
"bool False parameter should not appear in command at all"
)

def test_string_true_emits_value(self, captured_command):
"""String 'true' should emit -param true (with explicit value)."""
idx = captured_command.index("-string_bool_on")
assert captured_command[idx + 1] == "true", (
f"Expected 'true' value after -string_bool_on, got '{captured_command[idx + 1]}'"
)

def test_string_false_emits_value(self, captured_command):
"""String 'false' should emit -param false (with explicit value)."""
idx = captured_command.index("-string_bool_off")
assert captured_command[idx + 1] == "false", (
f"Expected 'false' value after -string_bool_off, got '{captured_command[idx + 1]}'"
)

def test_float_param(self, captured_command):
"""Float values should be emitted as string representation."""
idx = captured_command.index("-threshold")
assert captured_command[idx + 1] == "1000.0"

def test_string_param(self, captured_command):
"""Regular string values should be emitted as-is."""
idx = captured_command.index("-mode")
assert captured_command[idx + 1] == "fast"

def test_int_param(self, captured_command):
"""Integer values should be emitted as string representation."""
idx = captured_command.index("-count")
assert captured_command[idx + 1] == "5"

def test_empty_string_emits_flag_only(self, captured_command):
"""Empty string should emit the flag with no value."""
idx = captured_command.index("-empty_flag")
next_elem = captured_command[idx + 1]
assert next_elem.startswith("-"), (
f"Expected flag-only for empty string, but got value '{next_elem}'"
)

def test_multiline_string_splits(self, captured_command):
"""Multiline string should be split into separate values."""
idx = captured_command.index("-multi_value")
assert captured_command[idx + 1] == "val1"
assert captured_command[idx + 2] == "val2"

def test_threads_present(self, captured_command):
"""The -threads flag should always be present."""
assert "-threads" in captured_command

def test_ini_flag_present(self, captured_command):
"""The -ini flag should be present when ini file exists."""
assert "-ini" in captured_command
idx = captured_command.index("-ini")
assert captured_command[idx + 1].endswith("FakeTool.ini")

def test_input_file_present(self, captured_command):
"""Input files should be present in the command."""
assert "-in" in captured_command
idx = captured_command.index("-in")
assert captured_command[idx + 1] == "input.mzML"

def test_command_starts_with_tool(self, captured_command):
"""Command should start with the tool name."""
assert captured_command[0] == "FakeTool"
Loading