files: 
  ======================
      Examples
  ======================
  
  Let's have a look at a couple of real-life examples!
  
  You can obtain detailed list of available options for each command
  by invoking it with ``--help``. To control the verbosity of the
  output, use ``--debug`` and ``--quiet``. Depending on the command,
  ``--verbose`` might be also available.
  
  Simply run ``tmt`` to get started with exploring your working
  directory::
  
      $ tmt
      Found 2 tests: /tests/docs and /tests/ls.
      Found 3 plans: /plans/basic, /plans/helps and /plans/smoke.
      Found 109 stories: /spec/core/description, /spec/core/order,
      /spec/core/summary, /spec/plans/artifact, /spec/plans/gate,
      /spec/plans/summary, /spec/steps/discover and 103 more.
  
  
  
  Init
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Before starting a new project initialize the metadata tree root::
  
      $ tmt init
      Tree '/tmp/try' initialized.
      To populate it with example content, use --template with mini, base or full.
  
  You can also populate it with a minimal plan example::
  
      $ tmt init --template mini
      Tree '/tmp/try' initialized.
      Applying template 'mini'.
      Directory '/tmp/try/plans' created.
      Plan '/tmp/try/plans/example.fmf' created.
  
  Create a plan and a test::
  
      $ tmt init --template base
      Tree '/tmp/try' initialized.
      Applying template 'base'.
      Directory '/tmp/try/tests/example' created.
      Test metadata '/tmp/try/tests/example/main.fmf' created.
      Test script '/tmp/try/tests/example/test.sh' created.
      Directory '/tmp/try/plans' created.
      Plan '/tmp/try/plans/example.fmf' created.
  
  Initialize with a richer example that also includes the story
  (overwriting existing files)::
  
      $ tmt init --template full --force
      Tree '/tmp/try' already exists.
      Applying template 'full'.
      Directory '/tmp/try/tests/example' already exists.
      Test metadata '/tmp/try/tests/example/main.fmf' overwritten.
      Test script '/tmp/try/tests/example/test.sh' overwritten.
      Directory '/tmp/try/plans' already exists.
      Plan '/tmp/try/plans/example.fmf' overwritten.
      Directory '/tmp/try/stories' created.
      Story '/tmp/try/stories/example.fmf' created.
  
  
  
  Tests
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``tests`` command is used to investigate and handle tests.
  See the :ref:`specification` for details about the L1 Metadata.
  
  
  Explore Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests`` to briefly list discovered tests::
  
      $ tmt tests
      Found 2 tests: /tests/docs and /tests/ls.
  
  Use ``tmt tests ls`` to list available tests, one per line::
  
      $ tmt tests ls
      /tests/docs
      /tests/ls
  
  Use ``tmt tests show`` to see detailed test metadata::
  
      $ tmt tests show
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
  
      /tests/ls
           summary List available tests and plans
       description Make sure that 'tmt test ls' and 'tmt plan ls' work.
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/ls
          duration 5m
              tier 1
            result respect
           enabled yes
  
  Append ``--verbose`` to get the list of source files where
  metadata are defined::
  
      $ tmt show /test/docs --verbose
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
           sources /home/psss/git/tmt/tests/main.fmf
                   /home/psss/git/tmt/tests/docs/main.fmf
  
  
  Filter Tests
  ------------------------------------------------------------------
  
  Both ``tmt tests ls`` and ``tmt tests show`` can optionally filter
  tests with a regular expression, filter expression or a Python
  condition::
  
      $ tmt tests show docs
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
  
      $ tmt tests ls --filter 'tier: 0'
      /tests/docs
  
      $ tmt tests ls --condition 'tier > 0'
      /tests/ls
  
  In order to select tests under the current working directory use
  the single dot notation::
  
  
  
  [... the rest is omitted ...]
files: docs/examples.rst
files: 
  ======================
      Examples
  ======================
  
  Let's have a look at a couple of real-life examples!
  
  You can obtain detailed list of available options for each command
  by invoking it with ``--help``. To control the verbosity of the
  output, use ``--debug`` and ``--quiet``. Depending on the command,
  ``--verbose`` might be also available.
  
  Simply run ``tmt`` to get started with exploring your working
  directory::
  
      $ tmt
      Found 2 tests: /tests/docs and /tests/ls.
      Found 3 plans: /plans/basic, /plans/helps and /plans/smoke.
      Found 109 stories: /spec/core/description, /spec/core/order,
      /spec/core/summary, /spec/plans/artifact, /spec/plans/gate,
      /spec/plans/summary, /spec/steps/discover and 103 more.
  
  
  
  Init
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Before starting a new project initialize the metadata tree root::
  
      $ tmt init
      Tree '/tmp/try' initialized.
      To populate it with example content, use --template with mini, base or full.
  
  You can also populate it with a minimal plan example::
  
      $ tmt init --template mini
      Tree '/tmp/try' initialized.
      Applying template 'mini'.
      Directory '/tmp/try/plans' created.
      Plan '/tmp/try/plans/example.fmf' created.
  
  Create a plan and a test::
  
      $ tmt init --template base
      Tree '/tmp/try' initialized.
      Applying template 'base'.
      Directory '/tmp/try/tests/example' created.
      Test metadata '/tmp/try/tests/example/main.fmf' created.
      Test script '/tmp/try/tests/example/test.sh' created.
      Directory '/tmp/try/plans' created.
      Plan '/tmp/try/plans/example.fmf' created.
  
  Initialize with a richer example that also includes the story
  (overwriting existing files)::
  
      $ tmt init --template full --force
      Tree '/tmp/try' already exists.
      Applying template 'full'.
      Directory '/tmp/try/tests/example' already exists.
      Test metadata '/tmp/try/tests/example/main.fmf' overwritten.
      Test script '/tmp/try/tests/example/test.sh' overwritten.
      Directory '/tmp/try/plans' already exists.
      Plan '/tmp/try/plans/example.fmf' overwritten.
      Directory '/tmp/try/stories' created.
      Story '/tmp/try/stories/example.fmf' created.
  
  
  
  Tests
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``tests`` command is used to investigate and handle tests.
  See the :ref:`specification` for details about the L1 Metadata.
  
  
  Explore Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests`` to briefly list discovered tests::
  
      $ tmt tests
      Found 2 tests: /tests/docs and /tests/ls.
  
  Use ``tmt tests ls`` to list available tests, one per line::
  
      $ tmt tests ls
      /tests/docs
      /tests/ls
  
  Use ``tmt tests show`` to see detailed test metadata::
  
      $ tmt tests show
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
  
      /tests/ls
           summary List available tests and plans
       description Make sure that 'tmt test ls' and 'tmt plan ls' work.
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/ls
          duration 5m
              tier 1
            result respect
           enabled yes
  
  Append ``--verbose`` to get the list of source files where
  metadata are defined::
  
      $ tmt show /test/docs --verbose
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
           sources /home/psss/git/tmt/tests/main.fmf
                   /home/psss/git/tmt/tests/docs/main.fmf
  
  
  Filter Tests
  ------------------------------------------------------------------
  
  Both ``tmt tests ls`` and ``tmt tests show`` can optionally filter
  tests with a regular expression, filter expression or a Python
  condition::
  
      $ tmt tests show docs
      /tests/docs
           summary Check that essential documentation is working
           contact Petr Šplíchal <psplicha@redhat.com>
              test ./test.sh
              path /tests/docs
          duration 5m
              tier 0
            result respect
           enabled yes
  
      $ tmt tests ls --filter 'tier: 0'
      /tests/docs
  
      $ tmt tests ls --condition 'tier > 0'
      /tests/ls
  
  
  Lint Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests lint`` to check defined test metadata
  
  [... the rest is omitted ...]
