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
41 changes: 41 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,44 @@ def test_decide_colorization(
monkeypatch.setattr(sys.stderr, 'isatty', lambda: testcase.simulate_tty)

assert tmt.log.decide_colorization(no_color, force_color) == testcase.expected


@pytest.mark.parametrize(
'step_args',
[
pytest.param(
['execute', '--interactive', '--how', 'tmt'],
id='long-option-before-long-how',
),
pytest.param(
['execute', '--interactive', '-h', 'tmt'],
id='long-option-before-short-how',
),
pytest.param(
['execute', '-h', 'tmt', '--interactive'],
id='long-option-after-how',
),
],
)
def test_plugin_option_before_how(
run_tmt: 'RunTmt',
step_args: list[str],
) -> None:
"""Plugin-specific flag option works regardless of position relative to --how"""

tmp = tempfile.mkdtemp()
try:
result = run_tmt(
'--root',
example('local'),
'run',
'-i',
tmp,
*step_args,
)

# The command should not fail with "No such option"
if result.exit_code != 0:
assert 'No such option' not in result.output
Comment on lines +305 to +306
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Simplify the assertion. The if statement is unnecessary.

        assert 'No such option' not in result.output

finally:
shutil.rmtree(tmp)
25 changes: 24 additions & 1 deletion tmt/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,11 +647,34 @@ def _check_method(self, context: 'tmt.cli.Context', args: list[str]) -> None:
how = None
subcommands = tmt.steps.STEPS + tmt.steps.ACTIONS + ['tests', 'plans']

def _effective_nargs(param: click.Parameter) -> int:
if isinstance(param, click.core.Option) and (param.is_flag or param.count):
return 0
return param.nargs

def _find_option_by_arg(arg: str) -> Optional[click.Parameter]:
# Check base command params first
for option in self.params:
if arg in option.opts or arg in option.secondary_opts:
return option
return None

# Search across all plugin method commands for plugin-specific
# options. The same short option (e.g. `-i`) may appear in
# multiple plugins with different nargs (flag vs value-consuming).
# Collect all matches and return the one that consumes the most
# arguments — this is conservative and ensures _find_how properly
# skips over option values.
matches: list[click.Parameter] = []
for method_command in methods.values():
for option in method_command.params:
if arg in option.opts or arg in option.secondary_opts:
matches.append(option)
break

if not matches:
return None

return max(matches, key=_effective_nargs)

def _find_how(args: list[str]) -> Optional[str]:
while args:
Expand Down
Loading