Skip to content

Commit

Permalink
Extract tmt try and tmt init into their own tmt.cli modules (#3394
Browse files Browse the repository at this point in the history
)
  • Loading branch information
happz authored Dec 10, 2024
1 parent 3272f89 commit 9e88909
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 146 deletions.
3 changes: 3 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def invoke(
**extra: Any) -> click.testing.Result:
reset_common()

from tmt.__main__ import import_cli_commands
import_cli_commands()

return super().invoke(
cli,
args=args,
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_try.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

import tmt.cli
import tmt.cli.trying


@pytest.mark.parametrize(
Expand All @@ -18,4 +18,4 @@
)
def test_options_arch(params: dict[str, Any], expected: dict[str, Any]):

assert tmt.cli._root._construct_trying_provision_options(params) == expected
assert tmt.cli.trying._construct_trying_provision_options(params) == expected
17 changes: 13 additions & 4 deletions tmt/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import sys


def import_cli_commands() -> None:
""" Import CLI commands from their packages """

# TODO: some kind of `import tmt.cli.*` would be nice
import tmt.cli._root # type: ignore[reportUnusedImport,unused-ignore]
import tmt.cli.init # noqa: F401,I001,RUF100
import tmt.cli.status # noqa: F401,I001,RUF100
import tmt.cli.trying # noqa: F401,I001,RUF100


def run_cli() -> None:
"""
Entry point to tmt command.
Expand All @@ -13,12 +23,11 @@ def run_cli() -> None:
tmt.utils, we would not be able to intercept the exception below.
"""
try:
import tmt.utils # noqa: I001
import tmt.utils # noqa: F401,I001,RUF100

import_cli_commands()

# Import CLI commands.
# TODO: some kind of `import tmt.cli.*` would be nice
import tmt.cli._root
import tmt.cli.status

tmt.cli._root.main()

Expand Down
140 changes: 0 additions & 140 deletions tmt/cli/_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
command remains.
"""

import re
from typing import TYPE_CHECKING, Any, Optional, Union

import click
Expand Down Expand Up @@ -1486,145 +1485,6 @@ def stories_id(
tmt.identifier.id_command(context, story.node, "story", dry=kwargs["dry"])


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Init
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@main.command()
@pass_context
@click.argument('path', default='.')
@option(
'-t', '--template', default='empty',
choices=['empty', *tmt.templates.INIT_TEMPLATES],
help="Use this template to populate the tree.")
@verbosity_options
@force_dry_options
def init(
context: Context,
path: str,
template: str,
force: bool,
**kwargs: Any) -> None:
"""
Initialize a new tmt tree.
By default tree is created in the current directory.
Provide a PATH to create it in a different location.
\b
A tree can be optionally populated with example metadata:
* 'mini' template contains a minimal plan and no tests,
* 'base' template contains a plan and a beakerlib test,
* 'full' template contains a 'full' story, an 'full' plan and a shell test.
"""

tmt.Tree.store_cli_invocation(context)
tmt.Tree.init(logger=context.obj.logger, path=Path(path), template=template, force=force)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Try
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@main.command(name="try")
@click.pass_context
@click.argument("image_and_how", nargs=-1, metavar="IMAGE[@HOW]")
@option(
"-t", "--test", default=[], metavar="REGEXP", multiple=True,
help="""
Run tests matching given regular expression.
By default all tests under the current working directory are executed.
""")
@option(
"-p", "--plan", default=[], metavar="REGEXP", multiple=True,
help="""
Use provided plan. By default user config is checked for plans
named as '/user/plan*', default plan is used otherwise.
""")
@option(
"-l", "--login", is_flag=True, default=False,
help="Log into the guest only, do not run any tests.")
@option(
"--epel", is_flag=True, default=False,
help="Enable epel repository.")
@option(
"--install", default=[], metavar="PACKAGE", multiple=True,
help="Install package on the guest.")
@option(
"--arch", default=None, metavar="ARCH", multiple=False,
help="Specify guest CPU architecture.")
@option(
"-a", "--ask", is_flag=True, default=False,
help="Just provision the guest and ask what to do next.")
@verbosity_options
@force_dry_options
def try_command(context: Context, **kwargs: Any) -> None:
"""
Try tests or experiment with guests.
Provide an interactive session to run tests or play with guests.
Provisions a guest, runs tests from the current working directory
and provides menu with available options what to do next. If no
tests are detected logs into the guest to start experimenting.
In order to specify the guest just use the desired image name:
tmt try fedora
It's also possible to select the provision method for each guest:
\b
tmt try fedora@container
tmt try centos-stream-9@virtual
Or connect to the running guest by specifying FQDN or IP address:
\b
tmt try 192.168.12.23@connect
"""

tmt.trying.Try.store_cli_invocation(context)

# Inject custom image and provision method to the Provision options
options = _construct_trying_provision_options(context.params)
if options:
tmt.steps.provision.Provision.store_cli_invocation(
context=None, options=options)

# Finally, let's start trying!
trying = tmt.trying.Try(
tree=context.obj.tree,
logger=context.obj.logger)

trying.go()


def _construct_trying_provision_options(params: Any) -> dict[str, Any]:
""" Convert try-specific options into generic option format """
options: dict[str, Any] = {}

if params['image_and_how']:
# TODO: For now just pick the first image-how pair, let's allow
# specifying multiple images and provision methods as well
image_and_how = params['image_and_how'][0]
# We expect the 'image' or 'image@how' syntax
matched = re.match("([^@]+)@([^@]+)", image_and_how.strip())
if matched:
options = {"image": matched.group(1), "how": matched.group(2)}
else:
options = {"image": image_and_how}

# Add guest architecture if provided
if params['arch']:
options['arch'] = params['arch']

# For 'connect' rename 'image' to 'guest'
if options.get('how') == 'connect':
options['guest'] = options.pop('image')

return options


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Clean
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
44 changes: 44 additions & 0 deletions tmt/cli/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
""" ``tmt init`` implementation """

from typing import Any

import click

import tmt.templates
import tmt.utils
from tmt.cli import Context, pass_context
from tmt.cli._root import force_dry_options, main, verbosity_options
from tmt.options import option
from tmt.utils import Path


@main.command()
@pass_context
@click.argument('path', default='.')
@option(
'-t', '--template', default='empty',
choices=['empty', *tmt.templates.INIT_TEMPLATES],
help="Use this template to populate the tree.")
@verbosity_options
@force_dry_options
def init(
context: Context,
path: str,
template: str,
force: bool,
**kwargs: Any) -> None:
"""
Initialize a new tmt tree.
By default tree is created in the current directory.
Provide a PATH to create it in a different location.
\b
A tree can be optionally populated with example metadata:
* 'mini' template contains a minimal plan and no tests,
* 'base' template contains a plan and a beakerlib test,
* 'full' template contains a 'full' story, an 'full' plan and a shell test.
"""

tmt.Tree.store_cli_invocation(context)
tmt.Tree.init(logger=context.obj.logger, path=Path(path), template=template, force=force)
112 changes: 112 additions & 0 deletions tmt/cli/trying.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
""" ``tmt try`` implementation """

import re
from typing import Any

import click

import tmt.steps.provision
import tmt.trying
import tmt.utils
from tmt.cli import Context, pass_context
from tmt.cli._root import force_dry_options, main, verbosity_options
from tmt.options import option


@main.command(name="try")
@pass_context
@click.argument("image_and_how", nargs=-1, metavar="IMAGE[@HOW]")
@option(
"-t", "--test", default=[], metavar="REGEXP", multiple=True,
help="""
Run tests matching given regular expression.
By default all tests under the current working directory are executed.
""")
@option(
"-p", "--plan", default=[], metavar="REGEXP", multiple=True,
help="""
Use provided plan. By default user config is checked for plans
named as '/user/plan*', default plan is used otherwise.
""")
@option(
"-l", "--login", is_flag=True, default=False,
help="Log into the guest only, do not run any tests.")
@option(
"--epel", is_flag=True, default=False,
help="Enable epel repository.")
@option(
"--install", default=[], metavar="PACKAGE", multiple=True,
help="Install package on the guest.")
@option(
"--arch", default=None, metavar="ARCH", multiple=False,
help="Specify guest CPU architecture.")
@option(
"-a", "--ask", is_flag=True, default=False,
help="Just provision the guest and ask what to do next.")
@verbosity_options
@force_dry_options
def try_command(context: Context, **kwargs: Any) -> None:
"""
Try tests or experiment with guests.
Provide an interactive session to run tests or play with guests.
Provisions a guest, runs tests from the current working directory
and provides menu with available options what to do next. If no
tests are detected logs into the guest to start experimenting.
In order to specify the guest just use the desired image name:
tmt try fedora
It's also possible to select the provision method for each guest:
\b
tmt try fedora@container
tmt try centos-stream-9@virtual
Or connect to the running guest by specifying FQDN or IP address:
\b
tmt try 192.168.12.23@connect
"""

tmt.trying.Try.store_cli_invocation(context)

# Inject custom image and provision method to the Provision options
options = _construct_trying_provision_options(context.params)
if options:
tmt.steps.provision.Provision.store_cli_invocation(
context=None, options=options)

# Finally, let's start trying!
trying = tmt.trying.Try(
tree=context.obj.tree,
logger=context.obj.logger)

trying.go()


def _construct_trying_provision_options(params: Any) -> dict[str, Any]:
""" Convert try-specific options into generic option format """
options: dict[str, Any] = {}

if params['image_and_how']:
# TODO: For now just pick the first image-how pair, let's allow
# specifying multiple images and provision methods as well
image_and_how = params['image_and_how'][0]
# We expect the 'image' or 'image@how' syntax
matched = re.match("([^@]+)@([^@]+)", image_and_how.strip())
if matched:
options = {"image": matched.group(1), "how": matched.group(2)}
else:
options = {"image": image_and_how}

# Add guest architecture if provided
if params['arch']:
options['arch'] = params['arch']

# For 'connect' rename 'image' to 'guest'
if options.get('how') == 'connect':
options['guest'] = options.pop('image')

return options

0 comments on commit 9e88909

Please sign in to comment.