files: docs/examples.rst
files: story: 'As a user I want to comfortably work with plans'
  
  /ls:
      story: 'List available plans'
      example: tmt plan ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-plans
  
  /show:
      story: 'Show plan configuration'
      example: tmt plan show
      implemented: /tmt/cli
      documented: /docs/examples#explore-plans
  
  /filter:
      story: 'Filter available plans'
      description:
          Search plans using a regular expression or a filter.
          Use ``.`` to select plans under the current directory.
      example:
          - tmt plan ls .
          - tmt plan ls REGEXP
          - tmt plan show --filter artifact:build
      implemented: /tmt/base.py
      documented: /docs/examples#explore-plans
  
  /lint:
      story: 'Check plan against the L2 metadata specification'
      description:
          Verify that plan metadata are aligned with the
          specification, e.g. that all required attributes are
          present and that all attributes have correct type.
      example: tmt plan lint
      implemented: /tmt/cli
  
  /create:
      story: 'As a developer I want to easily enable CI'
      description:
          Provide a super-easy and user-friendly way how to
          enable tests in the CI. Several templates should be
          supported to cover common use cases.
      example:
          - tmt plan create /plans/smoke --template=mini
          - tmt plan create /plans/features --template=full
      implemented: /tmt/cli
      documented: /docs/examples#create-plans
  
  
  [... the rest is omitted ...]
files: stories/cli/plan.fmf
files: story: 'As a user I want to comfortably work with plans'
  
  /ls:
      story: 'List available plans'
      example: tmt plan ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-plans
  
  /show:
      story: 'Show plan configuration'
      example: tmt plan show
      implemented: /tmt/cli
      documented: /docs/examples#explore-plans
  
  /filter:
      story: 'Filter available plans'
      example:
          - tmt plan ls REGEXP
          - tmt plan show --filter artifact:build
      implemented: /tmt/base.py
      documented: /docs/examples#explore-plans
  
  /lint:
      story: 'Check plan against the L2 metadata specification'
      description:
          Verify that plan metadata are aligned with the
          specification, e.g. that all required attributes are
          present and that all attributes have correct type.
      example: tmt plan lint
      implemented: /tmt/cli
  
  /create:
      story: 'As a developer I want to easily enable CI'
      description:
          Provide a super-easy and user-friendly way how to
          enable tests in the CI. Several templates should be
          supported to cover common use cases.
      example:
          - tmt plan create /plans/smoke --template=mini
          - tmt plan create /plans/features --template=full
      implemented: /tmt/cli
      documented: /docs/examples#create-plans
  
  
  [... the rest is omitted ...]
