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::
  
      $ tmt test show .
      $ tmt run test --name .
  
  
  Lint Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests lint`` to check defined test metadata against the
  L1 Metadata Specification::
  
      $ tmt tests lint
      /tests/docs
      pass test script must be defined
      pass directory path must be defined
      warn summary should not exceed 50 characters
  
      /tests/ls
      pass test script must be defined
      pass directory path must be defined
  
  
  Create Tests
  ------------------------------------------------------------------
  
  Use ``tmt test create`` to create a new test based on a template::
  
      $ tmt test create /tests/smoke
      Template (shell or beakerlib): shell
      Directory '/home/psss/git/tmt/tests/smoke' created.
      Test metadata '/home/psss/git/tmt/tests/smoke/main.fmf' created.
      Test script '/home/psss/git/tmt/tests/smoke/test.sh' created.
  
  Specify templates non-interactively with ``-t`` or ``--template``::
  
      $ tmt tests create --template shell /tests/smoke
      $ tmt tests create --t beakerlib /tests/smoke
  
  Use ``-f`` or ``--force`` option to overwrite existing files.
  
  
  Convert Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests convert`` to gather old metadata stored in
  different sources and convert them into the new ``fmf`` format.
  By default ``Makefile`` and ``PURPOSE`` files in the current
  directory are inspected and the ``Nitrate`` test case management
  system is contacted to gather all related metadata::
  
      makefile ..... summary, component, duration
      purpose ...... description
      nitrate ...... environment, relevancy
  
  In order to fetch data from Nitrate you need to have ``nitrate``
  module installed. For each test case found in Nitrate separate fmf
  file is created with metadata unique to that case. Common metadata
  found in all test cases are stored in ``main.fmf``. You can use
  ``--no-nitrate`` to disable Nitrate integration, ``--no-makefile``
  and ``--no-purpose`` switches to disable the other two metadata
  sources.
  
  Example output of metadata conversion::
  
      $ tmt test convert
      Checking the '/home/psss/git/tmt/examples/convert' directory.
      Makefile found in '/home/psss/git/tmt/examples/convert/Makefile'.
      test: /tmt/smoke
      description: Simple smoke test
      component: tmt
      duration: 5m
      Purpose found in '/home/psss/git/tmt/examples/convert/PURPOSE'.
      description:
      Just run 'tmt --help' to make sure the binary is sane.
      This is really that simple. Nothing more here. Really.
      Nitrate test case found 'TC#0603489'.
      contact: Petr Šplíchal <psplicha@redhat.com>
      environment:
      {'TEXT': 'Text with spaces', 'X': '1', 'Y': '2', 'Z': '3'}
      relevancy:
      distro = rhel-4, rhel-5: False
      distro = rhel-6: False
      Metadata successfully stored into '/home/psss/git/tmt/examples/convert/main.fmf'.
  
  And here's the resulting ``main.fmf`` file::
  
      component: tmt
      contact: Petr Šplíchal <psplicha@redhat.com>
      description: |
          Just run 'tmt --help' to make sure the binary is sane.
          This is really that simple. Nothing more here. Really.
      duration: 5m
      environment:
          TEXT: Text with spaces
          X: '1'
          Y: '2'
          Z: '3'
      relevancy: |
          distro = rhel-4, rhel-5: False
          distro = rhel-6: False
      summary: Simple smoke test
  
  
  Plans
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``plans`` command is used to investigate and handle plans.
  See the :ref:`specification` for details about the L2 Metadata.
  
  
  Explore Plans
  ------------------------------------------------------------------
  
  Exploring ``plans`` is similar to using ``tests``::
  
      $ tmt plans
      Found 3 plans: /plans/basic, /plans/helps and /plans/smoke.
  
  Use ``tmt plans ls`` and ``tmt plans show`` to output plan names
  and detailed plan information, respectively::
  
      $ tmt plans ls
      /plans/basic
      /plans/helps
      /plans/smoke
  
      $ tmt plans show
      /plans/basic
           summary Essential command line features
          discover
               how fmf
        repository https://github.com/psss/tmt
          revision devel
            filter tier: 0,1
           prepare
               how ansible
         playbooks plans/packages.yml
  
      /plans/helps
           summary Check help messages
          discover
               how shell
  
      /plans/smoke
           summary Just a basic smoke test
           execute
               how shell
            script tmt --help
  
  Verbose output and filtering are similar as for exploring tests.
  See `Explore Tests`_ and `Filter Tests`_ for more examples.
  
  
  Create Plans
  ------------------------------------------------------------------
  
  Use ``tmt plan create`` to create a new plan with templates::
  
      tmt plans create --template mini /plans/smoke
      tmt plans create --t full /plans/features
  
  Options ``-f`` or ``--force`` can be used to overwrite existing
  files.
  
  
  Inherit Plans
  ------------------------------------------------------------------
  
  If several plans share similar content it is possible to use
  inheritance to prevent unnecessary duplication of the data::
  
      discover:
          how: fmf
          repository: https://github.com/psss/tmt
      prepare:
          how: ansible
          playbooks: ansible/packages.yml
      execute:
          how: beakerlib
  
      /basic:
          summary: Quick set of basic functionality tests
          discover+:
              filter: tier:1
  
      /features:
          summary: Detailed tests for individual features
          discover+:
              filter: tier:2
  
  Note that a ``+`` sign should be used if you want to extend the
  parent data instead of replacing them. See the `fmf features`_
  documentation for a detailed description of the hierarchy,
  inheritance and merging attributes.
  
  .. _fmf features: https://fmf.readthedocs.io/en/latest/features.html
  
  
  Stories
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``stories`` command is used to investigate and handle stories.
  See the :ref:`specification` for details about the L3 Metadata.
  
  
  Explore Stories
  ------------------------------------------------------------------
  
  Exploring ``stories`` is quite similar to using ``tests`` or
  ``plans``::
  
      $ tmt stories
      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 102 more.
  
  The ``tmt stories ls`` and ``tmt stories show`` commands output
  the names and the detailed information, respectively::
  
      $ tmt stories ls
      /spec/core/description
      /spec/core/order
      /spec/core/summary
      ...
  
      $ tmt stories show
      /spec/core/description
           summary Detailed description of the object
             story I want to have common core attributes used consistently
                   across all metadata levels.
       description Multiline ``string`` describing all important aspects of
                   the object. Usually spans across several paragraphs. For
                   detailed examples using a dedicated attributes 'examples'
                   should be considered.
       ...
  
  Verbose output and filtering are similar as for exploring tests.
  See `Explore Tests`_ and `Filter Tests`_ for more examples.
  
  
  Filter Stories
  ------------------------------------------------------------------
  
  Additionally, and specifically to stories, special flags are
  available for binary status filtering::
  
      $ tmt stories show --help | grep only
        -i, --implemented    Implemented stories only.
        -I, --unimplemented  Unimplemented stories only.
        -t, --tested         Tested stories only.
        -T, --untested       Untested stories only.
        -d, --documented     Documented stories only.
        -D, --undocumented   Undocumented stories only.
        -c, --covered        Covered stories only.
        -C, --uncovered      Uncovered stories only.
  
      $ tmt stories ls --implemented
      /spec/core/summary
      /stories/api/plan/attributes/artifact
      /stories/api/plan/attributes/gate
      ...
  
      $ tmt stories show --documented
      /stories/cli/common/debug
           summary Print out everything tmt is doing
             story I want to have common command line options consistenly used
                   across all supported commands and subcommands.
           example tmt run -d
                   tmt run --debug
       implemented /tmt/cli
        documented /tmt/cli
      ...
  
  In order to select stories under the current working directory use
  the single dot notation::
  
      $ tmt story show .
  
  
  Story Coverage
  ------------------------------------------------------------------
  
  Current status of the code, test and documentation coverage can be
  checked using the ``tmt story coverage`` command::
  
      $ tmt story coverage
      code test docs story
      todo todo todo /spec/core/description
      todo todo todo /spec/core/order
      done todo todo /spec/core/summary
      ...
      done todo todo /stories/cli/usability/completion
       39%   9%   9% from 109 stories
  
  
  Create Stories
  ------------------------------------------------------------------
  
  The ``tmt story create`` command can be used to create a new story
  based on given template::
  
      tmt story create --template full /stories/usability
  
  Use ``-f`` or ``--force`` to overwrite existing files.
  
  
  
  Run
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``tmt run`` command is used to execute tests. By default all
  steps for all discovered test plans are executed::
  
      $ tmt test run
      /var/tmp/tmt/run-080
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
          provision
          prepare
          execute
              how: beakerlib
              result: 2 tests passed, 0 tests failed
          report
          finish
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
          provision
          prepare
          execute
              how: shell
              result: 2 tests passed, 0 tests failed
          report
          finish
  
      /plans/smoke
          discover
              how: shell
              tests: 0 tests selected
          provision
          prepare
          execute
              how: shell
              result: 1 test passed, 0 tests failed
          report
          finish
  
  
  Dry run mode is enabled with the ``--dry`` option::
  
      tmt run --dry
  
  
  
  Select Plans
  ------------------------------------------------------------------
  
  Choose which plans should be executed::
  
      $ tmt run plan --name basic
      /var/tmp/tmt/run-083
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
          provision
          prepare
          execute
              how: beakerlib
              result: 2 tests passed, 0 tests failed
          report
          finish
  
  
  Select Tests
  ------------------------------------------------------------------
  
  Run only a subset of available tests across all plans::
  
      $ tmt run test --filter tier:1
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 1 test selected
          ...
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 0 tests selected
          ...
  
      /plans/smoke
          discover
              how: shell
              tests: 0 tests selected
          ...
  
  
  Select Steps
  ------------------------------------------------------------------
  
  The test execution is divided into the following six steps:
  ``discover``, ``provision``, ``prepare``, ``execute``, ``report``
  and ``finish``. See the :ref:`specification` for more details
  about individual steps.
  
  It is possible to execute only selected steps. For example in
  order to see which tests would be executed without actually
  running them choose the ``discover`` step::
  
      $ tmt run discover
      /var/tmp/tmt/run-085
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
  
  Use ``--verbose`` and ``--debug`` to enable more detailed output
  such as list of individual tests or showing the progress of the
  test environment provisioning::
  
      $ tmt run discover --verbose
      /var/tmp/tmt/run-767
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
                  /one/tests/docs
                  /one/tests/ls
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
                  /help/main
                  /help/test
                  /help/plan
                  /help/smoke
  
  You can also choose multiple steps to be executed::
  
      tmt run discover provision prepare
  
  Arguments for particular step can be specified after the step
  name, options for all steps should go to the ``run`` command::
  
      tmt run discover provision --debug  # debug output for provision only
      tmt run --debug discover provision  # debug output for all steps
  
  In order to execute all test steps while providing arguments to
  some of them it is possible to use the ``--all`` option::
  
      tmt run --all provision --how=local
  
  
  Debug Tests
  ------------------------------------------------------------------
  
  For debugging tests, the execution is anticipated to be split into
  separate invocations for provisioning, repeatedly (re)executing
  the test and cleaning up::
  
      tmt run --id <ID> --until provision  # prepare the testing environment
  
      tmt run -i <ID> execute              # ... and update the test
      tmt run -i <ID> execute              # ... and update the test again
      tmt run -i <ID> execute              # ... until you're done
  
      tmt run -i <ID> report finish
  
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 against the
  L1 Metadata Specification::
  
      $ tmt tests lint
      /tests/docs
      pass test script must be defined
      pass directory path must be defined
      warn summary should not exceed 50 characters
  
      /tests/ls
      pass test script must be defined
      pass directory path must be defined
  
  
  Create Tests
  ------------------------------------------------------------------
  
  Use ``tmt test create`` to create a new test based on a template::
  
      $ tmt test create /tests/smoke
      Template (shell or beakerlib): shell
      Directory '/home/psss/git/tmt/tests/smoke' created.
      Test metadata '/home/psss/git/tmt/tests/smoke/main.fmf' created.
      Test script '/home/psss/git/tmt/tests/smoke/test.sh' created.
  
  Specify templates non-interactively with ``-t`` or ``--template``::
  
      $ tmt tests create --template shell /tests/smoke
      $ tmt tests create --t beakerlib /tests/smoke
  
  Use ``-f`` or ``--force`` option to overwrite existing files.
  
  
  Convert Tests
  ------------------------------------------------------------------
  
  Use ``tmt tests convert`` to gather old metadata stored in
  different sources and convert them into the new ``fmf`` format.
  By default ``Makefile`` and ``PURPOSE`` files in the current
  directory are inspected and the ``Nitrate`` test case management
  system is contacted to gather all related metadata::
  
      makefile ..... summary, component, duration
      purpose ...... description
      nitrate ...... environment, relevancy
  
  In order to fetch data from Nitrate you need to have ``nitrate``
  module installed. For each test case found in Nitrate separate fmf
  file is created with metadata unique to that case. Common metadata
  found in all test cases are stored in ``main.fmf``. You can use
  ``--no-nitrate`` to disable Nitrate integration, ``--no-makefile``
  and ``--no-purpose`` switches to disable the other two metadata
  sources.
  
  Example output of metadata conversion::
  
      $ tmt test convert
      Checking the '/home/psss/git/tmt/examples/convert' directory.
      Makefile found in '/home/psss/git/tmt/examples/convert/Makefile'.
      test: /tmt/smoke
      description: Simple smoke test
      component: tmt
      duration: 5m
      Purpose found in '/home/psss/git/tmt/examples/convert/PURPOSE'.
      description:
      Just run 'tmt --help' to make sure the binary is sane.
      This is really that simple. Nothing more here. Really.
      Nitrate test case found 'TC#0603489'.
      contact: Petr Šplíchal <psplicha@redhat.com>
      environment:
      {'TEXT': 'Text with spaces', 'X': '1', 'Y': '2', 'Z': '3'}
      relevancy:
      distro = rhel-4, rhel-5: False
      distro = rhel-6: False
      Metadata successfully stored into '/home/psss/git/tmt/examples/convert/main.fmf'.
  
  And here's the resulting ``main.fmf`` file::
  
      component: tmt
      contact: Petr Šplíchal <psplicha@redhat.com>
      description: |
          Just run 'tmt --help' to make sure the binary is sane.
          This is really that simple. Nothing more here. Really.
      duration: 5m
      environment:
          TEXT: Text with spaces
          X: '1'
          Y: '2'
          Z: '3'
      relevancy: |
          distro = rhel-4, rhel-5: False
          distro = rhel-6: False
      summary: Simple smoke test
  
  
  Plans
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``plans`` command is used to investigate and handle plans.
  See the :ref:`specification` for details about the L2 Metadata.
  
  
  Explore Plans
  ------------------------------------------------------------------
  
  Exploring ``plans`` is similar to using ``tests``::
  
      $ tmt plans
      Found 3 plans: /plans/basic, /plans/helps and /plans/smoke.
  
  Use ``tmt plans ls`` and ``tmt plans show`` to output plan names
  and detailed plan information, respectively::
  
      $ tmt plans ls
      /plans/basic
      /plans/helps
      /plans/smoke
  
      $ tmt plans show
      /plans/basic
           summary Essential command line features
          discover
               how fmf
        repository https://github.com/psss/tmt
          revision devel
            filter tier: 0,1
           prepare
               how ansible
         playbooks plans/packages.yml
  
      /plans/helps
           summary Check help messages
          discover
               how shell
  
      /plans/smoke
           summary Just a basic smoke test
           execute
               how shell
            script tmt --help
  
  Verbose output and filtering are similar as for exploring tests.
  See `Explore Tests`_ and `Filter Tests`_ for more examples.
  
  
  Create Plans
  ------------------------------------------------------------------
  
  Use ``tmt plan create`` to create a new plan with templates::
  
      tmt plans create --template mini /plans/smoke
      tmt plans create --t full /plans/features
  
  Options ``-f`` or ``--force`` can be used to overwrite existing
  files.
  
  
  Inherit Plans
  ------------------------------------------------------------------
  
  If several plans share similar content it is possible to use
  inheritance to prevent unnecessary duplication of the data::
  
      discover:
          how: fmf
          repository: https://github.com/psss/tmt
      prepare:
          how: ansible
          playbooks: ansible/packages.yml
      execute:
          how: beakerlib
  
      /basic:
          summary: Quick set of basic functionality tests
          discover+:
              filter: tier:1
  
      /features:
          summary: Detailed tests for individual features
          discover+:
              filter: tier:2
  
  Note that a ``+`` sign should be used if you want to extend the
  parent data instead of replacing them. See the `fmf features`_
  documentation for a detailed description of the hierarchy,
  inheritance and merging attributes.
  
  .. _fmf features: https://fmf.readthedocs.io/en/latest/features.html
  
  Stories
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``stories`` command is used to investigate and handle stories.
  See the :ref:`specification` for details about the L3 Metadata.
  
  
  Explore Stories
  ------------------------------------------------------------------
  
  Exploring ``stories`` is quite similar to using ``tests`` or
  ``plans``::
  
      $ tmt stories
      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 102 more.
  
  The ``tmt stories ls`` and ``tmt stories show`` commands output
  the names and the detailed information, respectively::
  
      $ tmt stories ls
      /spec/core/description
      /spec/core/order
      /spec/core/summary
      ...
  
      $ tmt stories show
      /spec/core/description
           summary Detailed description of the object
             story I want to have common core attributes used consistently
                   across all metadata levels.
       description Multiline ``string`` describing all important aspects of
                   the object. Usually spans across several paragraphs. For
                   detailed examples using a dedicated attributes 'examples'
                   should be considered.
       ...
  
  Verbose output and filtering are similar as for exploring tests.
  See `Explore Tests`_ and `Filter Tests`_ for more examples.
  
  
  Filter Stories
  ------------------------------------------------------------------
  
  Additionally, and specifically to stories, special flags are
  available for binary status filtering::
  
      $ tmt stories show --help | grep only
        -i, --implemented    Implemented stories only.
        -I, --unimplemented  Unimplemented stories only.
        -t, --tested         Tested stories only.
        -T, --untested       Untested stories only.
        -d, --documented     Documented stories only.
        -D, --undocumented   Undocumented stories only.
        -c, --covered        Covered stories only.
        -C, --uncovered      Uncovered stories only.
  
      $ tmt stories ls --implemented
      /spec/core/summary
      /stories/api/plan/attributes/artifact
      /stories/api/plan/attributes/gate
      ...
  
      $ tmt stories show --documented
      /stories/cli/common/debug
           summary Print out everything tmt is doing
             story I want to have common command line options consistenly used
                   across all supported commands and subcommands.
           example tmt run -d
                   tmt run --debug
       implemented /tmt/cli
        documented /tmt/cli
      ...
  
  
  Story Coverage
  ------------------------------------------------------------------
  
  Current status of the code, test and documentation coverage can be
  checked using the ``tmt story coverage`` command::
  
      $ tmt story coverage
      code test docs story
      todo todo todo /spec/core/description
      todo todo todo /spec/core/order
      done todo todo /spec/core/summary
      ...
      done todo todo /stories/cli/usability/completion
       39%   9%   9% from 109 stories
  
  
  Create Stories
  ------------------------------------------------------------------
  
  The ``tmt story create`` command can be used to create a new story
  based on given template::
  
      tmt story create --template full /stories/usability
  
  Use ``-f`` or ``--force`` to overwrite existing files.
  
  
  
  Run
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  The ``tmt run`` command is used to execute tests. By default all
  steps for all discovered test plans are executed::
  
      $ tmt test run
      /var/tmp/tmt/run-080
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
          provision
          prepare
          execute
              how: beakerlib
              result: 2 tests passed, 0 tests failed
          report
          finish
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
          provision
          prepare
          execute
              how: shell
              result: 2 tests passed, 0 tests failed
          report
          finish
  
      /plans/smoke
          discover
              how: shell
              tests: 0 tests selected
          provision
          prepare
          execute
              how: shell
              result: 1 test passed, 0 tests failed
          report
          finish
  
  
  Dry run mode is enabled with the ``--dry`` option::
  
      tmt run --dry
  
  
  
  Select Plans
  ------------------------------------------------------------------
  
  Choose which plans should be executed::
  
      $ tmt run plan --name basic
      /var/tmp/tmt/run-083
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
          provision
          prepare
          execute
              how: beakerlib
              result: 2 tests passed, 0 tests failed
          report
          finish
  
  
  Select Tests
  ------------------------------------------------------------------
  
  Run only a subset of available tests across all plans::
  
      $ tmt run test --filter tier:1
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 1 test selected
          ...
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 0 tests selected
          ...
  
      /plans/smoke
          discover
              how: shell
              tests: 0 tests selected
          ...
  
  
  Select Steps
  ------------------------------------------------------------------
  
  The test execution is divided into the following six steps:
  ``discover``, ``provision``, ``prepare``, ``execute``, ``report``
  and ``finish``. See the :ref:`specification` for more details
  about individual steps.
  
  It is possible to execute only selected steps. For example in
  order to see which tests would be executed without actually
  running them choose the ``discover`` step::
  
      $ tmt run discover
      /var/tmp/tmt/run-085
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
  
  Use ``--verbose`` and ``--debug`` to enable more detailed output
  such as list of individual tests or showing the progress of the
  test environment provisioning::
  
      $ tmt run discover --verbose
      /var/tmp/tmt/run-767
  
      /plans/basic
          discover
              how: fmf
              repository: https://github.com/psss/tmt
              revision: devel
              filter: tier: 0,1
              tests: 2 tests selected
                  /one/tests/docs
                  /one/tests/ls
  
      /plans/helps
          discover
              how: shell
              directory: /home/psss/git/tmt
              tests: 4 tests selected
                  /help/main
                  /help/test
                  /help/plan
                  /help/smoke
  
  You can also choose multiple steps to be executed::
  
      tmt run discover provision prepare
  
  Arguments for particular step can be specified after the step
  name, options for all steps should go to the ``run`` command::
  
      tmt run discover provision --debug  # debug output for provision only
      tmt run --debug discover provision  # debug output for all steps
  
  In order to execute all test steps while providing arguments to
  some of them it is possible to use the ``--all`` option::
  
      tmt run --all provision --how=local
  
  
  Debug Tests
  ------------------------------------------------------------------
  
  For debugging tests, the execution is anticipated to be split into
  separate invocations for provisioning, repeatedly (re)executing
  the test and cleaning up::
  
      tmt run --id <ID> --until provision  # prepare the testing environment
  
      tmt run -i <ID> execute              # ... and update the test
      tmt run -i <ID> execute              # ... and update the test again
      tmt run -i <ID> execute              # ... until you're done
  
      tmt run -i <ID> report finish
  
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
  
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
  
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
  
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
  
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
  
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
  
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,
              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 for quick inspection'))
          elif len(self.summary) > 50:
              echo(verdict(2, 'summary should not exceed 50 characters'))
  
      def export(self, format_='yaml', keys=None):
          """
          Export test data into requested format
  
          In addition to 'yaml' and 'dict' it supports also a special
          format 'execute' used by the execute step which returns
          (test-name, test-data) tuples.
          """
          if format_ != 'execute':
              return super(Test, self).export(format_, keys)
  
          # Prepare special format for the executor
          name = self.name
          data = dict()
          data['test'] = self.test
          data['path'] = self.path
          if self.duration is not None:
              data['duration'] = self.duration
          if self.environment is not None:
              data['environment'] = ' '.join(
                  tmt.utils.dict_to_shell(self.environment))
          return name, data
  
  
  class Plan(Node):
      """ Plan object (L2 Metadata) """
  
      def __init__(self, node, run=None):
          """ Initialize the plan """
          super(Plan, self).__init__(node, parent=run)
          self.summary = node.get('summary')
          self.run = run
  
          # Initialize test steps
          self.discover = tmt.steps.discover.Discover(
              self.node.get('discover'), self)
          self.provision = tmt.steps.provision.Provision(
              self.node.get('provision'), self)
          self.prepare = tmt.steps.prepare.Prepare(
              self.node.get('prepare'), self)
          self.execute = tmt.steps.execute.Execute(
              self.node.get('execute'), self)
          self.report = tmt.steps.report.Report(
              self.node.get('report'), self)
          self.finish = tmt.steps.finish.Finish(
              self.node.get('finish'), self)
  
          # Relevant artifacts & gates (convert to list if needed)
          self.artifacts = node.get('artifacts')
          if self.artifacts:
              if not isinstance(artifacts, list):
                  artifacts = [artifacts]
          self.gates = node.get('gates')
          if self.gates:
              if not isinstance(gates, list):
                  gates = [gates]
  
      @staticmethod
      def overview(tree):
          """ Show overview of available plans """
          plans = [
              style(str(plan), fg='red') for plan in tree.plans()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(plans, 'plan'),
                  ': ' if plans else '',
                  listed(plans, max=12)
              ), fg='blue'))
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new plan """
          # Prepare paths
          (directory, plan) = os.path.split(name)
          directory_path = os.path.join(tree.root, directory.lstrip('/'))
          plan_path = os.path.join(directory_path, plan + '.fmf')
  
          # Create directory & plan
          tmt.utils.create_directory(directory_path, 'plan directory')
          try:
              content = tmt.templates.PLAN[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=plan_path, content=content,
              name='plan', force=force)
  
      def steps(self, enabled=True, disabled=False, names=False, skip=[]):
          """
          Iterate over enabled / all steps
  
          Yields instances of all enabled steps by default. Use 'names' to
          yield step names only and 'disabled=True' to iterate over all.
          Use 'skip' to pass the list of steps to be skipped.
          """
          for name in tmt.steps.STEPS:
              if name in skip:
                  continue
              step = getattr(self, name)
              if (enabled and step.enabled or disabled and not step.enabled):
                  yield name if names else step
  
      def show(self):
          """ Show plan details """
          self.ls(summary=True)
          for step in self.steps(disabled=True):
              step.show()
          if self.opt('verbose'):
              self._sources()
  
      def lint(self):
          """ Check plan against the L2 metadata specification. """
          self.ls()
          execute = self.node.get('execute')
          echo(verdict(execute is not None, 'execute step must be defined'))
          if self.summary is None:
              echo(verdict(2, 'summary is very useful for quick inspection'))
          elif len(self.summary) > 50:
              echo(verdict(2, 'summary should not exceed 50 characters'))
  
      def go(self):
          """ Execute the plan """
          # Show plan name and summary (one blank line to separate plans)
          self.info('')
          self.info(self.name, color='red')
          if self.summary:
              self.verbose('summary', self.summary, 'green')
          # Wake up all steps
          for step in self.steps(disabled=True):
              step.wake()
          # Run enabled steps except 'finish'
          try:
              for step in self.steps(skip=['finish']):
                  step.go()
          # Make sure we run 'finish' step always if enabled
          finally:
              if self.finish.enabled:
                  self.finish.go()
  
  
  class Story(Node):
      """ User story object """
  
      # Supported attributes (listed in display order)
      _keys = [
          'summary',
          'story',
          'description',
          'example',
          'implemented',
          'tested',
          'documented',
          ]
  
      def __init__(self, node):
          """ Initialize the story """
          super(Story, self).__init__(node)
          self.summary = node.get('summary')
          # Get all supported attributes
          for key in self._keys:
              setattr(self, key, self.node.get(key))
  
      def _match(
          self, implemented, tested, documented, covered,
          unimplemented, untested, undocumented, uncovered):
          """ Return true if story matches given conditions """
          if implemented and not self.implemented:
              return False
          if tested and not self.tested:
              return False
          if documented and not self.documented:
              return False
          if unimplemented and self.implemented:
              return False
          if untested and self.tested:
              return False
          if undocumented and self.documented:
              return False
          if uncovered and self.implemented and self.tested and self.documented:
              return False
          if covered and not (
                  self.implemented and self.tested and self.documented):
              return False
          return True
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new story """
          # Prepare paths
          (directory, story) = os.path.split(name)
          directory_path = os.path.join(tree.root, directory.lstrip('/'))
          story_path = os.path.join(directory_path, story + '.fmf')
  
          # Create directory & story
          tmt.utils.create_directory(directory_path, 'story directory')
          try:
              content = tmt.templates.STORY[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=story_path, content=content,
              name='story', force=force)
  
      @staticmethod
      def overview(tree):
          """ Show overview of available stories """
          stories = [
              style(str(story), fg='red') for story in tree.stories()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(stories, 'story'),
                  ': ' if stories else '',
                  listed(stories, max=12)
              ), fg='blue'))
  
      def show(self):
          """ Show story details """
          self.ls()
          for key in self._keys:
              value = getattr(self, key)
              if value is not None:
                  wrap = False if key == 'example' else 'auto'
                  echo(tmt.utils.format(key, value, wrap=wrap))
          if self.opt('verbose'):
              self._sources()
  
      def coverage(self, code, test, docs):
          """ Show story coverage """
          if code:
              code = bool(self.implemented)
              echo(verdict(code, good='done', bad='todo') + ' ', nl=False)
          if test:
              test = bool(self.tested)
              echo(verdict(test, good='done', bad='todo') + ' ', nl=False)
          if docs:
              docs = bool(self.documented)
              echo(verdict(docs, good='done', bad='todo') + ' ', nl=False)
          echo(self)
          return (code, test, docs)
  
      def export(self, format_='rst', title=True):
          """ Export story data into requested format """
  
          output = ''
  
          # Title
          if title:
              depth = len(re.findall('/', self.name)) - 1
              title = re.sub('.*/', '', self.name)
              output += '\n{}\n{}\n'.format(title, '=~^:-><'[depth] * len(title))
  
          # Summary, story and description
          if self.summary and self.summary != self.node.parent.get('summary'):
              output += '\n{}\n'.format(self.summary)
          if self.story != self.node.parent.get('story'):
              output += '\n*{}*\n'.format(self.story.strip())
          if self.description:
              output += '\n{}\n'.format(self.description)
  
          # Examples
          if self.example:
              output += '\nExamples::\n\n'
              output += tmt.utils.format(
                  '', self.example, wrap=False, indent=4,
                  key_color=None, value_color=None) + '\n'
  
          # Status
          if not self.node.children:
              status = []
              for coverage in ['implemented', 'tested', 'documented']:
                  if getattr(self, coverage):
                      status.append(coverage)
              output += "\nStatus: {}\n".format(
                  listed(status) if status else 'idea')
  
          return output
  
  
  class Tree(tmt.utils.Common):
      """ Test Metadata Tree """
  
      def __init__(self, path='.', tree=None):
          """ Initialize tmt tree from directory path or given fmf tree """
          self._path = path
          self._tree = tree
  
      @property
      def tree(self):
          """ Initialize tree only when accessed """
          if self._tree is None:
              try:
                  self._tree = fmf.Tree(self._path)
              except fmf.utils.RootError:
                  raise tmt.utils.GeneralError(
                      "No metadata found in the '{0}' directory.".format(
                          self._path))
              except fmf.utils.FileError as error:
                  raise tmt.utils.GeneralError(f"Invalid yaml syntax: {error}")
          return self._tree
  
      @property
      def root(self):
          """ Metadata root """
          return self.tree.root
  
      def tests(self, keys=[], names=[], filters=[], conditions=[]):
          """ Search available tests """
          # Apply possible command line options
          if Test._opt('names'):
              names.extend(Test._opt('names'))
          if Test._opt('filters'):
              filters.extend(Test._opt('filters'))
          if Test._opt('conditions'):
              conditions.extend(Test._opt('conditions'))
          # Build the list and convert to objects
          keys.append('test')
          return [Test(test) for test in self.tree.prune(
              keys=keys, names=names, filters=filters, conditions=conditions)]
  
      def plans(self, keys=[], names=[], filters=[], conditions=[], run=None):
          """ Search available plans """
          # Apply possible command line options
          if Plan._opt('names'):
              names.extend(Plan._opt('names'))
          if Plan._opt('filters'):
              filters.extend(Plan._opt('filters'))
          if Plan._opt('conditions'):
              conditions.extend(Plan._opt('conditions'))
          # Build the list and convert to objects
          keys.append('execute')
          return [Plan(plan, run=run) for plan in self.tree.prune(
              keys=keys, names=names, filters=filters, conditions=conditions)]
  
      def stories(
              self, keys=[], names=[], filters=[], conditions=[], whole=False):
          """ Search available stories """
          # Apply possible command line options
          if Story._opt('names'):
              names.extend(Story._opt('names'))
          if Story._opt('filters'):
              filters.extend(Story._opt('filters'))
          if Story._opt('conditions'):
              conditions.extend(Story._opt('conditions'))
          # Build the list and convert to objects
          keys.append('story')
          return [Story(story) for story in self.tree.prune(
              keys=keys, names=names,
              filters=filters, conditions=conditions, whole=whole)]
  
  
  class Run(tmt.utils.Common):
      """ Test run, a container of plans """
  
      def __init__(self, id_=None, tree=None):
          """ Initialize tree, workdir and plans """
          super(Run, self).__init__()
          # Save the tree
          self.tree = tree if tree else tmt.Tree('.')
          # Prepare the workdir
          self._workdir_init(id_)
          self.debug(f"Using tree '{self.tree.root}'.")
          self._plans = None
  
      @property
      def plans(self):
          """ Test plans for execution """
          if self._plans is None:
              self._plans = self.tree.plans(run=self)
          return self._plans
  
      def go(self):
          """ Go and do test steps for selected plans """
          # Show run id / workdir path
          self.info(self.workdir, color='magenta')
          # Enable all steps if none selected or --all provided
          if self.opt('all_') or not self._context.obj.steps:
              self._context.obj.steps = set(tmt.steps.STEPS)
          # Show summary and iterate over plans
          self.verbose('Found {0}.'.format(listed(self.plans, 'plan')))
          for plan in self.plans:
              plan.go()
  
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 for quick inspection'))
          elif len(self.summary) > 50:
              echo(verdict(2, 'summary should not exceed 50 characters'))
  
      def export(self, format_='yaml', keys=None):
          """
          Export test data into requested format
  
          In addition to 'yaml' and 'dict' it supports also a special
          format 'execute' used by the execute step which returns
          (test-name, test-data) tuples.
          """
          if format_ != 'execute':
              return super(Test, self).export(format_, keys)
  
          # Prepare special format for the executor
          name = self.name
          data = dict()
          data['test'] = self.test
          data['path'] = self.path
          if self.duration is not None:
              data['duration'] = self.duration
          if self.environment is not None:
              data['environment'] = ' '.join(
                  tmt.utils.dict_to_shell(self.environment))
          return name, data
  
  
  class Plan(Node):
      """ Plan object (L2 Metadata) """
  
      def __init__(self, node, run=None):
          """ Initialize the plan """
          super(Plan, self).__init__(node, parent=run)
          self.summary = node.get('summary')
          self.run = run
  
          # Initialize test steps
          self.discover = tmt.steps.discover.Discover(
              self.node.get('discover'), self)
          self.provision = tmt.steps.provision.Provision(
              self.node.get('provision'), self)
          self.prepare = tmt.steps.prepare.Prepare(
              self.node.get('prepare'), self)
          self.execute = tmt.steps.execute.Execute(
              self.node.get('execute'), self)
          self.report = tmt.steps.report.Report(
              self.node.get('report'), self)
          self.finish = tmt.steps.finish.Finish(
              self.node.get('finish'), self)
  
          # Relevant artifacts & gates (convert to list if needed)
          self.artifacts = node.get('artifacts')
          if self.artifacts:
              if not isinstance(artifacts, list):
                  artifacts = [artifacts]
          self.gates = node.get('gates')
          if self.gates:
              if not isinstance(gates, list):
                  gates = [gates]
  
      @staticmethod
      def overview(tree):
          """ Show overview of available plans """
          plans = [
              style(str(plan), fg='red') for plan in tree.plans()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(plans, 'plan'),
                  ': ' if plans else '',
                  listed(plans, max=12)
              ), fg='blue'))
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new plan """
          # Prepare paths
          (directory, plan) = os.path.split(name)
          directory_path = os.path.join(tree.root, directory.lstrip('/'))
          plan_path = os.path.join(directory_path, plan + '.fmf')
  
          # Create directory & plan
          tmt.utils.create_directory(directory_path, 'plan directory')
          try:
              content = tmt.templates.PLAN[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=plan_path, content=content,
              name='plan', force=force)
  
      def steps(self, enabled=True, disabled=False, names=False, skip=[]):
          """
          Iterate over enabled / all steps
  
          Yields instances of all enabled steps by default. Use 'names' to
          yield step names only and 'disabled=True' to iterate over all.
          Use 'skip' to pass the list of steps to be skipped.
          """
          for name in tmt.steps.STEPS:
              if name in skip:
                  continue
              step = getattr(self, name)
              if (enabled and step.enabled or disabled and not step.enabled):
                  yield name if names else step
  
      def show(self):
          """ Show plan details """
          self.ls(summary=True)
          for step in self.steps(disabled=True):
              step.show()
          if self.opt('verbose'):
              self._sources()
  
      def lint(self):
          """ Check plan against the L2 metadata specification. """
          self.ls()
          execute = self.node.get('execute')
          echo(verdict(execute is not None, 'execute step must be defined'))
          if self.summary is None:
              echo(verdict(2, 'summary is very useful for quick inspection'))
          elif len(self.summary) > 50:
              echo(verdict(2, 'summary should not exceed 50 characters'))
  
      def go(self):
          """ Execute the plan """
          # Show plan name and summary (one blank line to separate plans)
          self.info('')
          self.info(self.name, color='red')
          if self.summary:
              self.verbose('summary', self.summary, 'green')
          # Wake up all steps
          for step in self.steps(disabled=True):
              step.wake()
          # Run enabled steps except 'finish'
          try:
              for step in self.steps(skip=['finish']):
                  step.go()
          # Make sure we run 'finish' step always if enabled
          finally:
              if self.finish.enabled:
                  self.finish.go()
  
  
  class Story(Node):
      """ User story object """
  
      # Supported attributes (listed in display order)
      _keys = [
          'summary',
          'story',
          'description',
          'example',
          'implemented',
          'tested',
          'documented',
          ]
  
      def __init__(self, node):
          """ Initialize the story """
          super(Story, self).__init__(node)
          self.summary = node.get('summary')
          # Get all supported attributes
          for key in self._keys:
              setattr(self, key, self.node.get(key))
  
      def _match(
          self, implemented, tested, documented, covered,
          unimplemented, untested, undocumented, uncovered):
          """ Return true if story matches given conditions """
          if implemented and not self.implemented:
              return False
          if tested and not self.tested:
              return False
          if documented and not self.documented:
              return False
          if unimplemented and self.implemented:
              return False
          if untested and self.tested:
              return False
          if undocumented and self.documented:
              return False
          if uncovered and self.implemented and self.tested and self.documented:
              return False
          if covered and not (
                  self.implemented and self.tested and self.documented):
              return False
          return True
  
      @staticmethod
      def create(name, template, tree, force=False):
          """ Create a new story """
          # Prepare paths
          (directory, story) = os.path.split(name)
          directory_path = os.path.join(tree.root, directory.lstrip('/'))
          story_path = os.path.join(directory_path, story + '.fmf')
  
          # Create directory & story
          tmt.utils.create_directory(directory_path, 'story directory')
          try:
              content = tmt.templates.STORY[template]
          except KeyError:
              raise tmt.utils.GeneralError(
                  "Invalid template '{}'.".format(template))
          tmt.utils.create_file(
              path=story_path, content=content,
              name='story', force=force)
  
      @staticmethod
      def overview(tree):
          """ Show overview of available stories """
          stories = [
              style(str(story), fg='red') for story in tree.stories()]
          echo(style(
              'Found {}{}{}.'.format(
                  listed(stories, 'story'),
                  ': ' if stories else '',
                  listed(stories, max=12)
              ), fg='blue'))
  
      def show(self):
          """ Show story details """
          self.ls()
          for key in self._keys:
              value = getattr(self, key)
              if value is not None:
                  wrap = False if key == 'example' else 'auto'
                  echo(tmt.utils.format(key, value, wrap=wrap))
          if self.opt('verbose'):
              self._sources()
  
      def coverage(self, code, test, docs):
          """ Show story coverage """
          if code:
              code = bool(self.implemented)
              echo(verdict(code, good='done', bad='todo') + ' ', nl=False)
          if test:
              test = bool(self.tested)
              echo(verdict(test, good='done', bad='todo') + ' ', nl=False)
          if docs:
              docs = bool(self.documented)
              echo(verdict(docs, good='done', bad='todo') + ' ', nl=False)
          echo(self)
          return (code, test, docs)
  
      def export(self, format_='rst', title=True):
          """ Export story data into requested format """
  
          output = ''
  
          # Title
          if title:
              depth = len(re.findall('/', self.name)) - 1
              title = re.sub('.*/', '', self.name)
              output += '\n{}\n{}\n'.format(title, '=~^:-><'[depth] * len(title))
  
          # Summary, story and description
          if self.summary and self.summary != self.node.parent.get('summary'):
              output += '\n{}\n'.format(self.summary)
          if self.story != self.node.parent.get('story'):
              output += '\n*{}*\n'.format(self.story.strip())
          if self.description:
              output += '\n{}\n'.format(self.description)
  
          # Examples
          if self.example:
              output += '\nExamples::\n\n'
              output += tmt.utils.format(
                  '', self.example, wrap=False, indent=4,
                  key_color=None, value_color=None) + '\n'
  
          # Status
          if not self.node.children:
              status = []
              for coverage in ['implemented', 'tested', 'documented']:
                  if getattr(self, coverage):
                      status.append(coverage)
              output += "\nStatus: {}\n".format(
                  listed(status) if status else 'idea')
  
          return output
  
  
  class Tree(tmt.utils.Common):
      """ Test Metadata Tree """
  
      def __init__(self, path='.', tree=None):
          """ Initialize tmt tree from directory path or given fmf tree """
          self._path = path
          self._tree = tree
  
      @property
      def tree(self):
          """ Initialize tree only when accessed """
          if self._tree is None:
              try:
                  self._tree = fmf.Tree(self._path)
              except fmf.utils.RootError:
                  raise tmt.utils.GeneralError(
                      "No metadata found in the '{0}' directory.".format(
                          self._path))
              except fmf.utils.FileError as error:
                  raise tmt.utils.GeneralError(f"Invalid yaml syntax: {error}")
          return self._tree
  
      @property
      def root(self):
          """ Metadata root """
          return self.tree.root
  
      def tests(self, keys=[], names=[], filters=[], conditions=[]):
          """ Search available tests """
          # Apply possible command line options
          if Test._opt('names'):
              names.extend(Test._opt('names'))
          if Test._opt('filters'):
              filters.extend(Test._opt('filters'))
          if Test._opt('conditions'):
              conditions.extend(Test._opt('conditions'))
          # Build the list and convert to objects
          keys.append('test')
          return [Test(test) for test in self.tree.prune(
              keys=keys, names=names, filters=filters, conditions=conditions)]
  
      def plans(self, keys=[], names=[], filters=[], conditions=[], run=None):
          """ Search available plans """
          # Apply possible command line options
          if Plan._opt('names'):
              names.extend(Plan._opt('names'))
          if Plan._opt('filters'):
              filters.extend(Plan._opt('filters'))
          if Plan._opt('conditions'):
              conditions.extend(Plan._opt('conditions'))
          # Build the list and convert to objects
          keys.append('execute')
          return [Plan(plan, run=run) for plan in self.tree.prune(
              keys=keys, names=names, filters=filters, conditions=conditions)]
  
      def stories(
              self, keys=[], names=[], filters=[], conditions=[], whole=False):
          """ Search available stories """
          # Apply possible command line options
          if Story._opt('names'):
              names.extend(Story._opt('names'))
          if Story._opt('filters'):
              filters.extend(Story._opt('filters'))
          if Story._opt('conditions'):
              conditions.extend(Story._opt('conditions'))
          # Build the list and convert to objects
          keys.append('story')
          return [Story(story) for story in self.tree.prune(
              keys=keys, names=names,
              filters=filters, conditions=conditions, whole=whole)]
  
  
  class Run(tmt.utils.Common):
      """ Test run, a container of plans """
  
      def __init__(self, id_=None, tree=None):
          """ Initialize tree, workdir and plans """
          super(Run, self).__init__()
          # Save the tree
          self.tree = tree if tree else tmt.Tree('.')
          # Prepare the workdir
          self._workdir_init(id_)
          self.debug(f"Using tree '{self.tree.root}'.")
          self._plans = None
  
      @property
      def plans(self):
          """ Test plans for execution """
          if self._plans is None:
              self._plans = self.tree.plans(run=self)
          return self._plans
  
      def go(self):
          """ Go and do test steps for selected plans """
          # Show run id / workdir path
          self.info(self.workdir, color='magenta')
          # Enable all steps if none selected or --all provided
          if self.opt('all_') or not self._context.obj.steps:
              self._context.obj.steps = set(tmt.steps.STEPS)
          # Show summary and iterate over plans
          self.verbose('Found {0}.'.format(listed(self.plans, 'plan')))
          for plan in self.plans:
              plan.go()
  
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.overview(tree)
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Run
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(chain=True, invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @click.option(
      '-i', '--id', 'id_', help='Run id (name or directory path).', metavar="ID")
  @click.option(
      '-a', '--all', 'all_', help='Run all steps, customize some.', is_flag=True)
  @verbose_debug_quiet
  @force_dry
  def run(context, all_, id_, **kwargs):
      """ Run test steps. """
      # Initialize
      tmt.Run._save_context(context)
      run = tmt.Run(id_, context.obj.tree)
      context.obj.run = run
  
  main.add_command(run)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method to discover tests.')
  @verbose_debug_quiet
  @force_dry
  def discover(context, **kwargs):
      """ Gather and show information about test cases to be executed. """
      context.obj.steps.add('discover')
      tmt.steps.discover.Discover._save_context(context)
      return 'discover'
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for provisioning.')
  @click.option(
      '-i', '--image', metavar='IMAGE',
      help='Select image to use. Possible values depend on the method.')
  @click.option(
      '-b', '--box', metavar='BOX',
      help='Vagrant box name to use.')
  @click.option(
      '--vagrantfile', metavar='VAGRANTFILE',
      help='Vagrantfile to override initialized one and default entries.')
  @click.option(
      '-m', '--memory', metavar='MEMORY',
      help='Set memory available to guest in MB.')
  @click.option(
      '-u', '--user', metavar='USER',
      help='Username to use for all guest operations.')
  @click.option(
      '-p', '--password', metavar='PASSWORD',
      help='Password to use for login into guest system.')
  @click.option(
      '-k', '--key', metavar='PRIVATE_KEY',
      help='Private key to use for login into guest system.')
  @click.option(
      '-g', '--guest', metavar='GUEST',
      help='Select remote host to connect to (how: connect).')
  @click.option(
      '--container-pull', is_flag=True,
      help='Force pulling container image (how: container).')
  
  @verbose_debug_quiet
  @force_dry
  def provision(context, **kwargs):
      """ Provision an environment for testing (or use localhost). """
      context.obj.steps.add('provision')
      tmt.steps.provision.Provision._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for environment preparation.')
  @click.option(
      '-s', '--script', metavar='SCRIPT',
      help='Scriplet or path or URI to a script to execute.')
  @click.option(
      '-p', '--playbook', metavar='PLAYBOOK',
      help='Path or URI to ansible playbook to run.')
  @verbose_debug_quiet
  @force_dry
  def prepare(context, **kwargs):
      """ Configure environment for testing (like ansible playbook). """
      context.obj.steps.add('prepare')
      tmt.steps.prepare.Prepare._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for test execution.')
  @click.option(
      '-s', '--script', metavar='SCRIPT', multiple=True,
      help='Shell script to be executed as a test.')
  @verbose_debug_quiet
  @force_dry
  def execute(context, **kwargs):
      """ Run the tests (using the specified framework and its settings). """
      context.obj.steps.add('execute')
      tmt.steps.execute.Execute._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for result reporting.')
  @verbose_debug_quiet
  @force_dry
  def report(context, **kwargs):
      """ Provide an overview of test results and send notifications. """
      context.obj.steps.add('report')
      tmt.steps.report.Report._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for finishing tasks.')
  @verbose_debug_quiet
  @force_dry
  def finish(context, **kwargs):
      """ Additional actions to be performed after the test execution. """
      context.obj.steps.add('finish')
      tmt.steps.finish.Finish._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '--name', 'names', metavar='REGEXP', multiple=True,
      help="Regular expression to match plan name.")
  @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.")
  @verbose_debug_quiet
  def plans(context, **kwargs):
      """
      Select plans which should be executed
  
      Regular expression can be used to filter plans by name.
      Use '.' to select plans under the current working directory.
      """
      tmt.base.Plan._save_context(context)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '--name', 'names', metavar='REGEXP', multiple=True,
      help="Regular expression to match test name.")
  @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.")
  @verbose_debug_quiet
  def tests(context, **kwargs):
      """
      Select tests which should be executed
  
      Regular expression can be used to filter tests by name.
      Use '.' to select tests under the current working directory.
      """
      tmt.base.Test._save_context(context)
  
  
  @run.resultcallback()
  @click.pass_context
  def finito(context, commands, *args, **kwargs):
      """ Run tests if run defined """
      if hasattr(context.obj, 'run'):
          context.obj.run.go()
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Test
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def tests(context, **kwargs):
      """
      Manage tests (L1 metadata).
  
      Check available tests, inspect their metadata.
      Convert old metadata into the new fmf format.
      """
  
      # Show overview of available tests
      if context.invoked_subcommand is None:
          tmt.Test.overview(context.obj.tree)
  
  main.add_command(tests)
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def ls(context, **kwargs):
      """
      List available tests
  
      Regular expression can be used to filter tests by name.
      Use '.' to select tests under the current working directory.
      """
      tmt.Test._save_context(context)
      for test in context.obj.tree.tests():
          test.ls()
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def show(context, **kwargs):
      """
      Show test details
  
      Regular expression can be used to filter tests by name.
      Use '.' to select tests under the current working directory.
      """
      tmt.Test._save_context(context)
      for test in context.obj.tree.tests():
          test.show()
          echo()
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def lint(context, **kwargs):
      """
      Check tests against the L1 metadata specification
  
      Regular expression can be used to filter tests for linting.
      Use '.' to select tests under the current working directory.
      """
      tmt.Test._save_context(context)
      for test in context.obj.tree.tests():
          test.lint()
          echo()
  
  
  _test_templates = listed(tmt.templates.TEST, join='or')
  @tests.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      help='Test template ({}).'.format(_test_templates),
      prompt='Template ({})'.format(_test_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new test based on given template. """
      tmt.Test._save_context(context)
      tmt.Test.create(name, template, context.obj.tree, force)
  
  
  @tests.command(name='import')
  @click.pass_context
  @click.argument('paths', nargs=-1, metavar='[PATH]...')
  @click.option(
      '--nitrate / --no-nitrate', default=True,
      help='Import test metadata from Nitrate')
  @click.option(
      '--purpose / --no-purpose', default=True,
      help='Migrate description from PURPOSE file')
  @click.option(
      '--makefile / --no-makefile', default=True,
      help='Convert Beaker Makefile metadata')
  @verbose_debug_quiet
  @force_dry
  def import_(context, paths, makefile, nitrate, purpose, **kwargs):
      """
      Import old test metadata into the new fmf format.
  
      Accepts one or more directories where old metadata are stored.
      By default all available sources and current directory are used.
      The following test metadata are converted for each source:
  
      \b
      makefile ..... summary, component, duration, require
      purpose ...... description
      nitrate ...... contact, component, tag,
                     environment, relevancy, enabled
      """
      tmt.Test._save_context(context)
      if not paths:
          paths = ['.']
      for path in paths:
          # Make sure we've got a real directory
          path = os.path.realpath(path)
          if not os.path.isdir(path):
              raise tmt.utils.GeneralError(
                  "Path '{0}' is not a directory.".format(path))
          # Gather old metadata and store them as fmf
          common, individual = tmt.convert.read(path, makefile, nitrate, purpose)
          # Add path to common metadata if there are virtual test cases
          if individual:
              root = fmf.Tree(path).root
              common['path'] = os.path.join( '/', os.path.relpath(path, root))
          # Store common metadata
          common_path = os.path.join(path, 'main.fmf')
          tmt.convert.write(common_path, common)
          # Store individual data (as virtual tests)
          for testcase in individual:
              testcase_path = os.path.join(
                  path, str(testcase['extra-nitrate']) + '.fmf')
              tmt.convert.write(testcase_path, testcase)
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @click.option(
      '--format', 'format_', default='yaml', show_default=True, metavar='FORMAT',
      help='Output format.')
  @click.option(
      '-d', '--debug', is_flag=True,
      help='Provide as much debugging details as possible.')
  def export(context, format_, **kwargs):
      """
      Export test data into the desired format
  
      Regular expression can be used to filter tests by name.
      Use '.' to select tests under the current working directory.
      """
      tmt.Test._save_context(context)
      for test in context.obj.tree.tests():
          echo(test.export(format_=format_))
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Plan
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def plans(context, **kwargs):
      """
      Manage test plans (L2 metadata).
  
      \b
      Search for available plans.
      Explore detailed test step configuration.
      """
      tmt.Plan._save_context(context)
  
      # Show overview of available plans
      if context.invoked_subcommand is None:
          tmt.Plan.overview(context.obj.tree)
  
  main.add_command(plans)
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def ls(context, **kwargs):
      """
      List available plans
  
      Regular expression can be used to filter plans by name.
      Use '.' to select plans under the current working directory.
      """
      tmt.Plan._save_context(context)
      for plan in context.obj.tree.plans():
          plan.ls()
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def show(context, **kwargs):
      """
      Show plan details
  
      Regular expression can be used to filter plans by name.
      Use '.' to select plans under the current working directory.
      """
      tmt.Plan._save_context(context)
      for plan in context.obj.tree.plans():
          plan.show()
          echo()
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def lint(context, **kwargs):
      """
      Check plans against the L2 metadata specification
  
      Regular expression can be used to filter plans by name.
      Use '.' to select plans under the current working directory.
      """
      tmt.Plan._save_context(context)
      for plan in context.obj.tree.plans():
          plan.lint()
          echo()
  
  
  _plan_templates = listed(tmt.templates.PLAN, join='or')
  @plans.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      help='Plan template ({}).'.format(_plan_templates),
      prompt='Template ({})'.format(_plan_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new plan based on given template. """
      tmt.Plan._save_context(context)
      tmt.Plan.create(name, template, context.obj.tree, force)
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Story
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def stories(context, **kwargs):
      """
      Manage user stories.
  
      \b
      Check available user stories.
      Explore coverage (test, implementation, documentation).
      """
      tmt.Story._save_context(context)
  
      # Show overview of available stories
      if context.invoked_subcommand is None:
          tmt.Story.overview(context.obj.tree)
  
  main.add_command(stories)
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def ls(
      context, implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """
      List available stories
  
      Regular expression can be used to filter stories by name.
      Use '.' to select stories under the current working directory.
      """
      tmt.Story._save_context(context)
      for story in context.obj.tree.stories():
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              story.ls()
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def show(
      context, implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """
      Show story details
  
      Regular expression can be used to filter stories by name.
      Use '.' to select stories under the current working directory.
      """
      tmt.Story._save_context(context)
      for story in context.obj.tree.stories():
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              story.show()
              echo()
  
  
  _story_templates = listed(tmt.templates.STORY, join='or')
  @stories.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      prompt='Template ({})'.format(_story_templates),
      help='Story template ({}).'.format(_story_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new story based on given template. """
      tmt.Story._save_context(context)
      tmt.base.Story.create(name, template, context.obj.tree, force)
  
  
  @stories.command()
  @click.option(
      '--docs', is_flag=True, help='Show docs coverage.')
  @click.option(
      '--test', is_flag=True, help='Show test coverage.')
  @click.option(
      '--code', is_flag=True, help='Show code coverage.')
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def coverage(
      context, code, test, docs,
      implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """
      Show code, test and docs coverage for given stories
  
      Regular expression can be used to filter stories by name.
      Use '.' to select stories under the current working directory.
      """
      tmt.Story._save_context(context)
  
      def headfoot(text):
          """ Format simple header/footer """
          echo(style(text.rjust(4) + ' ', fg='blue'), nl=False)
  
      header = False
      total = code_coverage = test_coverage = docs_coverage = 0
      if not any([code, test, docs]):
          code = test = docs = True
      for story in context.obj.tree.stories():
          # Header
          if not header:
              if code:
                  headfoot('code')
              if test:
                  headfoot('test')
              if docs:
                  headfoot('docs')
              headfoot('story')
              echo()
              header = True
          # Coverage
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              status = story.coverage(code, test, docs)
              total += 1
              code_coverage += status[0]
              test_coverage += status[1]
              docs_coverage += status[2]
      # Summary
      if code:
          headfoot('{}%'.format(round(100 * code_coverage / total)))
      if test:
          headfoot('{}%'.format(round(100 * test_coverage / total)))
      if docs:
          headfoot('{}%'.format(round(100 * docs_coverage / total)))
      headfoot('from {}'.format(listed(total, 'story')))
      echo()
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @click.option(
      '--format', 'format_', default='rst', show_default=True, metavar='FORMAT',
      help='Output format.')
  @click.option(
      '-d', '--debug', is_flag=True,
      help='Provide as much debugging details as possible.')
  def export(
      context, format_,
      implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """
      Export selected stories into desired format
  
      Regular expression can be used to filter stories by name.
      Use '.' to select stories under the current working directory.
      """
      tmt.Story._save_context(context)
  
      for story in context.obj.tree.stories(whole=True):
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              echo(story.export(format_))
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Init
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  _init_template_choices = ['empty', 'mini', 'base', 'full']
  _init_templates = listed(_init_template_choices, join='or')
  @main.command()
  @click.pass_context
  @click.argument('path', default='.')
  @click.option(
      '-t', '--template', default='empty', metavar='TEMPLATE',
      type=click.Choice(_init_template_choices),
      help='Template ({}).'.format(_init_templates))
  @verbose_debug_quiet
  @force_dry
  def init(context, path, template, force, **kwargs):
      """
      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.
      """
  
      # Check for existing tree
      path = os.path.realpath(path)
      try:
          tree = tmt.Tree(path)
          # Are we creating a new tree under the existing one?
          if path == tree.root:
              echo("Tree '{}' already exists.".format(tree.root))
          else:
              tree = None
      except tmt.utils.GeneralError:
          tree = None
      # Create a new tree
      if tree is None:
          try:
              fmf.Tree.init(path)
              tree = tmt.Tree(path)
          except fmf.utils.GeneralError as error:
              raise tmt.utils.GeneralError(
                  "Failed to initialize tree in '{}': {}".format(
                      path, error))
          echo("Tree '{}' initialized.".format(tree.root))
  
      # Populate the tree with example objects if requested
      if template == 'empty':
          non_empty_choices = [c for c in _init_template_choices if c != 'empty']
          echo("To populate it with example content, use --template with "
               "{}.".format(listed(non_empty_choices, join='or')))
      else:
          echo("Applying template '{}'.".format(template, _init_templates))
      if template == 'mini':
          tmt.Plan.create('/plans/example', 'mini', tree, force)
      elif template == 'base':
          tmt.Test.create('/tests/example', 'beakerlib', tree, force)
          tmt.Plan.create('/plans/example', 'base', tree, force)
      elif template == 'full':
          tmt.Test.create('/tests/example', 'shell', tree, force)
          tmt.Plan.create('/plans/example', 'full', tree, force)
          tmt.Story.create('/stories/example', 'full', tree, force)
  
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.overview(tree)
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Run
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(chain=True, invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @click.option(
      '-i', '--id', 'id_', help='Run id (name or directory path).', metavar="ID")
  @click.option(
      '-a', '--all', 'all_', help='Run all steps, customize some.', is_flag=True)
  @verbose_debug_quiet
  @force_dry
  def run(context, all_, id_, **kwargs):
      """ Run test steps. """
      # Initialize
      run = tmt.Run(id_, context.obj.tree)
      run._context = context
      context.obj.run = run
  
  main.add_command(run)
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method to discover tests.')
  @verbose_debug_quiet
  @force_dry
  def discover(context, **kwargs):
      """ Gather and show information about test cases to be executed. """
      context.obj.steps.add('discover')
      tmt.steps.discover.Discover._context = context
      return 'discover'
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for provisioning.')
  @click.option(
      '-i', '--image', metavar='IMAGE',
      help='Select image to use. Possible values depend on the method.')
  @click.option(
      '-b', '--box', metavar='BOX',
      help='Vagrant box name to use.')
  @click.option(
      '--vagrantfile', metavar='VAGRANTFILE',
      help='Vagrantfile to override initialized one and default entries.')
  @click.option(
      '-m', '--memory', metavar='MEMORY',
      help='Set memory available to guest in MB.')
  @click.option(
      '-u', '--user', metavar='USER',
      help='Username to use for all guest operations.')
  @click.option(
      '-p', '--password', metavar='PASSWORD',
      help='Password to use for login into guest system.')
  @click.option(
      '-k', '--key', metavar='PRIVATE_KEY',
      help='Private key to use for login into guest system.')
  @click.option(
      '-g', '--guest', metavar='GUEST',
      help='Select remote host to connect to (how: connect).')
  @click.option(
      '--container-pull', is_flag=True,
      help='Force pulling container image (how: container).')
  
  @verbose_debug_quiet
  @force_dry
  def provision(context, **kwargs):
      """ Provision an environment for testing (or use localhost). """
      context.obj.steps.add('provision')
      tmt.steps.provision.Provision._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for environment preparation.')
  @click.option(
      '-s', '--script', metavar='SCRIPT',
      help='Scriplet or path or URI to a script to execute.')
  @click.option(
      '-p', '--playbook', metavar='PLAYBOOK',
      help='Path or URI to ansible playbook to run.')
  @verbose_debug_quiet
  @force_dry
  def prepare(context, **kwargs):
      """ Configure environment for testing (like ansible playbook). """
      context.obj.steps.add('prepare')
      tmt.steps.prepare.Prepare._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for test execution.')
  @click.option(
      '-s', '--script', metavar='SCRIPT', multiple=True,
      help='Shell script to be executed as a test.')
  @verbose_debug_quiet
  @force_dry
  def execute(context, **kwargs):
      """ Run the tests (using the specified framework and its settings). """
      context.obj.steps.add('execute')
      tmt.steps.execute.Execute._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for result reporting.')
  @verbose_debug_quiet
  @force_dry
  def report(context, **kwargs):
      """ Provide an overview of test results and send notifications. """
      context.obj.steps.add('report')
      tmt.steps.report.Report._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '-h', '--how', metavar='METHOD',
      help='Use specified method for finishing tasks.')
  @verbose_debug_quiet
  @force_dry
  def finish(context, **kwargs):
      """ Additional actions to be performed after the test execution. """
      context.obj.steps.add('finish')
      tmt.steps.finish.Finish._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '--name', 'names', metavar='REGEXP', multiple=True,
      help="Regular expression to match plan name.")
  @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.")
  @verbose_debug_quiet
  def plans(context, **kwargs):
      """ Select plans which should be executed. """
      tmt.base.Plan._context = context
  
  
  @run.command()
  @click.pass_context
  @click.option(
      '--name', 'names', metavar='REGEXP', multiple=True,
      help="Regular expression to match test name.")
  @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.")
  @verbose_debug_quiet
  def tests(context, **kwargs):
      """ Select tests which should be executed. """
      tmt.base.Test._context = context
  
  
  @run.resultcallback()
  @click.pass_context
  def finito(context, commands, *args, **kwargs):
      """ Run tests if run defined """
      if hasattr(context.obj, 'run'):
          context.obj.run.go()
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Test
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def tests(context, **kwargs):
      """
      Manage tests (L1 metadata).
  
      Check available tests, inspect their metadata.
      Convert old metadata into the new fmf format.
      """
  
      # Show overview of available tests
      if context.invoked_subcommand is None:
          tmt.Test.overview(context.obj.tree)
  
  main.add_command(tests)
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def ls(context, **kwargs):
      """ List available tests. """
      tmt.Test._context = context
      for test in context.obj.tree.tests():
          test.ls()
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def show(context, **kwargs):
      """ Show test details. """
      tmt.Test._context = context
      for test in context.obj.tree.tests():
          test.show()
          echo()
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def lint(context, **kwargs):
      """ Check tests against the L1 metadata specification. """
      tmt.Test._context = context
      for test in context.obj.tree.tests():
          test.lint()
          echo()
  
  
  _test_templates = listed(tmt.templates.TEST, join='or')
  @tests.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      help='Test template ({}).'.format(_test_templates),
      prompt='Template ({})'.format(_test_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new test based on given template. """
      tmt.Test._context = context
      tmt.Test.create(name, template, context.obj.tree, force)
  
  
  @tests.command(name='import')
  @click.pass_context
  @click.argument('paths', nargs=-1, metavar='[PATH]...')
  @click.option(
      '--nitrate / --no-nitrate', default=True,
      help='Import test metadata from Nitrate')
  @click.option(
      '--purpose / --no-purpose', default=True,
      help='Migrate description from PURPOSE file')
  @click.option(
      '--makefile / --no-makefile', default=True,
      help='Convert Beaker Makefile metadata')
  @verbose_debug_quiet
  @force_dry
  def import_(context, paths, makefile, nitrate, purpose, **kwargs):
      """
      Import old test metadata into the new fmf format.
  
      Accepts one or more directories where old metadata are stored.
      By default all available sources and current directory are used.
      The following test metadata are converted for each source:
  
      \b
      makefile ..... summary, component, duration, require
      purpose ...... description
      nitrate ...... contact, component, tag,
                     environment, relevancy, enabled
      """
      tmt.Test._context = context
      if not paths:
          paths = ['.']
      for path in paths:
          # Make sure we've got a real directory
          path = os.path.realpath(path)
          if not os.path.isdir(path):
              raise tmt.utils.GeneralError(
                  "Path '{0}' is not a directory.".format(path))
          # Gather old metadata and store them as fmf
          common, individual = tmt.convert.read(path, makefile, nitrate, purpose)
          # Add path to common metadata if there are virtual test cases
          if individual:
              root = fmf.Tree(path).root
              common['path'] = os.path.join( '/', os.path.relpath(path, root))
          # Store common metadata
          common_path = os.path.join(path, 'main.fmf')
          tmt.convert.write(common_path, common)
          # Store individual data (as virtual tests)
          for testcase in individual:
              testcase_path = os.path.join(
                  path, str(testcase['extra-nitrate']) + '.fmf')
              tmt.convert.write(testcase_path, testcase)
  
  
  @tests.command()
  @click.pass_context
  @name_filter_condition
  @click.option(
      '--format', 'format_', default='yaml', show_default=True, metavar='FORMAT',
      help='Output format.')
  @click.option(
      '-d', '--debug', is_flag=True,
      help='Provide as much debugging details as possible.')
  def export(context, format_, **kwargs):
      """ Export test data into the desired format. """
      tmt.Test._context = context
      for test in context.obj.tree.tests():
          echo(test.export(format_=format_))
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Plan
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def plans(context, **kwargs):
      """
      Manage test plans (L2 metadata).
  
      \b
      Search for available plans.
      Explore detailed test step configuration.
      """
      tmt.Plan._context = context
  
      # Show overview of available plans
      if context.invoked_subcommand is None:
          tmt.Plan.overview(context.obj.tree)
  
  main.add_command(plans)
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def ls(context, **kwargs):
      """ List available plans. """
      tmt.Plan._context = context
      for plan in context.obj.tree.plans():
          plan.ls()
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def show(context, **kwargs):
      """ Show plan details. """
      tmt.Plan._context = context
      for plan in context.obj.tree.plans():
          plan.show()
          echo()
  
  
  @plans.command()
  @click.pass_context
  @name_filter_condition
  @verbose_debug_quiet
  def lint(context, **kwargs):
      """ Check plans against the L2 metadata specification. """
      tmt.Plan._context = context
      for plan in context.obj.tree.plans():
          plan.lint()
          echo()
  
  
  _plan_templates = listed(tmt.templates.PLAN, join='or')
  @plans.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      help='Plan template ({}).'.format(_plan_templates),
      prompt='Template ({})'.format(_plan_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new plan based on given template. """
      tmt.Plan._context = context
      tmt.Plan.create(name, template, context.obj.tree, force)
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Story
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  @click.group(invoke_without_command=True, cls=CustomGroup)
  @click.pass_context
  @verbose_debug_quiet
  def stories(context, **kwargs):
      """
      Manage user stories.
  
      \b
      Check available user stories.
      Explore coverage (test, implementation, documentation).
      """
      tmt.Story._context = context
  
      # Show overview of available stories
      if context.invoked_subcommand is None:
          tmt.Story.overview(context.obj.tree)
  
  main.add_command(stories)
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def ls(
      context, implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """ List available stories. """
      tmt.Story._context = context
      for story in context.obj.tree.stories():
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              story.ls()
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def show(
      context, implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """ Show story details. """
      tmt.Story._context = context
      for story in context.obj.tree.stories():
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              story.show()
              echo()
  
  
  _story_templates = listed(tmt.templates.STORY, join='or')
  @stories.command()
  @click.pass_context
  @click.argument('name')
  @click.option(
      '-t', '--template', metavar='TEMPLATE',
      prompt='Template ({})'.format(_story_templates),
      help='Story template ({}).'.format(_story_templates))
  @verbose_debug_quiet
  @force_dry
  def create(context, name, template, force, **kwargs):
      """ Create a new story based on given template. """
      tmt.Story._context = context
      tmt.base.Story.create(name, template, context.obj.tree, force)
  
  
  @stories.command()
  @click.option(
      '--docs', is_flag=True, help='Show docs coverage.')
  @click.option(
      '--test', is_flag=True, help='Show test coverage.')
  @click.option(
      '--code', is_flag=True, help='Show code coverage.')
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @verbose_debug_quiet
  def coverage(
      context, code, test, docs,
      implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """ Show code, test and docs coverage for given stories. """
      tmt.Story._context = context
  
      def headfoot(text):
          """ Format simple header/footer """
          echo(style(text.rjust(4) + ' ', fg='blue'), nl=False)
  
      header = False
      total = code_coverage = test_coverage = docs_coverage = 0
      if not any([code, test, docs]):
          code = test = docs = True
      for story in context.obj.tree.stories():
          # Header
          if not header:
              if code:
                  headfoot('code')
              if test:
                  headfoot('test')
              if docs:
                  headfoot('docs')
              headfoot('story')
              echo()
              header = True
          # Coverage
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              status = story.coverage(code, test, docs)
              total += 1
              code_coverage += status[0]
              test_coverage += status[1]
              docs_coverage += status[2]
      # Summary
      if code:
          headfoot('{}%'.format(round(100 * code_coverage / total)))
      if test:
          headfoot('{}%'.format(round(100 * test_coverage / total)))
      if docs:
          headfoot('{}%'.format(round(100 * docs_coverage / total)))
      headfoot('from {}'.format(listed(total, 'story')))
      echo()
  
  
  @stories.command()
  @click.pass_context
  @name_filter_condition
  @implemented_tested_documented
  @click.option(
      '--format', 'format_', default='rst', show_default=True, metavar='FORMAT',
      help='Output format.')
  @click.option(
      '-d', '--debug', is_flag=True,
      help='Provide as much debugging details as possible.')
  def export(
      context, format_,
      implemented, tested, documented, covered,
      unimplemented, untested, undocumented, uncovered, **kwargs):
      """ Export selected stories into desired format. """
      tmt.Story._context = context
  
      for story in context.obj.tree.stories(whole=True):
          if story._match(implemented, tested, documented, covered,
                  unimplemented, untested, undocumented, uncovered):
              echo(story.export(format_))
  
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Init
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  _init_template_choices = ['empty', 'mini', 'base', 'full']
  _init_templates = listed(_init_template_choices, join='or')
  @main.command()
  @click.pass_context
  @click.argument('path', default='.')
  @click.option(
      '-t', '--template', default='empty', metavar='TEMPLATE',
      type=click.Choice(_init_template_choices),
      help='Template ({}).'.format(_init_templates))
  @verbose_debug_quiet
  @force_dry
  def init(context, path, template, force, **kwargs):
      """
      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.
      """
  
      # Check for existing tree
      path = os.path.realpath(path)
      try:
          tree = tmt.Tree(path)
          # Are we creating a new tree under the existing one?
          if path == tree.root:
              echo("Tree '{}' already exists.".format(tree.root))
          else:
              tree = None
      except tmt.utils.GeneralError:
          tree = None
      # Create a new tree
      if tree is None:
          try:
              fmf.Tree.init(path)
              tree = tmt.Tree(path)
          except fmf.utils.GeneralError as error:
              raise tmt.utils.GeneralError(
                  "Failed to initialize tree in '{}': {}".format(
                      path, error))
          echo("Tree '{}' initialized.".format(tree.root))
  
      # Populate the tree with example objects if requested
      if template == 'empty':
          non_empty_choices = [c for c in _init_template_choices if c != 'empty']
          echo("To populate it with example content, use --template with "
               "{}.".format(listed(non_empty_choices, join='or')))
      else:
          echo("Applying template '{}'.".format(template, _init_templates))
      if template == 'mini':
          tmt.Plan.create('/plans/example', 'mini', tree, force)
      elif template == 'base':
          tmt.Test.create('/tests/example', 'beakerlib', tree, force)
          tmt.Plan.create('/plans/example', 'base', tree, force)
      elif template == 'full':
          tmt.Test.create('/tests/example', 'shell', tree, force)
          tmt.Plan.create('/plans/example', 'full', tree, force)
          tmt.Story.create('/stories/example', 'full', tree, force)
  
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():
                      line = process.stdout.readline().decode('utf-8')
                      stdout += line
                      if line != '':
                          self.debug('out', line.rstrip('\n'), 'yellow')
                  # Handle stderr
                  if descriptor == process.stderr.fileno():
                      line = process.stderr.readline().decode('utf-8')
                      stderr += line
                      if line != '':
                          self.debug('err', line.rstrip('\n'), 'yellow')
  
          # Check for possible additional output
          for line in process.stdout.readlines():
              line = line.decode('utf-8')
              stdout += line
              self.debug('out', line.rstrip('\n'), 'yellow')
          for line in process.stderr.readlines():
              line = line.decode('utf-8')
              stderr += line
              self.debug('err', line.rstrip('\n'), 'yellow')
  
          # Handle the exit code, return output
          if process.returncode != 0:
              if isinstance(command, (list, tuple)):
                  command = ' '.join(command)
              raise subprocess.CalledProcessError(process.returncode, command)
          return stdout, stderr
  
      def run(self, command, message=None, cwd=None, dry=False, shell=True):
          """
          Run command, give message, handle errors
  
          Command is run in the workdir be default.
          In dry mode commands are not executed unless dry=True.
          Returns (stdout, stderr) tuple.
          """
          # Use a generic message if none given, prepare error message
          if not message:
              if isinstance(command, (list, tuple)):
                  line = ' '.join(command)
              else:
                  line = command
              message = f"Run command '{line}'."
          self.debug(message)
          message = "Failed to " + message[0].lower() + message[1:]
  
          # Nothing more to do in dry mode (unless requested)
          if self.opt('dry') and not dry:
              return None, None
  
          # Prepare command, run it, handle the exit code
          try:
              return self._run(command, cwd=cwd or self.workdir, shell=shell)
          except (OSError, subprocess.CalledProcessError) as error:
              raise GeneralError(f"{message}\n{error}")
  
      def read(self, path):
          """ Read a file from the workdir """
          path = os.path.join(self.workdir, path)
          self.debug(f"Read file '{path}'.")
          try:
              with open(path) as data:
                  return data.read()
          except OSError as error:
              raise GeneralError(f"Failed to read '{path}'.\n{error}")
  
      def write(self, path, data):
          """ Write a file to the workdir """
          path = os.path.join(self.workdir, path)
          self.debug(f"Write file '{path}'.")
          # Dry mode
          if self.opt('dry'):
              return
          try:
              with open(path, 'w') as target:
                  return target.write(data)
          except OSError as error:
              raise GeneralError(f"Failed to write '{path}'.\n{error}")
  
      def status(self, status=None):
          """ Get and set current status, store in workdir """
          # Check for valid values
          if status and status not in ['todo', 'done', 'going']:
              raise GeneralError(f"Invalid status '{status}'.")
          # Store status
          if status:
              self.write('status.txt', status + '\n')
          # Read status
          else:
              try:
                  return self.read('status.txt').strip()
              except GeneralError:
                  return None
  
      def _workdir_init(self, id_):
          """
          Initialize the work directory
  
          Workdir under WORKDIR_ROOT is used/created if 'id' is provided.
          If 'id' is a path, that directory is used instead. Otherwise a
          new workdir is created under WORKDIR_ROOT.
          """
          # Construct the workdir
          if id_ is not None:
              # Use provided directory if path given
              if '/' in id_:
                  workdir = id_
              # Construct directory name under workdir root
              else:
                  if isinstance(id_, int):
                      id_ = str(id_).rjust(3, '0')
                  directory = 'run-{}'.format(id_)
                  workdir = os.path.join(WORKDIR_ROOT, directory)
          else:
              # Generate a unique run id
              for id_ in range(1, WORKDIR_MAX + 1):
                  directory = 'run-{}'.format(str(id_).rjust(3, '0'))
                  workdir = os.path.join(WORKDIR_ROOT, directory)
                  if not os.path.exists(workdir):
                      break
              if id_ == WORKDIR_MAX:
                  raise GeneralError(
                      "Cleanup the '{}' directory.".format(WORKDIR_ROOT))
  
          # Create the workdir
          create_directory(workdir, 'workdir', quiet=True)
          self._workdir = workdir
  
      @property
      def workdir(self):
          """ Get the workdir, create if does not exist """
          if self._workdir is None:
              # Need the parent workdir
              if self.parent is None:
                  raise GeneralError('Parent workdir not available')
              # Append name and create
              self._workdir = os.path.join(
                  self.parent.workdir, self.name.lstrip('/'))
              create_directory(self._workdir, 'workdir', quiet=True)
          return self._workdir
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Exceptions
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class GeneralError(Exception):
      """ General error """
  
  class SpecificationError(GeneralError):
      """ Metadata specification error """
  
  class ConvertError(GeneralError):
      """ Metadata conversion error """
  
  class StructuredFieldError(GeneralError):
      """ StructuredField parsing error """
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Utilities
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  def quote(string):
      """ Surround a string with double quotes """
      return f'"{string}"'
  
  def ascii(text):
      """ Transliterate special unicode characters into pure ascii """
      try:
          if not isinstance(text, unicode):
              text = unicode(text)
      except NameError:
          if not isinstance(text, str):
              text = str(text)
      return unicodedata.normalize('NFKD', text).encode('ascii','ignore')
  
  
  def variables_to_dictionary(variables):
      """
      Convert shell-like variables into a dictionary
  
      Accepts single string or list of strings. Allowed forms are:
      'X=1'
      'X=1 Y=2 Z=3'
      ['X=1', 'Y=2', 'Z=3']
      ['X=1 Y=2 Z=3', 'A=1 B=2 C=3']
      'TXT="Some text with spaces in it"'
      """
      if not isinstance(variables, list):
          variables = [variables]
      result = dict()
      for variable in variables:
          if variable is None:
              continue
          for var in shlex.split(variable):
              matched = re.match("([^=]+)=(.*)", var)
              if not matched:
                  raise GeneralError("Invalid parameter {0}".format(var))
              name, value = matched.groups()
              result[name] = value
      return result
  
  
  def dictionary_to_yaml(data):
      """ Convert dictionary into yaml """
      output = io.StringIO()
      yaml.safe_dump(
          data, output,
          encoding='utf-8', allow_unicode=True,
          indent=4, default_flow_style=False)
      return output.getvalue()
  
  
  def dict_to_shell(data):
      """ Convert dictionary to list of key=value pairs """
      return [f"{key}={shlex.quote(str(value))}" for key, value in data.items()]
  
  
  def verdict(decision, comment=None, good='pass', bad='fail', problem='warn'):
      """
      Return verdict in green or red based on the decision
  
      0 or False ... good (green)
      1 or True .... bad (red)
      otherwise .... problem (yellow)
      """
  
      if decision == 0:
          text = style(bad, fg='red')
      elif decision == 1:
          text = style(good, fg='green')
      else:
          text = style(problem, fg='yellow')
      if comment:
          return text + ' ' + comment
      else:
          return text
  
  
  def format(
          key, value=None,
          indent=12, width=72, wrap='auto',
          key_color='green', value_color='black'):
      """
      Nicely format and indent a key-value pair
  
      The following values for 'wrap' are supported:
  
          True .... always reformat text and wrap long lines
          False ... preserve text, no new line changes
          auto .... wrap only if text contains a long line
      """
      indent_string = (indent + 1) * ' '
      # Key
      output = '{} '.format(str(key).rjust(indent, ' '))
      if key_color is not None:
          output = style(output, fg=key_color)
      # Bool
      if isinstance(value, bool):
          output += ('yes' if value else 'no')
      # List
      elif isinstance(value, list):
          listed_text = fmf.utils.listed(value)
          has_spaces = any([item.find(' ') > -1 for item in value])
          # Use listed output only for short lists without spaces
          if len(listed_text) < width - indent and not has_spaces:
              output += listed_text
          # Otherwise just place each item on a new line
          else:
              output += ('\n' + indent_string).join(value)
      # Text
      elif isinstance(value, str):
          # In 'auto' mode enable wrapping when long lines present
          if wrap == 'auto':
              wrap = any(
                  [len(line) + indent - 7 > width
                  for line in value.split('\n')])
          if wrap:
              output += (wrap_text(
                  value, width=width,
                  preserve_paragraphs=True,
                  initial_indent=indent_string,
                  subsequent_indent=indent_string).lstrip())
          else:
              output += (('\n' + indent_string).join(
                  value.rstrip().split('\n')))
      else:
          output += str(value)
      return output
  
  
  def create_directory(path, name, quiet=False):
      """ Create a new directory, handle errors """
      say = log.debug if quiet else echo
      if os.path.isdir(path):
          say("Directory '{}' already exists.".format(path))
          return
      try:
          os.makedirs(path, exist_ok=True)
          say("Directory '{}' created.".format(path))
      except OSError as error:
          raise GeneralError("Failed to create {} '{}' ({})".format(
              name, path, error))
  
  
  def create_file(path, content, name, force=False, mode=0o664, quiet=False):
      """ Create a new file, handle errors """
      say = log.debug if quiet else echo
      action = 'created'
      if os.path.exists(path):
          if force:
              action = 'overwritten'
          else:
              raise GeneralError("File '{}' already exists.".format(path))
      try:
          with open(path, 'w') as file_:
              file_.write(content)
          say("{} '{}' {}.".format(name.capitalize(), path, action))
          os.chmod(path, mode)
      except OSError as error:
          raise GeneralError("Failed to create {} '{}' ({})".format(
              name, path, error))
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  StructuredField
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class StructuredField(object):
      """
      Handling multiple text data in a single text field
  
      The StructuredField allows you to easily store and extract several
      sections of text data to/from a single text field. The sections are
      separated by section names in square brackets and can be hosted in
      other text as well.
  
      The section names have to be provided on a separate line and there
      must be no leading/trailing white space before/after the brackets.
      The StructuredField supports two versions of the format:
  
      Version 0: Simple, concise, useful when neither the surrounding text
      or the section data can contain lines which could resemble section
      names. Here's an example of a simple StructuredField::
  
          Note written by human.
  
          [section-one]
          Section one content.
  
          [section-two]
          Section two content.
  
          [section-three]
          Section three content.
  
          [end]
  
          Another note written by human.
  
      Version 1: Includes unique header to prevent collisions with the
      surrounding text and escapes any section-like lines in the content::
  
          Note written by human.
  
          [structured-field-start]
          This is StructuredField version 1. Please, edit with care.
  
          [section-one]
          Section one content.
  
          [section-two]
          Section two content.
          [structured-field-escape][something-resembling-section-name]
  
          [section-three]
          Section three content.
  
          [structured-field-end]
  
          Another note written by human.
  
      Note that an additional empty line is added at the end of each
      section to improve the readability. This line is not considered
      to be part of the section content.
  
      Besides handling the whole section content it's also possible to
      store several key-value pairs in a single section, similarly as in
      the ini config format::
  
          [section]
          key1 = value1
          key2 = value2
          key3 = value3
  
      Provide the key name as the optional argument 'item' when accessing
      these single-line items. Note that the section cannot contain both
      plain text data and key-value pairs.
  
      Example::
  
          field = qe.StructuredField()
          field.set("project", "Project Name")
          field.set("details", "somebody", "owner")
          field.set("details", "2013-05-27", "started")
          field.set("description", "This is a description.\\n"
                  "It spans across multiple lines.\\n")
          print field.save()
  
              [structured-field-start]
              This is StructuredField version 1. Please, edit with care.
  
              [project]
              Project Name
  
              [details]
              owner = somebody
              started = 2013-05-27
  
              [description]
              This is a description.
              It spans across multiple lines.
  
              [structured-field-end]
  
          field.version(0)
          print field.save()
  
              [project]
              Project Name
  
              [details]
              owner = somebody
              started = 2013-05-27
  
              [description]
              This is a description.
              It spans across multiple lines.
  
              [end]
  
      Multiple values for the same key are supported as well. Enable this
      feature with 'multi=True' when initializing the structured field.
      If multiple values are present their list will be returned instead
      of a single string. Similarly use list for setting multiple values::
  
          field = qe.StructuredField(multi=True)
          requirements = ['hypervisor=', 'labcontroller=lab.example.com']
          field.set("hardware", requirements, "hostrequire")
          print field.save()
  
              [structured-field-start]
              This is StructuredField version 1. Please, edit with care.
  
              [hardware]
              hostrequire = hypervisor=
              hostrequire = labcontroller=lab.example.com
  
              [structured-field-end]
  
          print field.get("hardware", "hostrequire")
  
              ['hypervisor=', 'labcontroller=lab.example.com']
      """
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Special
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def __init__(self, text=None, version=1, multi=False):
          """ Initialize the structured field """
          self.version(version)
          self._header = ""
          self._footer = ""
          self._sections = {}
          self._order = []
          self._multi = multi
          if text is not None:
              self.load(text)
  
      def __iter__(self):
          """ By default iterate through all available sections """
          for section in self._order:
              yield section
  
      def __nonzero__(self):
          """ True when any section is defined """
          return len(self._order) > 0
  
      __bool__ = __nonzero__
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Private
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def _load_version_zero(self, text):
          """ Load version 0 format """
          # Attempt to split the text according to the section tag
          section = re.compile(r"\n?^\[([^\]]+)\]\n", re.MULTILINE)
          parts = section.split(text)
          # If just one part ---> no sections present, just plain text
          if len(parts) == 1:
              self._header = parts[0]
              return
          # Pick header & footer, make sure [end] tag is present
          self._header = parts[0]
          self._footer = re.sub("^\n", "", parts[-1])
          if parts[-2] != "end":
              raise StructuredFieldError("No [end] section tag found")
          # Convert to dictionary and save the order
          keys = parts[1:-2:2]
          values = parts[2:-2:2]
          for key, value in zip(keys, values):
              self.set(key, value)
  
      def _load(self, text):
          """ Load version 1+ format """
          # The text must exactly match the format
          format = re.compile(
                  r"(.*)^\[structured-field-start\][ \t]*\n"
                  r"(.*)\n\[structured-field-end\][ \t]*\n(.*)",
                  re.DOTALL + re.MULTILINE)
          # No match ---> plain text or broken structured field
          matched = format.search(text)
          if not matched:
              if "[structured-field" in text:
                  raise StructuredFieldError("StructuredField parse error")
              self._header = text
              log.debug("StructuredField not found, treating as a plain text")
              return
          # Save header & footer (remove trailing new lines)
          self._header = re.sub("\n\n$", "\n", matched.groups()[0])
          if self._header:
              log.debug(u"Parsed header:\n{0}".format(self._header))
          self._footer = re.sub("^\n", "", matched.groups()[2])
          if self._footer:
              log.debug(u"Parsed footer:\n{0}".format(self._footer))
          # Split the content on the section names
          section = re.compile(r"\n\[([^\]]+)\][ \t]*\n", re.MULTILINE)
          parts = section.split(matched.groups()[1])
          # Detect the version
          try:
              self.version(int(re.search(
                  r"version (\d+)", parts[0]).groups()[0]))
              log.debug("Detected StructuredField version {0}".format(
                      self.version()))
          except AttributeError:
              log.error(parts[0])
              raise StructuredFieldError(
                      "Unable to detect StructuredField version")
          # Convert to dictionary, remove escapes and save the order
          keys = parts[1::2]
          escape = re.compile(r"^\[structured-field-escape\]", re.MULTILINE)
          values = [escape.sub("", value) for value in parts[2::2]]
          for key, value in zip(keys, values):
              self.set(key, value)
          log.debug(u"Parsed sections:\n{0}".format(
              pprint.pformat(self._sections)))
  
      def _save_version_zero(self):
          """ Save version 0 format """
          result = []
          if self._header:
              result.append(self._header)
          for section, content in self.iterate():
              result.append(u"[{0}]\n{1}".format(section, content))
          if self:
              result.append(u"[end]\n")
          if self._footer:
              result.append(self._footer)
          return "\n".join(result)
  
      def _save(self):
          """ Save version 1+ format """
          result = []
          # Regular expression for escaping section-like lines
          escape = re.compile(r"^(\[.+\])$", re.MULTILINE)
          # Header
          if self._header:
              result.append(self._header)
          # Sections
          if self:
              result.append(
                      u"[structured-field-start]\n"
                      u"This is StructuredField version {0}. "
                      u"Please, edit with care.\n".format(self._version))
              for section, content in self.iterate():
                  result.append(u"[{0}]\n{1}".format(section,
                          escape.sub("[structured-field-escape]\\1", content)))
              result.append(u"[structured-field-end]\n")
          # Footer
          if self._footer:
              result.append(self._footer)
          return "\n".join(result)
  
      def _read_section(self, content):
          """ Parse config section and return ordered dictionary """
          dictionary = OrderedDict()
          for line in content.split("\n"):
              # Remove comments and skip empty lines
              line = re.sub("#.*", "", line)
              if re.match(r"^\s*$", line):
                  continue
              # Parse key and value
              matched = re.search("([^=]+)=(.*)", line)
              if not matched:
                  raise StructuredFieldError(
                      "Invalid key/value line: {0}".format(line))
              key = matched.groups()[0].strip()
              value = matched.groups()[1].strip()
              # Handle multiple values if enabled
              if key in dictionary and self._multi:
                  if isinstance(dictionary[key], list):
                      dictionary[key].append(value)
                  else:
                      dictionary[key] = [dictionary[key], value]
              else:
                  dictionary[key] = value
          return dictionary
  
      def _write_section(self, dictionary):
          """ Convert dictionary into a config section format """
          section = ""
          for key in dictionary:
              if isinstance(dictionary[key], list):
                  for value in dictionary[key]:
                      section += "{0} = {1}\n".format(key, value)
              else:
                  section += "{0} = {1}\n".format(key, dictionary[key])
          return section
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Methods
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def iterate(self):
          """ Return (section, content) tuples for all sections """
          for section in self:
              yield section, self._sections[section]
  
      def version(self, version=None):
          """ Get or set the StructuredField version """
          if version is not None:
              if version in [0, 1]:
                  self._version = version
              else:
                  raise StructuredFieldError(
                          "Bad StructuredField version: {0}".format(version))
          return self._version
  
      def load(self, text, version=None):
          """ Load the StructuredField from a string """
          if version is not None:
              self.version(version)
          # Make sure we got a text, convert to unicode if necessary
          try:
              if not isinstance(text, basestring):
                  raise StructuredFieldError(
                          "Invalid StructuredField, expecting string or unicode")
              if not isinstance(text, unicode):
                  text = text.decode("utf8")
          except NameError:
              if not isinstance(text, str):
                  raise StructuredFieldError(
                          "Invalid StructuredField, expecting string")
          # Remove possible carriage returns
          text = re.sub("\r\n", "\n", text)
          # Make sure the text has a new line at the end
          if text and text[-1] != "\n":
              text += "\n"
          log.debug(u"Parsing StructuredField\n{0}".format(text))
          # Parse respective format version
          if self._version == 0:
              self._load_version_zero(text)
          else:
              self._load(text)
  
      def save(self):
          """ Convert the StructuredField into a string """
          if self.version() == 0:
              return self._save_version_zero()
          else:
              return self._save()
  
      def header(self, content=None):
          """ Get or set the header content """
          if content is not None:
              self._header = content
          return self._header
  
      def footer(self, content=None):
          """ Get or set the footer content """
          if content is not None:
              self._footer = content
          return self._footer
  
      def sections(self):
          """ Get the list of available sections """
          return self._order
  
      def get(self, section, item=None):
          """ Return content of given section or section item """
          try:
              content = self._sections[section]
          except KeyError:
              raise StructuredFieldError(
                      "Section [{0}] not found".format(ascii(section)))
          # Return the whole section content
          if item is None:
              return content
          # Return only selected item from the section
          try:
              return self._read_section(content)[item]
          except KeyError:
              raise StructuredFieldError(
                      "Unable to read '{0}' from section '{1}'".format(
                      ascii(item), ascii(section)))
  
      def set(self, section, content, item=None):
          """ Update content of given section or section item """
          # Convert to string if necessary, keep lists untouched
          if not isinstance(content, list):
              try:
                  if not isinstance(content, basestring):
                      content = unicode(content)
                  elif not isinstance(content, unicode):
                      content = content.decode("utf8")
              except:
                  if not isinstance(content, str):
                      content = str(content)
          # Set the whole section content
          if item is None:
              # Add new line if missing
              if content and content[-1] != "\n":
                  content += "\n"
              self._sections[section] = content
          # Set only selected item from the section
          else:
              try:
                  current = self._sections[section]
              except KeyError:
                  current = ""
              dictionary = self._read_section(current)
              dictionary[item] = content
              self._sections[section] = self._write_section(dictionary)
          # Remember the order when adding a new section
          if section not in self._order:
              self._order.append(section)
  
      def remove(self, section, item=None):
          """ Remove given section or section item """
          # Remove the whole section
          if item is None:
              try:
                  del self._sections[section]
                  del self._order[self._order.index(section)]
              except KeyError:
                  raise StructuredFieldError(
                          "Section [{0}] not found".format(ascii(section)))
          # Remove only selected item from the section
          else:
              try:
                  dictionary = self._read_section(self._sections[section])
                  del(dictionary[item])
              except KeyError:
                  raise StructuredFieldError(
                          "Unable to remove '{0}' from section '{1}'".format(
                          ascii(item), ascii(section)))
              self._sections[section] = self._write_section(dictionary)
  
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.rstrip('\n'), 'yellow')
                  # Handle stderr
                  if descriptor == process.stderr.fileno():
                      line = process.stderr.readline().decode('utf-8')
                      stderr += line
                      if line != '':
                          self.debug('err', line.rstrip('\n'), 'yellow')
  
          # Check for possible additional output
          for line in process.stdout.readlines():
              line = line.decode('utf-8')
              stdout += line
              self.debug('out', line.rstrip('\n'), 'yellow')
          for line in process.stderr.readlines():
              line = line.decode('utf-8')
              stderr += line
              self.debug('err', line.rstrip('\n'), 'yellow')
  
          # Handle the exit code, return output
          if process.returncode != 0:
              if isinstance(command, (list, tuple)):
                  command = ' '.join(command)
              raise subprocess.CalledProcessError(process.returncode, command)
          return stdout, stderr
  
      def run(self, command, message=None, cwd=None, dry=False, shell=True):
          """
          Run command, give message, handle errors
  
          Command is run in the workdir be default.
          In dry mode commands are not executed unless dry=True.
          Returns (stdout, stderr) tuple.
          """
          # Use a generic message if none given, prepare error message
          if not message:
              if isinstance(command, (list, tuple)):
                  line = ' '.join(command)
              else:
                  line = command
              message = f"Run command '{line}'."
          self.debug(message)
          message = "Failed to " + message[0].lower() + message[1:]
  
          # Nothing more to do in dry mode (unless requested)
          if self.opt('dry') and not dry:
              return None, None
  
          # Prepare command, run it, handle the exit code
          try:
              return self._run(command, cwd=cwd or self.workdir, shell=shell)
          except (OSError, subprocess.CalledProcessError) as error:
              raise GeneralError(f"{message}\n{error}")
  
      def read(self, path):
          """ Read a file from the workdir """
          path = os.path.join(self.workdir, path)
          self.debug(f"Read file '{path}'.")
          try:
              with open(path) as data:
                  return data.read()
          except OSError as error:
              raise GeneralError(f"Failed to read '{path}'.\n{error}")
  
      def write(self, path, data):
          """ Write a file to the workdir """
          path = os.path.join(self.workdir, path)
          self.debug(f"Write file '{path}'.")
          # Dry mode
          if self.opt('dry'):
              return
          try:
              with open(path, 'w') as target:
                  return target.write(data)
          except OSError as error:
              raise GeneralError(f"Failed to write '{path}'.\n{error}")
  
      def status(self, status=None):
          """ Get and set current status, store in workdir """
          # Check for valid values
          if status and status not in ['todo', 'done', 'going']:
              raise GeneralError(f"Invalid status '{status}'.")
          # Store status
          if status:
              self.write('status.txt', status + '\n')
          # Read status
          else:
              try:
                  return self.read('status.txt').strip()
              except GeneralError:
                  return None
  
      def _workdir_init(self, id_):
          """
          Initialize the work directory
  
          Workdir under WORKDIR_ROOT is used/created if 'id' is provided.
          If 'id' is a path, that directory is used instead. Otherwise a
          new workdir is created under WORKDIR_ROOT.
          """
          # Construct the workdir
          if id_ is not None:
              # Use provided directory if path given
              if '/' in id_:
                  workdir = id_
              # Construct directory name under workdir root
              else:
                  if isinstance(id_, int):
                      id_ = str(id_).rjust(3, '0')
                  directory = 'run-{}'.format(id_)
                  workdir = os.path.join(WORKDIR_ROOT, directory)
          else:
              # Generate a unique run id
              for id_ in range(1, WORKDIR_MAX + 1):
                  directory = 'run-{}'.format(str(id_).rjust(3, '0'))
                  workdir = os.path.join(WORKDIR_ROOT, directory)
                  if not os.path.exists(workdir):
                      break
              if id_ == WORKDIR_MAX:
                  raise GeneralError(
                      "Cleanup the '{}' directory.".format(WORKDIR_ROOT))
  
          # Create the workdir
          create_directory(workdir, 'workdir', quiet=True)
          self._workdir = workdir
  
      @property
      def workdir(self):
          """ Get the workdir, create if does not exist """
          if self._workdir is None:
              # Need the parent workdir
              if self.parent is None:
                  raise GeneralError('Parent workdir not available')
              # Append name and create
              self._workdir = os.path.join(
                  self.parent.workdir, self.name.lstrip('/'))
              create_directory(self._workdir, 'workdir', quiet=True)
          return self._workdir
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Exceptions
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class GeneralError(Exception):
      """ General error """
  
  class SpecificationError(GeneralError):
      """ Metadata specification error """
  
  class ConvertError(GeneralError):
      """ Metadata conversion error """
  
  class StructuredFieldError(GeneralError):
      """ StructuredField parsing error """
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  Utilities
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  def quote(string):
      """ Surround a string with double quotes """
      return f'"{string}"'
  
  def ascii(text):
      """ Transliterate special unicode characters into pure ascii """
      try:
          if not isinstance(text, unicode):
              text = unicode(text)
      except NameError:
          if not isinstance(text, str):
              text = str(text)
      return unicodedata.normalize('NFKD', text).encode('ascii','ignore')
  
  
  def variables_to_dictionary(variables):
      """
      Convert shell-like variables into a dictionary
  
      Accepts single string or list of strings. Allowed forms are:
      'X=1'
      'X=1 Y=2 Z=3'
      ['X=1', 'Y=2', 'Z=3']
      ['X=1 Y=2 Z=3', 'A=1 B=2 C=3']
      'TXT="Some text with spaces in it"'
      """
      if not isinstance(variables, list):
          variables = [variables]
      result = dict()
      for variable in variables:
          if variable is None:
              continue
          for var in shlex.split(variable):
              matched = re.match("([^=]+)=(.*)", var)
              if not matched:
                  raise GeneralError("Invalid parameter {0}".format(var))
              name, value = matched.groups()
              result[name] = value
      return result
  
  
  def dictionary_to_yaml(data):
      """ Convert dictionary into yaml """
      output = io.StringIO()
      yaml.safe_dump(
          data, output,
          encoding='utf-8', allow_unicode=True,
          indent=4, default_flow_style=False)
      return output.getvalue()
  
  
  def dict_to_shell(data):
      """ Convert dictionary to list of key=value pairs """
      return [f"{key}={shlex.quote(str(value))}" for key, value in data.items()]
  
  
  def verdict(decision, comment=None, good='pass', bad='fail', problem='warn'):
      """
      Return verdict in green or red based on the decision
  
      0 or False ... good (green)
      1 or True .... bad (red)
      otherwise .... problem (yellow)
      """
  
      if decision == 0:
          text = style(bad, fg='red')
      elif decision == 1:
          text = style(good, fg='green')
      else:
          text = style(problem, fg='yellow')
      if comment:
          return text + ' ' + comment
      else:
          return text
  
  
  def format(
          key, value=None,
          indent=12, width=72, wrap='auto',
          key_color='green', value_color='black'):
      """
      Nicely format and indent a key-value pair
  
      The following values for 'wrap' are supported:
  
          True .... always reformat text and wrap long lines
          False ... preserve text, no new line changes
          auto .... wrap only if text contains a long line
      """
      indent_string = (indent + 1) * ' '
      # Key
      output = '{} '.format(str(key).rjust(indent, ' '))
      if key_color is not None:
          output = style(output, fg=key_color)
      # Bool
      if isinstance(value, bool):
          output += ('yes' if value else 'no')
      # List
      elif isinstance(value, list):
          listed_text = fmf.utils.listed(value)
          has_spaces = any([item.find(' ') > -1 for item in value])
          # Use listed output only for short lists without spaces
          if len(listed_text) < width - indent and not has_spaces:
              output += listed_text
          # Otherwise just place each item on a new line
          else:
              output += ('\n' + indent_string).join(value)
      # Text
      elif isinstance(value, str):
          # In 'auto' mode enable wrapping when long lines present
          if wrap == 'auto':
              wrap = any(
                  [len(line) + indent - 7 > width
                  for line in value.split('\n')])
          if wrap:
              output += (wrap_text(
                  value, width=width,
                  preserve_paragraphs=True,
                  initial_indent=indent_string,
                  subsequent_indent=indent_string).lstrip())
          else:
              output += (('\n' + indent_string).join(
                  value.rstrip().split('\n')))
      else:
          output += str(value)
      return output
  
  
  def create_directory(path, name, quiet=False):
      """ Create a new directory, handle errors """
      say = log.debug if quiet else echo
      if os.path.isdir(path):
          say("Directory '{}' already exists.".format(path))
          return
      try:
          os.makedirs(path, exist_ok=True)
          say("Directory '{}' created.".format(path))
      except OSError as error:
          raise GeneralError("Failed to create {} '{}' ({})".format(
              name, path, error))
  
  
  def create_file(path, content, name, force=False, mode=0o664, quiet=False):
      """ Create a new file, handle errors """
      say = log.debug if quiet else echo
      action = 'created'
      if os.path.exists(path):
          if force:
              action = 'overwritten'
          else:
              raise GeneralError("File '{}' already exists.".format(path))
      try:
          with open(path, 'w') as file_:
              file_.write(content)
          say("{} '{}' {}.".format(name.capitalize(), path, action))
          os.chmod(path, mode)
      except OSError as error:
          raise GeneralError("Failed to create {} '{}' ({})".format(
              name, path, error))
  
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  #  StructuredField
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  class StructuredField(object):
      """
      Handling multiple text data in a single text field
  
      The StructuredField allows you to easily store and extract several
      sections of text data to/from a single text field. The sections are
      separated by section names in square brackets and can be hosted in
      other text as well.
  
      The section names have to be provided on a separate line and there
      must be no leading/trailing white space before/after the brackets.
      The StructuredField supports two versions of the format:
  
      Version 0: Simple, concise, useful when neither the surrounding text
      or the section data can contain lines which could resemble section
      names. Here's an example of a simple StructuredField::
  
          Note written by human.
  
          [section-one]
          Section one content.
  
          [section-two]
          Section two content.
  
          [section-three]
          Section three content.
  
          [end]
  
          Another note written by human.
  
      Version 1: Includes unique header to prevent collisions with the
      surrounding text and escapes any section-like lines in the content::
  
          Note written by human.
  
          [structured-field-start]
          This is StructuredField version 1. Please, edit with care.
  
          [section-one]
          Section one content.
  
          [section-two]
          Section two content.
          [structured-field-escape][something-resembling-section-name]
  
          [section-three]
          Section three content.
  
          [structured-field-end]
  
          Another note written by human.
  
      Note that an additional empty line is added at the end of each
      section to improve the readability. This line is not considered
      to be part of the section content.
  
      Besides handling the whole section content it's also possible to
      store several key-value pairs in a single section, similarly as in
      the ini config format::
  
          [section]
          key1 = value1
          key2 = value2
          key3 = value3
  
      Provide the key name as the optional argument 'item' when accessing
      these single-line items. Note that the section cannot contain both
      plain text data and key-value pairs.
  
      Example::
  
          field = qe.StructuredField()
          field.set("project", "Project Name")
          field.set("details", "somebody", "owner")
          field.set("details", "2013-05-27", "started")
          field.set("description", "This is a description.\\n"
                  "It spans across multiple lines.\\n")
          print field.save()
  
              [structured-field-start]
              This is StructuredField version 1. Please, edit with care.
  
              [project]
              Project Name
  
              [details]
              owner = somebody
              started = 2013-05-27
  
              [description]
              This is a description.
              It spans across multiple lines.
  
              [structured-field-end]
  
          field.version(0)
          print field.save()
  
              [project]
              Project Name
  
              [details]
              owner = somebody
              started = 2013-05-27
  
              [description]
              This is a description.
              It spans across multiple lines.
  
              [end]
  
      Multiple values for the same key are supported as well. Enable this
      feature with 'multi=True' when initializing the structured field.
      If multiple values are present their list will be returned instead
      of a single string. Similarly use list for setting multiple values::
  
          field = qe.StructuredField(multi=True)
          requirements = ['hypervisor=', 'labcontroller=lab.example.com']
          field.set("hardware", requirements, "hostrequire")
          print field.save()
  
              [structured-field-start]
              This is StructuredField version 1. Please, edit with care.
  
              [hardware]
              hostrequire = hypervisor=
              hostrequire = labcontroller=lab.example.com
  
              [structured-field-end]
  
          print field.get("hardware", "hostrequire")
  
              ['hypervisor=', 'labcontroller=lab.example.com']
      """
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Special
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def __init__(self, text=None, version=1, multi=False):
          """ Initialize the structured field """
          self.version(version)
          self._header = ""
          self._footer = ""
          self._sections = {}
          self._order = []
          self._multi = multi
          if text is not None:
              self.load(text)
  
      def __iter__(self):
          """ By default iterate through all available sections """
          for section in self._order:
              yield section
  
      def __nonzero__(self):
          """ True when any section is defined """
          return len(self._order) > 0
  
      __bool__ = __nonzero__
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Private
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def _load_version_zero(self, text):
          """ Load version 0 format """
          # Attempt to split the text according to the section tag
          section = re.compile(r"\n?^\[([^\]]+)\]\n", re.MULTILINE)
          parts = section.split(text)
          # If just one part ---> no sections present, just plain text
          if len(parts) == 1:
              self._header = parts[0]
              return
          # Pick header & footer, make sure [end] tag is present
          self._header = parts[0]
          self._footer = re.sub("^\n", "", parts[-1])
          if parts[-2] != "end":
              raise StructuredFieldError("No [end] section tag found")
          # Convert to dictionary and save the order
          keys = parts[1:-2:2]
          values = parts[2:-2:2]
          for key, value in zip(keys, values):
              self.set(key, value)
  
      def _load(self, text):
          """ Load version 1+ format """
          # The text must exactly match the format
          format = re.compile(
                  r"(.*)^\[structured-field-start\][ \t]*\n"
                  r"(.*)\n\[structured-field-end\][ \t]*\n(.*)",
                  re.DOTALL + re.MULTILINE)
          # No match ---> plain text or broken structured field
          matched = format.search(text)
          if not matched:
              if "[structured-field" in text:
                  raise StructuredFieldError("StructuredField parse error")
              self._header = text
              log.debug("StructuredField not found, treating as a plain text")
              return
          # Save header & footer (remove trailing new lines)
          self._header = re.sub("\n\n$", "\n", matched.groups()[0])
          if self._header:
              log.debug(u"Parsed header:\n{0}".format(self._header))
          self._footer = re.sub("^\n", "", matched.groups()[2])
          if self._footer:
              log.debug(u"Parsed footer:\n{0}".format(self._footer))
          # Split the content on the section names
          section = re.compile(r"\n\[([^\]]+)\][ \t]*\n", re.MULTILINE)
          parts = section.split(matched.groups()[1])
          # Detect the version
          try:
              self.version(int(re.search(
                  r"version (\d+)", parts[0]).groups()[0]))
              log.debug("Detected StructuredField version {0}".format(
                      self.version()))
          except AttributeError:
              log.error(parts[0])
              raise StructuredFieldError(
                      "Unable to detect StructuredField version")
          # Convert to dictionary, remove escapes and save the order
          keys = parts[1::2]
          escape = re.compile(r"^\[structured-field-escape\]", re.MULTILINE)
          values = [escape.sub("", value) for value in parts[2::2]]
          for key, value in zip(keys, values):
              self.set(key, value)
          log.debug(u"Parsed sections:\n{0}".format(
              pprint.pformat(self._sections)))
  
      def _save_version_zero(self):
          """ Save version 0 format """
          result = []
          if self._header:
              result.append(self._header)
          for section, content in self.iterate():
              result.append(u"[{0}]\n{1}".format(section, content))
          if self:
              result.append(u"[end]\n")
          if self._footer:
              result.append(self._footer)
          return "\n".join(result)
  
      def _save(self):
          """ Save version 1+ format """
          result = []
          # Regular expression for escaping section-like lines
          escape = re.compile(r"^(\[.+\])$", re.MULTILINE)
          # Header
          if self._header:
              result.append(self._header)
          # Sections
          if self:
              result.append(
                      u"[structured-field-start]\n"
                      u"This is StructuredField version {0}. "
                      u"Please, edit with care.\n".format(self._version))
              for section, content in self.iterate():
                  result.append(u"[{0}]\n{1}".format(section,
                          escape.sub("[structured-field-escape]\\1", content)))
              result.append(u"[structured-field-end]\n")
          # Footer
          if self._footer:
              result.append(self._footer)
          return "\n".join(result)
  
      def _read_section(self, content):
          """ Parse config section and return ordered dictionary """
          dictionary = OrderedDict()
          for line in content.split("\n"):
              # Remove comments and skip empty lines
              line = re.sub("#.*", "", line)
              if re.match(r"^\s*$", line):
                  continue
              # Parse key and value
              matched = re.search("([^=]+)=(.*)", line)
              if not matched:
                  raise StructuredFieldError(
                      "Invalid key/value line: {0}".format(line))
              key = matched.groups()[0].strip()
              value = matched.groups()[1].strip()
              # Handle multiple values if enabled
              if key in dictionary and self._multi:
                  if isinstance(dictionary[key], list):
                      dictionary[key].append(value)
                  else:
                      dictionary[key] = [dictionary[key], value]
              else:
                  dictionary[key] = value
          return dictionary
  
      def _write_section(self, dictionary):
          """ Convert dictionary into a config section format """
          section = ""
          for key in dictionary:
              if isinstance(dictionary[key], list):
                  for value in dictionary[key]:
                      section += "{0} = {1}\n".format(key, value)
              else:
                  section += "{0} = {1}\n".format(key, dictionary[key])
          return section
  
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #  StructuredField Methods
      # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
      def iterate(self):
          """ Return (section, content) tuples for all sections """
          for section in self:
              yield section, self._sections[section]
  
      def version(self, version=None):
          """ Get or set the StructuredField version """
          if version is not None:
              if version in [0, 1]:
                  self._version = version
              else:
                  raise StructuredFieldError(
                          "Bad StructuredField version: {0}".format(version))
          return self._version
  
      def load(self, text, version=None):
          """ Load the StructuredField from a string """
          if version is not None:
              self.version(version)
          # Make sure we got a text, convert to unicode if necessary
          try:
              if not isinstance(text, basestring):
                  raise StructuredFieldError(
                          "Invalid StructuredField, expecting string or unicode")
              if not isinstance(text, unicode):
                  text = text.decode("utf8")
          except NameError:
              if not isinstance(text, str):
                  raise StructuredFieldError(
                          "Invalid StructuredField, expecting string")
          # Remove possible carriage returns
          text = re.sub("\r\n", "\n", text)
          # Make sure the text has a new line at the end
          if text and text[-1] != "\n":
              text += "\n"
          log.debug(u"Parsing StructuredField\n{0}".format(text))
          # Parse respective format version
          if self._version == 0:
              self._load_version_zero(text)
          else:
              self._load(text)
  
      def save(self):
          """ Convert the StructuredField into a string """
          if self.version() == 0:
              return self._save_version_zero()
          else:
              return self._save()
  
      def header(self, content=None):
          """ Get or set the header content """
          if content is not None:
              self._header = content
          return self._header
  
      def footer(self, content=None):
          """ Get or set the footer content """
          if content is not None:
              self._footer = content
          return self._footer
  
      def sections(self):
          """ Get the list of available sections """
          return self._order
  
      def get(self, section, item=None):
          """ Return content of given section or section item """
          try:
              content = self._sections[section]
          except KeyError:
              raise StructuredFieldError(
                      "Section [{0}] not found".format(ascii(section)))
          # Return the whole section content
          if item is None:
              return content
          # Return only selected item from the section
          try:
              return self._read_section(content)[item]
          except KeyError:
              raise StructuredFieldError(
                      "Unable to read '{0}' from section '{1}'".format(
                      ascii(item), ascii(section)))
  
      def set(self, section, content, item=None):
          """ Update content of given section or section item """
          # Convert to string if necessary, keep lists untouched
          if not isinstance(content, list):
              try:
                  if not isinstance(content, basestring):
                      content = unicode(content)
                  elif not isinstance(content, unicode):
                      content = content.decode("utf8")
              except:
                  if not isinstance(content, str):
                      content = str(content)
          # Set the whole section content
          if item is None:
              # Add new line if missing
              if content and content[-1] != "\n":
                  content += "\n"
              self._sections[section] = content
          # Set only selected item from the section
          else:
              try:
                  current = self._sections[section]
              except KeyError:
                  current = ""
              dictionary = self._read_section(current)
              dictionary[item] = content
              self._sections[section] = self._write_section(dictionary)
          # Remember the order when adding a new section
          if section not in self._order:
              self._order.append(section)
  
      def remove(self, section, item=None):
          """ Remove given section or section item """
          # Remove the whole section
          if item is None:
              try:
                  del self._sections[section]
                  del self._order[self._order.index(section)]
              except KeyError:
                  raise StructuredFieldError(
                          "Section [{0}] not found".format(ascii(section)))
          # Remove only selected item from the section
          else:
              try:
                  dictionary = self._read_section(self._sections[section])
                  del(dictionary[item])
              except KeyError:
                  raise StructuredFieldError(
                          "Unable to remove '{0}' from section '{1}'".format(
                          ascii(item), ascii(section)))
              self._sections[section] = self._write_section(dictionary)
  
files: tmt/utils.py