files: stories/cli/plan.fmf
files: story: 'As a developer I want to comfortably work with stories'
  
  /ls:
      story: 'List available stories'
      example: tmt story ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-stories
  
  /show:
      story: 'Show story details'
      example: tmt story show
      implemented: /tmt/cli
      documented: /docs/examples#explore-stories
  
  /filter:
      story: 'Search available stories'
      description:
          Search stories using a regular expression, a filter or
          coverage status. Use ``.`` to select stories under the
          current directory.
      example:
          - tmt story ls .
          - tmt story ls REGEXP
          - tmt story ls --implemented
          - tmt story ls --unimplemented
          - tmt story ls --tested
          - tmt story ls --untested
          - tmt story ls --documented
          - tmt story ls --undocumented
      implemented: /tmt/cli
      documented: /docs/examples#filter-stories
  
  /create:
      story: 'As a developer I want to easily create new story.'
      description:
          Provide an easy way how to create a new story.
          Several templates should be provided to choose from.
      example:
          - tmt story create /stories/area/story
          - tmt story create /stories/area/story --template mini
          - tmt story create /stories/area/story --template mini --force
      implemented: /tmt/cli
      documented: /docs/examples#create-stories
  
  /coverage:
      summary: 'Show story coverage'
      story:
          As a developer I want to easily view which stories
          have been implemented, are covered by tests and
          documented and which still need work to be done.
      implemented: /tmt/cli
      documented: /docs/examples#story-coverage
  
      /overview:
          summary: 'General coverage overview'
          description:
              Complete overview of the story coverage including
              implementation, testing and documentation status.
          example: tmt story coverage
  
      /code:
          summary: 'View code implementation status'
          example:
              - tmt story coverage -c
              - tmt story coverage --code
  
      /test:
          summary: 'View test coverage status'
          example:
              - tmt story coverage -t
              - tmt story coverage --test
  
      /docs:
          summary: 'View documentation status'
          example:
              - tmt story coverage -d
              - tmt story coverage --docs
  
  /export:
      story: 'I need to convert stories into a beautiful format'
  
      /rst:
          story: 'Export stories into reStructuredText'
          description:
              So that they can be included in tmt documentation.
          example: tmt story export --rst
          implemented:
              - /tmt/cli
              - /tmt/base
  
      /html:
          story: 'Export stories into html'
          description:
              So that they can be quickly reviewed in a web
              browser. Optionally a simple python httpd server
              could be started to serve the page or pages.
          example: tmt story export --html --server
  
  
  [... the rest is omitted ...]
files: stories/cli/story.fmf
files: story: 'As a developer I want to comfortably work with stories'
  
  /ls:
      story: 'List available stories'
      example: tmt story ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-stories
  
  /show:
      story: 'Show story details'
      example: tmt story show
      implemented: /tmt/cli
      documented: /docs/examples#explore-stories
  
  /filter:
      story: 'Search available stories'
      example:
          - tmt story --implemented
          - tmt story --unimplemented
          - tmt story --tested
          - tmt story --untested
          - tmt story --documented
          - tmt story --undocumented
      implemented: /tmt/cli
      documented: /docs/examples#filter-stories
  
  /create:
      story: 'As a developer I want to easily create new story.'
      description:
          Provide an easy way how to create a new story.
          Several templates should be provided to choose from.
      example:
          - tmt story create /stories/area/story
          - tmt story create /stories/area/story --template mini
          - tmt story create /stories/area/story --template mini --force
      implemented: /tmt/cli
      documented: /docs/examples#create-stories
  
  /coverage:
      summary: 'Show story coverage'
      story:
          As a developer I want to easily view which stories
          have been implemented, are covered by tests and
          documented and which still need work to be done.
      implemented: /tmt/cli
      documented: /docs/examples#story-coverage
  
      /overview:
          summary: 'General coverage overview'
          description:
              Complete overview of the story coverage including
              implementation, testing and documentation status.
          example: tmt story coverage
  
      /code:
          summary: 'View code implementation status'
          example:
              - tmt story coverage -c
              - tmt story coverage --code
  
      /test:
          summary: 'View test coverage status'
          example:
              - tmt story coverage -t
              - tmt story coverage --test
  
      /docs:
          summary: 'View documentation status'
          example:
              - tmt story coverage -d
              - tmt story coverage --docs
  
  /export:
      story: 'I need to convert stories into a beautiful format'
  
      /rst:
          story: 'Export stories into reStructuredText'
          description:
              So that they can be included in tmt documentation.
          example: tmt story export --rst
          implemented:
              - /tmt/cli
              - /tmt/base
  
      /html:
          story: 'Export stories into html'
          description:
              So that they can be quickly reviewed in a web
              browser. Optionally a simple python httpd server
              could be started to serve the page or pages.
          example: tmt story export --html --server
  
  
  [... the rest is omitted ...]
files: stories/cli/story.fmf
files: story: 'As a user I want to comfortably work with tests'
  
  /ls:
      story: 'List available tests'
      example: tmt test ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-tests
  
  /show:
      story: 'Show test metadata'
      example: tmt test show
      implemented: /tmt/cli
      documented: /docs/examples#explore-tests
  
  /lint:
      story: 'Check test against the L1 metadata specification'
      description:
          Verify that test metadata are aligned with the
          specification, e.g. that all required attributes are
          present and that all attributes have correct type.
      example: tmt test lint
      implemented: /tmt/cli
      documented: /docs/examples#lint-tests
  
  /create:
      story: 'Create a new test skeleton'
      description:
          Provide an easy way how to create a new test. Ideally
          with support for multiple test skeletons. Similarly as
          beaker-wizard (which should be obsoleted by this) the
          tool should prompt for required values if not provided
          on the command line.
      example:
          - tmt test create
          - tmt test create /tests/area/feature
          - tmt test create /tests/area/feature --skeleton beakerlib
          - tmt test create /tests/area/feature --duration=1h
      implemented: /tmt/cli
      documented: /docs/examples#create-tests
  
  /convert:
      story: 'Convert old test metadata'
      example: tmt test convert
      implemented: /tmt/convert
      tested: /tests/test_convert
      documented: /docs/examples#convert-tests
  
  /export:
      story: 'I want to export test metadata.'
  
      /format:
          story: 'I want to export test metadata into given format.'
          example: tmt test export --format yaml
          implemented: /tmt/base
  
      /nitrate:
          story: 'I want to export test metadata into nitrate.'
          example: tmt test export --nitrate
          description: |
              In order to keep metadata in sync with the old test
              case management system we need to export selected set
              of attributes back to nitrate. The full fmf object
              identifier should be added to the structured field
              under the key ``fmf``. A warning should be added to
              the structured field informing users that the test
              metadata are now maintained in git.
  
              Below is the list of supported ``fmf`` attributes and
              corresponding nitrate fields:
  
              * component — components tab
              * contact — default tester
              * description — purpose-file in the structured field
              * duration — estimated time
              * enabled — status
              * environment — arguments
              * path — not synced
              * relevancy — relevancy in the structured field
              * result — not synced
              * summary — description in the structured field
              * tag — tags tab
              * test — not synced
              * tier — tags (e.g. ``1`` synced to the ``Tier1`` tag)
  
              The following attributes, if present, should be
              exported as well:
  
              * extra-hardware — hardware in the structured field
              * extra-pepa — pepa in the structured field
  
              They have the ``extra`` prefix as they are not part of
              the L1 Metadata Specification and are supposed to be
              synced temporarily to keep backward compatibility.
  
              After a successful export, special tag ``fmf-export``
              should be added to the nitrate test case in order to
              allow easy search for migrated test cases directly
              from the web interface.
  
  /coverage:
      story: 'Test coverage overview'
      description:
          Show current status of test coverage.
          Alias for /stories/cli/story/coverage/test
      example: tmt test coverage
  
  /filter:
      story: 'Filter available tests'
      description:
          Search tests using a regular expression or a filter.
          Use ``.`` to select tests under the current directory.
      example:
          - tmt test ls .
          - tmt test ls REGEXP
          - tmt test show --filter tier:1
          - tmt test show --condition 'tier < 2'
      implemented: /tmt/base.py
      documented: /docs/examples#filter-tests
  
  
  [... the rest is omitted ...]
files: stories/cli/test.fmf
files: story: 'As a user I want to comfortably work with tests'
  
  /ls:
      story: 'List available tests'
      example: tmt test ls
      implemented: /tmt/cli
      tested: /tests/ls
      documented: /docs/examples#explore-tests
  
  /show:
      story: 'Show test metadata'
      example: tmt test show
      implemented: /tmt/cli
      documented: /docs/examples#explore-tests
  
  /lint:
      story: 'Check test against the L1 metadata specification'
      description:
          Verify that test metadata are aligned with the
          specification, e.g. that all required attributes are
          present and that all attributes have correct type.
      example: tmt test lint
      implemented: /tmt/cli
      documented: /docs/examples#lint-tests
  
  /create:
      story: 'Create a new test skeleton'
      description:
          Provide an easy way how to create a new test. Ideally
          with support for multiple test skeletons. Similarly as
          beaker-wizard (which should be obsoleted by this) the
          tool should prompt for required values if not provided
          on the command line.
      example:
          - tmt test create
          - tmt test create /tests/area/feature
          - tmt test create /tests/area/feature --skeleton beakerlib
          - tmt test create /tests/area/feature --duration=1h
      implemented: /tmt/cli
      documented: /docs/examples#create-tests
  
  /convert:
      story: 'Convert old test metadata'
      example: tmt test convert
      implemented: /tmt/convert
      tested: /tests/test_convert
      documented: /docs/examples#convert-tests
  
  /export:
      story: 'I want to export test metadata.'
  
      /format:
          story: 'I want to export test metadata into given format.'
          example: tmt test export --format yaml
          implemented: /tmt/base
  
      /nitrate:
          story: 'I want to export test metadata into nitrate.'
          example: tmt test export --nitrate
          description: |
              In order to keep metadata in sync with the old test
              case management system we need to export selected set
              of attributes back to nitrate. The full fmf object
              identifier should be added to the structured field
              under the key ``fmf``. A warning should be added to
              the structured field informing users that the test
              metadata are now maintained in git.
  
              Below is the list of supported ``fmf`` attributes and
              corresponding nitrate fields:
  
              * component — components tab
              * contact — default tester
              * description — purpose-file in the structured field
              * duration — estimated time
              * enabled — status
              * environment — arguments
              * path — not synced
              * relevancy — relevancy in the structured field
              * result — not synced
              * summary — description in the structured field
              * tag — tags tab
              * test — not synced
              * tier — tags (e.g. ``1`` synced to the ``Tier1`` tag)
  
              The following attributes, if present, should be
              exported as well:
  
              * extra-hardware — hardware in the structured field
              * extra-pepa — pepa in the structured field
  
              They have the ``extra`` prefix as they are not part of
              the L1 Metadata Specification and are supposed to be
              synced temporarily to keep backward compatibility.
  
              After a successful export, special tag ``fmf-export``
              should be added to the nitrate test case in order to
              allow easy search for migrated test cases directly
              from the web interface.
  
  /coverage:
      story: 'Test coverage overview'
      description:
          Show current status of test coverage.
          Alias for /stories/cli/story/coverage/test
      example: tmt test coverage
  
  /filter:
      story: 'Filter available tests'
      example:
          - tmt test ls REGEXP
          - tmt test show --filter tier:1
          - tmt test show --condition 'tier < 2'
      implemented: /tmt/base.py
      documented: /docs/examples#filter-tests
  
  
  [... the rest is omitted ...]
files: stories/cli/test.fmf
files: # coding: utf-8
  
  """ Base Metadata Classes """
  
  import os
  import re
  import fmf
  import pprint
  
  import tmt.steps
  import tmt.utils
  import tmt.templates
  
  import tmt.steps.discover
  import tmt.steps.provision
  import tmt.steps.prepare
  import tmt.steps.execute
  import tmt.steps.report
  import tmt.steps.finish
  
  from tmt.utils import verdict
  from fmf.utils import listed
  from click import echo, style
  
  
  class Node(tmt.utils.Common):
      """
      General node object
  
      Corresponds to given fmf.Tree node.
      Implements common Test, Plan and Story methods.
      """
  
      # Supported attributes
      _keys = ['name']
  
      def __init__(self, node, parent=None):
          """ Initialize the node """
          super(Node, self).__init__(parent=parent, name=node.name)
          self.node = node
  
      def __str__(self):
          """ Node name """
          return self.name
  
      def _sources(self):
          """ Show source files """
          echo(tmt.utils.format(
              'sources', self.node.sources, key_color='magenta'))
  
      @classmethod
      def _save_context(cls, context):
          """ Save provided command line context for future use """
          super(Node, cls)._save_context(context)
  
          # Handle '.' as an alias for the current working directory
          names = cls._opt('names')
          if names is not None and '.' in names:
              root = context.obj.tree.root
              current = os.getcwd()
              # Handle special case when directly in the metadata root
              if current == root:
                  path = '/'
              # Prepare path from the tree root to the current directory
              else:
                  path = os.path.join('/', os.path.relpath(current, root))
              cls._context.params['names'] = (
                  path if name == '.' else name for name in names)
  
      def name_and_summary(self):
          """ Node name and optional summary """
          if self.summary:
              return '{0} ({1})'.format(self.name, self.summary)
          else:
              return self.name
  
      def ls(self, summary=False):
          """ List node """
          echo(style(self.name, fg='red'))
          if summary and self.summary:
              echo(tmt.utils.format('summary', self.summary))
  
      def export(self, format_='yaml', keys=None):
          """ Export data into requested format (yaml or dict) """
          if keys is None:
              keys = self._keys
          data = dict([(key, getattr(self, key)) for key in keys])
          # Choose proper format
          if format_ == 'dict':
              return data
          elif format_ == 'yaml':
              return tmt.utils.dictionary_to_yaml(data)
          else:
              raise tmt.utils.GeneralError(
                  f"Invalid test export format '{format_}'.")
  
  
  class Test(Node):
      """ Test object (L1 Metadata) """
  
      # Supported attributes (listed in display order)
      _keys = [
          'summary',
          'description',
          'contact',
          'component',
          'test',
          'path',
          'duration',
          'environment',
          'relevancy',
          'tags',
          'tier',
          'result',
          'enabled',
          ]
  
      def __init__(self, node):
          """ Initialize the test """
          super(Test, self).__init__(node)
          # Get all supported attributes
          for key in self._keys:
              setattr(self, key, self.node.get(key))
          # Path defaults to the node name
          if self.path is None:
              self.path = self.name
          # Handle other default values
          if self.enabled is None:
              disabled = self.node.get('disabled')
              if disabled is not None:
                  self.enabled = not disabled
              else:
                  self.enabled = True
          if self.result is None:
              self.result = 'respect'
  
      @staticmethod
      def overview(tree):
          """ Show overview of available tests """
          tests = [
              style(str(test), fg='red') for test in tree.tests()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(tests, 'test'),
                  ': ' if tests else '',
                  listed(tests, max=12)
              ), fg='blue'))
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new test """
          # Create directory
          directory_path = os.path.join(tree.root, name.lstrip('/'))
          tmt.utils.create_directory(directory_path, 'test directory')
  
          # Create metadata
          metadata_path = os.path.join(directory_path, 'main.fmf')
          tmt.utils.create_file(
              path=metadata_path, content=tmt.templates.TEST_METADATA,
              name='test metadata', force=force)
  
          # Create script
          script_path = os.path.join(directory_path, 'test.sh')
          try:
              content = tmt.templates.TEST[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=script_path, content=content,
             
  
  [... the rest is omitted ...]
files: tmt/base.py
files: # coding: utf-8
  
  """ Base Metadata Classes """
  
  import os
  import re
  import fmf
  import pprint
  
  import tmt.steps
  import tmt.utils
  import tmt.templates
  
  import tmt.steps.discover
  import tmt.steps.provision
  import tmt.steps.prepare
  import tmt.steps.execute
  import tmt.steps.report
  import tmt.steps.finish
  
  from tmt.utils import verdict
  from fmf.utils import listed
  from click import echo, style
  
  
  class Node(tmt.utils.Common):
      """
      General node object
  
      Corresponds to given fmf.Tree node.
      Implements common Test, Plan and Story methods.
      """
  
      # Supported attributes
      _keys = ['name']
  
      def __init__(self, node, parent=None):
          """ Initialize the node """
          super(Node, self).__init__(parent=parent, name=node.name)
          self.node = node
  
      def __str__(self):
          """ Node name """
          return self.name
  
      def _sources(self):
          """ Show source files """
          echo(tmt.utils.format(
              'sources', self.node.sources, key_color='magenta'))
  
      def name_and_summary(self):
          """ Node name and optional summary """
          if self.summary:
              return '{0} ({1})'.format(self.name, self.summary)
          else:
              return self.name
  
      def ls(self, summary=False):
          """ List node """
          echo(style(self.name, fg='red'))
          if summary and self.summary:
              echo(tmt.utils.format('summary', self.summary))
  
      def export(self, format_='yaml', keys=None):
          """ Export data into requested format (yaml or dict) """
          if keys is None:
              keys = self._keys
          data = dict([(key, getattr(self, key)) for key in keys])
          # Choose proper format
          if format_ == 'dict':
              return data
          elif format_ == 'yaml':
              return tmt.utils.dictionary_to_yaml(data)
          else:
              raise tmt.utils.GeneralError(
                  f"Invalid test export format '{format_}'.")
  
  
  class Test(Node):
      """ Test object (L1 Metadata) """
  
      # Supported attributes (listed in display order)
      _keys = [
          'summary',
          'description',
          'contact',
          'component',
          'test',
          'path',
          'duration',
          'environment',
          'relevancy',
          'tags',
          'tier',
          'result',
          'enabled',
          ]
  
      def __init__(self, node):
          """ Initialize the test """
          super(Test, self).__init__(node)
          # Get all supported attributes
          for key in self._keys:
              setattr(self, key, self.node.get(key))
          # Path defaults to the node name
          if self.path is None:
              self.path = self.name
          # Handle other default values
          if self.enabled is None:
              disabled = self.node.get('disabled')
              if disabled is not None:
                  self.enabled = not disabled
              else:
                  self.enabled = True
          if self.result is None:
              self.result = 'respect'
  
      @staticmethod
      def overview(tree):
          """ Show overview of available tests """
          tests = [
              style(str(test), fg='red') for test in tree.tests()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(tests, 'test'),
                  ': ' if tests else '',
                  listed(tests, max=12)
              ), fg='blue'))
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new test """
          # Create directory
          directory_path = os.path.join(tree.root, name.lstrip('/'))
          tmt.utils.create_directory(directory_path, 'test directory')
  
          # Create metadata
          metadata_path = os.path.join(directory_path, 'main.fmf')
          tmt.utils.create_file(
              path=metadata_path, content=tmt.templates.TEST_METADATA,
              name='test metadata', force=force)
  
          # Create script
          script_path = os.path.join(directory_path, 'test.sh')
          try:
              content = tmt.templates.TEST[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=script_path, content=content,
              name='test script', force=force, mode=0o755)
  
      def show(self):
          """ Show test details """
          self.ls()
          for key in self._keys:
              value = getattr(self, key)
              if value is None:
                  continue
              else:
                  echo(tmt.utils.format(key, value))
          if self.opt('verbose'):
              self._sources()
  
      def lint(self):
          """ Check test against the L1 metadata specification. """
          self.ls()
          echo(verdict(self.test is not None, 'test script must be defined'))
          echo(verdict(self.path is not None, 'directory path must be defined'))
          if self.summary is None:
              echo(verdict(2, 'summary is very useful
  
  [... the rest is omitted ...]
files: tmt/base.py
files: # coding: utf-8
  
  """ Command line interface for the Test Management Tool """
  
  from click import echo, style
  from fmf.utils import listed
  
  import click
  import os
  
  import fmf
  import tmt
  import tmt.utils
  import tmt.convert
  import tmt.steps
  import tmt.templates
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Custom Group
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class CustomGroup(click.Group):
      """ Custom Click Group """
  
      def list_commands(self, context):
          """ Prevent alphabetical sorting """
          return self.commands.keys()
  
      def get_command(self, context, cmd_name):
          """ Allow command shortening """
          # Backward-compatible 'test convert' (just temporary for now FIXME)
          cmd_name = cmd_name.replace('convert', 'import')
          # Support both story & stories
          cmd_name = cmd_name.replace('story', 'stories')
          found = click.Group.get_command(self, context, cmd_name)
          if found is not None:
              return found
          matches = [command for command in self.list_commands(context)
                     if command.startswith(cmd_name)]
          if not matches:
              return None
          elif len(matches) == 1:
              return click.Group.get_command(self, context, matches[0])
          context.fail('Did you mean {}?'.format(
              listed(sorted(matches), join='or')))
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Common Options
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  def verbose_debug_quiet(function):
      """ Verbose, debug and quiet output """
  
      options = [
          click.option(
             '-v', '--verbose', is_flag=True,
             help='I want to see more details. Please, be verbose.'),
          click.option(
             '-d', '--debug', is_flag=True,
             help='Provide as much debugging details as possible.'),
          click.option(
              '-q', '--quiet', is_flag=True,
              help='Be quiet. Exit code is just enough for me.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def force_dry(function):
      """ Force and dry actions """
  
      options = [
          click.option(
              '-f', '--force', is_flag=True,
              help='Force overwriting existing stuff.'),
          click.option(
              '-n', '--dry', is_flag=True,
              help='No changes, please. Run in dry mode.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def name_filter_condition(function):
      """ Common filter option """
      options = [
          click.argument(
              'names', nargs=-1, metavar='[REGEXP]'),
          click.option(
              '--filter', 'filters', metavar='FILTER', multiple=True,
              help="Apply advanced filter (see 'pydoc fmf.filter')."),
          click.option(
              '--condition', 'conditions', metavar="EXPR", multiple=True,
              help="Use arbitrary Python expression for filtering."),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def implemented_tested_documented(function):
      """ Common story options """
  
      options = [
          click.option(
              '-i', '--implemented', is_flag=True,
              help='Implemented stories only.'),
          click.option(
              '-I', '--unimplemented', is_flag=True,
              help='Unimplemented stories only.'),
          click.option(
              '-t', '--tested', is_flag=True,
              help='Tested stories only.'),
          click.option(
              '-T', '--untested', is_flag=True,
              help='Untested stories only.'),
          click.option(
              '-d', '--documented', is_flag=True,
              help='Documented stories only.'),
          click.option(
              '-D', '--undocumented', is_flag=True,
              help='Undocumented stories only.'),
          click.option(
              '-c', '--covered', is_flag=True,
              help='Covered stories only.'),
          click.option(
              '-C', '--uncovered', is_flag=True,
              help='Uncovered stories only.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Main
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @click.option(
      '-r', '--root', metavar='PATH', default='.', show_default=True,
      help='Path to the tree root.')
  @verbose_debug_quiet
  def main(context, root, **kwargs):
      """ Test Management Tool """
      # Initialize metadata tree
      tree = tmt.Tree(root)
      tree._save_context(context)
      context.obj = tmt.utils.Common()
      context.obj.tree = tree
      # List of enabled steps
      context.obj.steps = set()
  
      # Show overview of available tests, plans and stories
      if context.invoked_subcommand is None:
          tmt.Test.overview(tree)
          tmt.Plan.overview(tree)
          tmt.Story
  
  [... the rest is omitted ...]
files: tmt/cli.py
files: # coding: utf-8
  
  """ Command line interface for the Test Management Tool """
  
  from click import echo, style
  from fmf.utils import listed
  
  import click
  import os
  
  import fmf
  import tmt
  import tmt.utils
  import tmt.convert
  import tmt.steps
  import tmt.templates
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Custom Group
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class CustomGroup(click.Group):
      """ Custom Click Group """
  
      def list_commands(self, context):
          """ Prevent alphabetical sorting """
          return self.commands.keys()
  
      def get_command(self, context, cmd_name):
          """ Allow command shortening """
          # Backward-compatible 'test convert' (just temporary for now FIXME)
          cmd_name = cmd_name.replace('convert', 'import')
          # Support both story & stories
          cmd_name = cmd_name.replace('story', 'stories')
          found = click.Group.get_command(self, context, cmd_name)
          if found is not None:
              return found
          matches = [command for command in self.list_commands(context)
                     if command.startswith(cmd_name)]
          if not matches:
              return None
          elif len(matches) == 1:
              return click.Group.get_command(self, context, matches[0])
          context.fail('Did you mean {}?'.format(
              listed(sorted(matches), join='or')))
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Common Options
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  def verbose_debug_quiet(function):
      """ Verbose, debug and quiet output """
  
      options = [
          click.option(
             '-v', '--verbose', is_flag=True,
             help='I want to see more details. Please, be verbose.'),
          click.option(
             '-d', '--debug', is_flag=True,
             help='Provide as much debugging details as possible.'),
          click.option(
              '-q', '--quiet', is_flag=True,
              help='Be quiet. Exit code is just enough for me.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def force_dry(function):
      """ Force and dry actions """
  
      options = [
          click.option(
              '-f', '--force', is_flag=True,
              help='Force overwriting existing stuff.'),
          click.option(
              '-n', '--dry', is_flag=True,
              help='No changes, please. Run in dry mode.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def name_filter_condition(function):
      """ Common filter option """
      options = [
          click.argument(
              'names', nargs=-1, metavar='[REGEXP]'),
          click.option(
              '--filter', 'filters', metavar='FILTER', multiple=True,
              help="Apply advanced filter (see 'pydoc fmf.filter')."),
          click.option(
              '--condition', 'conditions', metavar="EXPR", multiple=True,
              help="Use arbitrary Python expression for filtering."),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  def implemented_tested_documented(function):
      """ Common story options """
  
      options = [
          click.option(
              '-i', '--implemented', is_flag=True,
              help='Implemented stories only.'),
          click.option(
              '-I', '--unimplemented', is_flag=True,
              help='Unimplemented stories only.'),
          click.option(
              '-t', '--tested', is_flag=True,
              help='Tested stories only.'),
          click.option(
              '-T', '--untested', is_flag=True,
              help='Untested stories only.'),
          click.option(
              '-d', '--documented', is_flag=True,
              help='Documented stories only.'),
          click.option(
              '-D', '--undocumented', is_flag=True,
              help='Undocumented stories only.'),
          click.option(
              '-c', '--covered', is_flag=True,
              help='Covered stories only.'),
          click.option(
              '-C', '--uncovered', is_flag=True,
              help='Uncovered stories only.'),
          ]
  
      for option in reversed(options):
          function = option(function)
      return function
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Main
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @click.option(
      '-r', '--root', metavar='PATH', default='.', show_default=True,
      help='Path to the tree root.')
  @verbose_debug_quiet
  def main(context, root, **kwargs):
      """ Test Management Tool """
      # Initialize metadata tree
      tree = tmt.Tree(root)
      tree._context = context
      context.obj = tmt.utils.Common()
      context.obj.tree = tree
      # List of enabled steps
      context.obj.steps = set()
  
      # Show overview of available tests, plans and stories
      if context.invoked_subcommand is None:
          tmt.Test.overview(tree)
          tmt.Plan.overview(tree)
          tmt.Story
  
  [... the rest is omitted ...]
files: tmt/cli.py
files: # coding: utf-8
  
  """ Test Metadata Utilities """
  
  from click import style, echo, wrap_text
  
  from collections import OrderedDict
  import unicodedata
  import subprocess
  import fmf.utils
  import pprint
  import shlex
  import select
  import yaml
  import re
  import io
  import os
  
  log = fmf.utils.Logging('tmt').logger
  
  # Default workdir root and max
  WORKDIR_ROOT = '/var/tmp/tmt'
  WORKDIR_MAX = 1000
  
  # Hierarchy indent
  INDENT = 4
  
  # Simple runner script name
  RUNNER = 'run.sh'
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Common
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class Common(object):
      """
      Common shared stuff
  
      Takes care of command line context and workdir handling.
      """
  
      # Command line context, workdir and status
      _context = None
      _workdir = None
  
      def __init__(self, parent=None, name=None):
          """ Initialize name and relation with the parent object """
          # Use lowercase class name as the default name
          self.name = name or self.__class__.__name__.lower()
          self.parent = parent
  
      def __str__(self):
          """ Name is the default string representation """
          return self.name
  
      @classmethod
      def _save_context(cls, context):
          """ Save provided command line context for future use """
          cls._context = context
  
      @classmethod
      def _opt(cls, option, default=None):
          """ Get an option from the command line context (class version) """
          if cls._context is None:
              return None
          return cls._context.params.get(option, default)
  
      def opt(self, option, default=None):
          """
          Get an option from the command line context
  
          Checks also parent options. For flags (boolean values) parent's
          True wins over child's False (e.g. run --verbose enables verbose
          mode for all included plans and steps).
          """
          # Check local option
          local = default
          if self._context is not None:
              local = self._context.params.get(option, default)
          # Check parent option
          parent = None
          if self.parent:
              parent = self.parent.opt(option)
          # Special handling for flags (parent's yes wins)
          if isinstance(parent, bool):
              return parent if parent else local
          return parent if parent is not None else local
  
      def _level(self):
          """ Hierarchy level """
          if self.parent is None:
              return -1
          else:
              return self.parent._level() + 1
  
      def _indent(self, key, value=None, color=None, shift=0):
          """ Indent message according to the object hierarchy """
          level = self._level() + shift
          indent = ' ' * INDENT * level
          deeper = ' ' * INDENT * (level + 1)
          if color is not None:
              key = style(key, fg=color)
          if value is None:
              message = key
          else:
              # Multiline content indented deeper
              if isinstance(value, str):
                  lines = value.splitlines()
                  if len(lines) > 1:
                      value = ''.join([f"\n{deeper}{line}" for line in lines])
              message = f'{key}: {value}'
          return indent + message
  
      def _log(self, message):
          """ Append provided message to the current log """
          with open(os.path.join(self.workdir, 'log.txt'), 'a') as log:
              log.write(message + '\n')
  
      def info(self, key, value=None, color=None, shift=0):
          """ Show a message unless in quiet mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if not self.opt('quiet'):
              echo(self._indent(key, value, color, shift))
  
      def verbose(self, key, value=None, color=None, shift=0):
          """ Show message if in verbose or debug mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if self.opt('verbose') or self.opt('debug'):
              echo(self._indent(key, value, color, shift))
  
      def debug(self, key, value=None, color=None, shift=1):
          """ Show message if in debug mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if self.opt('debug'):
              echo(self._indent(key, value, color, shift))
  
      def _run(self, command, cwd, shell):
          """ Run command, capture the output """
          # Create the process
          process = subprocess.Popen(
              command, cwd=cwd, shell=shell,
              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          descriptors = [process.stdout.fileno(), process.stderr.fileno()]
          stdout = ''
          stderr = ''
  
          # Capture the output
          while process.poll() is None:
              # Check which file descriptors are ready for read
              selected = select.select(descriptors, [], [])
              for descriptor in selected[0]:
                  # Handle stdout
                  if descriptor == process.stdout.fileno():
  
  
  [... the rest is omitted ...]
files: tmt/utils.py
files: # coding: utf-8
  
  """ Test Metadata Utilities """
  
  from click import style, echo, wrap_text
  
  from collections import OrderedDict
  import unicodedata
  import subprocess
  import fmf.utils
  import pprint
  import shlex
  import select
  import yaml
  import re
  import io
  import os
  
  log = fmf.utils.Logging('tmt').logger
  
  # Default workdir root and max
  WORKDIR_ROOT = '/var/tmp/tmt'
  WORKDIR_MAX = 1000
  
  # Hierarchy indent
  INDENT = 4
  
  # Simple runner script name
  RUNNER = 'run.sh'
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Common
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class Common(object):
      """
      Common shared stuff
  
      Takes care of command line context and workdir handling.
      """
  
      # Command line context, workdir and status
      _context = None
      _workdir = None
  
      def __init__(self, parent=None, name=None):
          """ Initialize name and relation with the parent object """
          # Use lowercase class name as the default name
          self.name = name or self.__class__.__name__.lower()
          self.parent = parent
  
      def __str__(self):
          """ Name is the default string representation """
          return self.name
  
      @classmethod
      def _opt(cls, option, default=None):
          """ Get an option from the command line context (class version) """
          if cls._context is None:
              return None
          return cls._context.params.get(option, default)
  
      def opt(self, option, default=None):
          """
          Get an option from the command line context
  
          Checks also parent options. For flags (boolean values) parent's
          True wins over child's False (e.g. run --verbose enables verbose
          mode for all included plans and steps).
          """
          # Check local option
          local = default
          if self._context is not None:
              local = self._context.params.get(option, default)
          # Check parent option
          parent = None
          if self.parent:
              parent = self.parent.opt(option)
          # Special handling for flags (parent's yes wins)
          if isinstance(parent, bool):
              return parent if parent else local
          return parent if parent is not None else local
  
      def _level(self):
          """ Hierarchy level """
          if self.parent is None:
              return -1
          else:
              return self.parent._level() + 1
  
      def _indent(self, key, value=None, color=None, shift=0):
          """ Indent message according to the object hierarchy """
          level = self._level() + shift
          indent = ' ' * INDENT * level
          deeper = ' ' * INDENT * (level + 1)
          if color is not None:
              key = style(key, fg=color)
          if value is None:
              message = key
          else:
              # Multiline content indented deeper
              if isinstance(value, str):
                  lines = value.splitlines()
                  if len(lines) > 1:
                      value = ''.join([f"\n{deeper}{line}" for line in lines])
              message = f'{key}: {value}'
          return indent + message
  
      def _log(self, message):
          """ Append provided message to the current log """
          with open(os.path.join(self.workdir, 'log.txt'), 'a') as log:
              log.write(message + '\n')
  
      def info(self, key, value=None, color=None, shift=0):
          """ Show a message unless in quiet mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if not self.opt('quiet'):
              echo(self._indent(key, value, color, shift))
  
      def verbose(self, key, value=None, color=None, shift=0):
          """ Show message if in verbose or debug mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if self.opt('verbose') or self.opt('debug'):
              echo(self._indent(key, value, color, shift))
  
      def debug(self, key, value=None, color=None, shift=1):
          """ Show message if in debug mode """
          self._log(self._indent(key, value, color=None, shift=shift))
          if self.opt('debug'):
              echo(self._indent(key, value, color, shift))
  
      def _run(self, command, cwd, shell):
          """ Run command, capture the output """
          # Create the process
          process = subprocess.Popen(
              command, cwd=cwd, shell=shell,
              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          descriptors = [process.stdout.fileno(), process.stderr.fileno()]
          stdout = ''
          stderr = ''
  
          # Capture the output
          while process.poll() is None:
              # Check which file descriptors are ready for read
              selected = select.select(descriptors, [], [])
              for descriptor in selected[0]:
                  # Handle stdout
                  if descriptor == process.stdout.fileno():
                      line = process.stdout.readline().decode('utf-8')
                      stdout += line
                      if line != '':
                          self.debug('out', line.r
  
  [... the rest is omitted ...]
files: tmt/utils.py