├── .fmf
└── version
├── .git_archival.txt
├── .gitattributes
├── .github
├── CODEOWNERS
├── pull_request_template.md
└── workflows
│ ├── pre-commit.yml
│ └── release.yml
├── .gitignore
├── .packit.yaml
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── LICENSE
├── Makefile
├── README.rst
├── docs
├── Makefile
├── _static
│ └── .empty
├── concept.rst
├── conf.py
├── context.rst
├── contribute.rst
├── examples.rst
├── features.rst
├── header.txt
├── index.rst
├── modules.rst
├── overview.rst
├── releases.rst
└── toolbelt-catalog.yaml
├── examples
├── child
│ ├── .fmf
│ │ └── version
│ ├── main.fmf
│ ├── nobody
│ │ ├── .fmf
│ │ │ └── version
│ │ └── main.fmf
│ └── son
│ │ └── main.fmf
├── code
│ └── simple.py
├── conditions
│ ├── .fmf
│ │ └── version
│ └── main.fmf
├── deep
│ ├── .fmf
│ │ └── version
│ └── main.fmf
├── empty
│ ├── .fmf
│ │ └── version
│ ├── main.fmf
│ └── nothing
│ │ └── ignored.txt
├── hidden
│ ├── .fmf
│ │ ├── config
│ │ └── version
│ └── .plans
│ │ └── basic.fmf
├── merge
│ ├── .fmf
│ │ └── version
│ ├── main.fmf
│ ├── parent-dict.fmf
│ ├── parent-list.fmf
│ ├── parent.fmf
│ └── stray.fmf
├── scatter
│ ├── .fmf
│ │ └── version
│ ├── main.fmf
│ ├── object.fmf
│ └── object
│ │ └── main.fmf
├── touch
│ ├── .fmf
│ │ └── version
│ ├── main.fmf
│ └── runtest.sh
└── wget
│ ├── .fmf
│ └── version
│ ├── .hidden.fmf
│ ├── .hidden
│ └── main.fmf
│ ├── download
│ └── main.fmf
│ ├── main.fmf
│ ├── protocols
│ ├── ftp
│ │ └── main.fmf
│ ├── http
│ │ └── main.fmf
│ ├── https
│ │ └── main.fmf
│ └── main.fmf
│ ├── recursion
│ ├── deep.fmf
│ ├── fast.fmf
│ └── main.fmf
│ └── requirements
│ └── main.fmf
├── fmf.spec
├── fmf
├── __init__.py
├── _compat
│ ├── __init__.py
│ └── jsonschema.py
├── base.py
├── cli.py
├── context.py
└── utils.py
├── plans
├── features.fmf
├── integration.fmf
├── main.fmf
└── smoke.fmf
├── pyproject.toml
├── stories
├── docs.fmf
├── explore.fmf
└── select.fmf
└── tests
├── basic
├── main.fmf
└── test.sh
├── core
├── empty
│ ├── data
│ │ ├── .fmf
│ │ │ └── version
│ │ ├── empty.fmf
│ │ ├── one
│ │ │ └── main.fmf
│ │ ├── other
│ │ │ └── ignored
│ │ ├── two
│ │ │ └── empty.fmf
│ │ └── virtual
│ │ │ ├── main.fmf
│ │ │ └── name.fmf
│ ├── main.fmf
│ └── test.sh
├── inherit
│ ├── data
│ │ ├── .fmf
│ │ │ └── version
│ │ ├── ci.fmf
│ │ ├── full
│ │ │ └── main.fmf
│ │ ├── main.fmf
│ │ ├── mini
│ │ │ └── main.fmf
│ │ └── plans
│ │ │ ├── features.fmf
│ │ │ └── main.fmf
│ ├── invalid
│ │ ├── .fmf
│ │ │ └── version
│ │ └── directive.fmf
│ ├── main.fmf
│ └── test.sh
└── select
│ ├── data
│ ├── .fmf
│ │ └── version
│ └── foo
│ │ ├── child.fmf
│ │ ├── hidden.fmf
│ │ └── main.fmf
│ ├── main.fmf
│ └── test.sh
├── docs
├── main.fmf
└── test.sh
├── main.fmf
└── unit
├── assets
├── schema_base.yaml
├── schema_plan.yaml
├── schema_test.yaml
└── schema_test_ref.yaml
├── data
└── select_source
│ ├── .fmf
│ └── version
│ ├── foo
│ ├── inner
│ │ └── main.fmf
│ └── special.fmf
│ └── main.fmf
├── main.fmf
├── test_adjust.py
├── test_base.py
├── test_cli.py
├── test_context.py
├── test_items.py
├── test_modify.py
├── test_smoke.py
└── test_utils.py
/.fmf/version:
--------------------------------------------------------------------------------
1 | 1
2 |
--------------------------------------------------------------------------------
/.git_archival.txt:
--------------------------------------------------------------------------------
1 | node: 39af408279b305bd8744e1a74c4ad09958394587
2 | node-date: 2025-03-10T09:43:10+01:00
3 | describe-name: 1.7.0-1-g39af4082
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | .git_archival.txt export-subst
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in
5 | # the repo. Unless a later match takes precedence
6 | * @psss @lukaszachy @happz @thrix @janhavlin @martinhoyer
7 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Checklist
2 |
3 | * [ ] implement the feature
4 | * [ ] write the documentation
5 | * [ ] extend the test coverage
6 | * [ ] mention the version
7 | * [ ] include a release note
8 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | name: pre-commit
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | pre-commit:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-python@v2
14 | - uses: pre-commit/action@v2.0.2
15 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | release:
9 | name: 🚀 Release
10 | runs-on: ubuntu-latest
11 | environment:
12 | name: pypi
13 | url: https://pypi.org/p/fmf
14 | permissions:
15 | id-token: write # For pypi-publish
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Build package
19 | run: pipx run hatch build
20 | - name: Publish to PyPI
21 | uses: pypa/gh-action-pypi-publish@release/v1
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
3 | *.egg
4 | *.egg-info
5 | .pytest_cache
6 | *,cover
7 | .coverage
8 | /tmp/
9 | docs/_build
10 | docs/spec
11 | docs/stories
12 |
13 | # Python
14 |
15 | # Byte-compiled / optimized / DLL files
16 | __pycache__/
17 | *.py[cod]
18 | *$py.class
19 |
20 | # Distribution / packaging
21 | .Python
22 | build/
23 | develop-eggs/
24 | dist/
25 | downloads/
26 | eggs/
27 | .eggs/
28 | lib/
29 | lib64/
30 | parts/
31 | sdist/
32 | var/
33 | wheels/
34 | share/python-wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 | fmf.1
40 |
41 | # Testing
42 | .mypy_cache
43 | .pytest_cache
44 | *.tgz
45 | # Created by pytest-html reporting plugin
46 | /assets/style.css
47 | /report.html
48 |
49 | # Virtual environment
50 | .env
51 | .venv
52 | env/
53 | venv/
54 | ENV/
55 | env.bak/
56 | venv.bak/
57 |
58 | # Jetbrains
59 | .idea/
60 |
61 | # Vim
62 |
63 | # Swap
64 | [._]*.s[a-v][a-z]
65 | [._]*.sw[a-p]
66 | [._]s[a-rt-v][a-z]
67 | [._]ss[a-gi-z]
68 | [._]sw[a-p]
69 |
70 | # Session
71 | Session.vim
72 | Sessionx.vim
73 |
74 | # Temporary
75 | .netrwhist
76 | *~
77 |
78 | # Auto-generated tag files
79 | tags
80 |
81 | # Persistent undo
82 | [._]*.un~
83 |
84 | # Visual Studio Code
85 | .vscode
86 |
--------------------------------------------------------------------------------
/.packit.yaml:
--------------------------------------------------------------------------------
1 | specfile_path: fmf.spec
2 | files_to_sync:
3 | - fmf.spec
4 | - .packit.yaml
5 |
6 | upstream_package_name: fmf
7 | downstream_package_name: fmf
8 |
9 | # Epel9 fails to build because of too old version of setuptools_scm
10 | # Need to create archive with PKG-INFO
11 | actions:
12 | create-archive:
13 | - "hatch build -t sdist"
14 | - "sh -c 'echo dist/fmf-*.tar.gz'"
15 | get-current-version:
16 | - hatch version
17 |
18 | # Common definitions
19 | _:
20 | # Copr setup
21 | - &copr
22 | list_on_homepage: True
23 | preserve_project: True
24 | owner: "@teemtee"
25 |
26 | # Supported targets
27 | - targets: &targets
28 | - fedora-all
29 | - epel-9
30 | - epel-10
31 |
32 | srpm_build_deps:
33 | - hatch
34 |
35 | jobs:
36 | # Build pull requests
37 | - job: copr_build
38 | trigger: pull_request
39 | targets: *targets
40 |
41 | # Test pull requests
42 | - job: tests
43 | trigger: pull_request
44 | targets: *targets
45 |
46 | # Build commits merged to main (copr latest)
47 | - job: copr_build
48 | trigger: commit
49 | branch: main
50 | targets: *targets
51 | <<: *copr
52 | project: latest
53 | release_suffix: "{PACKIT_PROJECT_BRANCH}"
54 |
55 | # Build release (copr stable)
56 | - job: copr_build
57 | trigger: release
58 | targets: *targets
59 | <<: *copr
60 | project: stable
61 |
62 | # Propose downstream pull requests
63 | - job: propose_downstream
64 | trigger: release
65 | dist_git_branches: *targets
66 |
67 | # Build in Koji
68 | - job: koji_build
69 | trigger: commit
70 | allowed_pr_authors: ["packit", "all_committers"]
71 | allowed_committers: ["packit", "all_committers"]
72 | dist_git_branches: *targets
73 |
74 | # Create bodhi updates
75 | - job: bodhi_update
76 | trigger: commit
77 | dist_git_branches:
78 | - fedora-branched
79 | - epel-9
80 | - epel-10
81 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/hhatto/autopep8
5 | rev: 'v2.3.1'
6 | hooks:
7 | - id: autopep8
8 | args:
9 | - --recursive
10 | - --in-place
11 | - --aggressive
12 | - --aggressive
13 | - --hang-closing
14 | - --max-line-length=99
15 |
16 | - repo: https://github.com/PyCQA/isort
17 | rev: "5.13.2"
18 | hooks:
19 | - id: isort
20 |
21 | - repo: https://github.com/pycqa/flake8
22 | rev: "7.1.1"
23 | hooks:
24 | - id: flake8
25 | args:
26 | - --max-line-length=99
27 | files: >
28 | (?x)^(
29 | bin/.*|
30 | examples/.*|
31 | fmf/.*|
32 | tests/.*
33 | )$
34 |
35 | - repo: https://github.com/pre-commit/pre-commit-hooks
36 | rev: "v5.0.0"
37 | hooks:
38 | - id: end-of-file-fixer
39 | - id: mixed-line-ending
40 | - id: trailing-whitespace
41 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Config for building https://fmf.readthedocs.io/
2 | version: 2
3 |
4 | sphinx:
5 | configuration: docs/conf.py
6 |
7 | build:
8 | os: ubuntu-22.04
9 | tools:
10 | python: "3"
11 |
12 | python:
13 | install:
14 | - method: pip
15 | path: .
16 | extra_requirements: [docs]
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Prepare variables
2 | TMP = $(CURDIR)/tmp
3 | VERSION = $(shell hatch version)
4 | PACKAGE = fmf-$(VERSION)
5 | FILES = LICENSE README.rst \
6 | Makefile fmf.spec pyproject.toml \
7 | examples fmf tests
8 |
9 | # Define special targets
10 | all: docs packages
11 | .PHONY: docs hooks tmp
12 |
13 | # Temporary directory, include .fmf to prevent exploring tests there
14 | tmp:
15 | mkdir -p $(TMP)/.fmf
16 | mkdir -p $(TMP)/$(PACKAGE)
17 |
18 |
19 | # Run the test suite, optionally with coverage
20 | test: tmp
21 | hatch run test:unit
22 | smoke: tmp
23 | hatch run test:smoke
24 | coverage: tmp
25 | hatch run cov:cov
26 |
27 |
28 | # Build documentation, prepare man page
29 | docs: man
30 | hatch run docs:html
31 | man: tmp
32 | cp docs/header.txt $(TMP)/man.rst
33 | tail -n+7 README.rst >> $(TMP)/man.rst
34 | rst2man $(TMP)/man.rst > $(TMP)/$(PACKAGE)/fmf.1
35 |
36 |
37 | # RPM packaging
38 | tarball: man
39 | hatch build -t sdist
40 | rpm: tarball
41 | rpmbuild --define '_topdir $(TMP)' -bb fmf.spec
42 | srpm: tarball
43 | rpmbuild --define '_topdir $(TMP)' -bs fmf.spec
44 | packages: rpm srpm
45 |
46 |
47 | # Python packaging
48 | wheel:
49 | hatch build -t wheel
50 | upload: wheel tarball
51 | hatch publish
52 |
53 |
54 | # Vim tags and cleanup
55 | tags:
56 | find fmf -name '*.py' | xargs ctags --python-kinds=-i
57 | clean:
58 | rm -rf $(TMP) build dist .cache .pytest_cache fmf*.tar.gz
59 | find . -type f -name "*.py[co]" -delete
60 | find . -type f -name "*,cover" -delete
61 | find . -type d -name "__pycache__" -delete
62 | rm -rf docs/_build
63 | rm -f .coverage tags
64 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | ======================
3 | fmf
4 | ======================
5 |
6 | Flexible Metadata Format
7 |
8 |
9 | Description
10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11 |
12 | The ``fmf`` Python module and command line tool implement a
13 | flexible format for defining metadata in plain text files which
14 | can be stored close to the source code and structured in a
15 | hierarchical way with support for inheritance.
16 |
17 | Although the proposal initially originated from user stories
18 | centered around test execution, the format is general and thus
19 | can be used in broader scenarios, e.g. test coverage mapping.
20 |
21 | Using this approach it's also possible to combine both test
22 | execution metadata and test coverage information. Thanks to
23 | elasticity and hierarchy it provides ability to organize data
24 | into well-sized text documents while preventing duplication.
25 |
26 |
27 | Synopsis
28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29 |
30 | Command line usage is straightforward::
31 |
32 | fmf command [options]
33 |
34 | There are following commands available::
35 |
36 | fmf ls List identifiers of available objects
37 | fmf show Show metadata of available objects
38 | fmf init Initialize a new metadata tree
39 | fmf clean Remove cache directory and its content
40 |
41 |
42 | Examples
43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44 |
45 | List names of all objects stored in the metadata tree::
46 |
47 | fmf ls
48 |
49 | Show all test metadata (with 'test' attribute defined)::
50 |
51 | fmf show --key test
52 |
53 | Show metadata for all tree nodes (not only leaves)::
54 |
55 | fmf show --key test --whole
56 |
57 | List all attributes for the ``/recursion`` tests::
58 |
59 | fmf show --key test --name /recursion
60 |
61 | Show all covered requirements::
62 |
63 | fmf show --key requirement --key coverage
64 |
65 | Search for all tests with the ``Tier1`` tag defined and show a
66 | brief summary of what was found::
67 |
68 | fmf show --key test --filter tags:Tier1 --verbose
69 |
70 | Use arbitrary Python expressions to access deeper objects and
71 | create more complex conditions::
72 |
73 | fmf show --condition "execute['how'] == 'shell'"
74 |
75 | Initialize a new metadata tree in the current directory::
76 |
77 | fmf init
78 |
79 | Check help message of individual commands for the full list of
80 | available options.
81 |
82 |
83 | Options
84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
85 |
86 | Here is the list of the most frequently used options.
87 |
88 | Select
89 | ------
90 |
91 | Limit which metadata should be listed.
92 |
93 | --key=KEYS
94 | Key content definition (required attributes)
95 |
96 | --name=NAMES
97 | List objects with name matching regular expression
98 |
99 | --filter=FLTRS
100 | Apply advanced filter when selecting objects
101 |
102 | --condition=EXPR
103 | Use arbitrary Python expression for filtering
104 |
105 | --whole
106 | Consider the whole tree (leaves only by default)
107 |
108 | For filtering regular expressions can be used as well. See
109 | ``pydoc fmf.filter`` for advanced filtering options.
110 |
111 | Format
112 | ------
113 |
114 | Choose the best format for showing the metadata.
115 |
116 | --format=FMT
117 | Custom output format using the {} expansion
118 |
119 | --value=VALUES
120 | Values for the custom formatting string
121 |
122 | See online documentation for details about custom format.
123 |
124 | Utils
125 | -----
126 |
127 | Various utility options.
128 |
129 | --path PATHS
130 | Path to the metadata tree (default: current directory)
131 |
132 | --verbose
133 | Print additional information standard error output
134 |
135 | --debug
136 | Turn on debugging output, do not catch exceptions
137 |
138 | Check help message of individual commands for the full list of
139 | available options.
140 |
141 |
142 | Install
143 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
144 |
145 | The fmf package is available in Fedora and EPEL::
146 |
147 | dnf install fmf
148 |
149 | Install the latest version from the Copr repository::
150 |
151 | dnf copr enable @teemtee/fmf
152 | dnf install fmf
153 |
154 | or use PIP::
155 |
156 | pip install fmf
157 |
158 | See documentation for more details about installation options.
159 |
160 |
161 | Variables
162 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
163 |
164 | Here is the list of environment variables understood by fmf:
165 |
166 | FMF_CACHE_DIRECTORY
167 | Directory used to cache git clone calls for fmf identifiers.
168 |
169 |
170 | Links
171 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172 |
173 | Git:
174 | https://github.com/teemtee/fmf
175 |
176 | Docs:
177 | http://fmf.readthedocs.io/
178 |
179 | Issues:
180 | https://github.com/teemtee/fmf/issues
181 |
182 | Releases:
183 | https://github.com/teemtee/fmf/releases
184 |
185 | Copr:
186 | http://copr.fedoraproject.org/coprs/g/teemtee/fmf
187 |
188 | PIP:
189 | https://pypi.org/project/fmf/
190 |
191 |
192 | Authors
193 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
194 |
195 | Petr Šplíchal, Miro Hrončok, Jakub Krysl, Jan Ščotka, Alois
196 | Mahdal, Cleber Rosa, Miroslav Vadkerti, Lukáš Zachar, František
197 | Nečas, Evgeny Fedin, Pablo Martin, Zhaojuan Guo, Laura Barcziová,
198 | Petr Matyáš, Filip Vágner, Cristian Le, Ondrej Moriš, Martin
199 | Hoyer and Karel Šrot.
200 |
201 |
202 | Copyright
203 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
204 |
205 | Copyright (c) 2018-2021 Red Hat, Inc.
206 |
207 | This program is free software; you can redistribute it and/or
208 | modify it under the terms of the GNU General Public License as
209 | published by the Free Software Foundation; either version 2 of
210 | the License, or (at your option) any later version.
211 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
28 | @echo " text to make text files"
29 | @echo " man to make manual pages"
30 |
31 | clean:
32 | rm -rf $(BUILDDIR)/*
33 |
34 | html:
35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
36 | @echo
37 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
38 |
39 | latexpdf:
40 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
41 | @echo "Running LaTeX files through pdflatex..."
42 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
43 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
44 |
45 | text:
46 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
47 | @echo
48 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
49 |
50 | man:
51 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
52 | @echo
53 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
54 |
--------------------------------------------------------------------------------
/docs/_static/.empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teemtee/fmf/39af408279b305bd8744e1a74c4ad09958394587/docs/_static/.empty
--------------------------------------------------------------------------------
/docs/concept.rst:
--------------------------------------------------------------------------------
1 |
2 | ======================
3 | Concept
4 | ======================
5 |
6 | In order to keep test execution efficient when number of test
7 | cases grows, it is crucial to maintain corresponding metadata,
8 | which define some aspects of how the test coverage is executed.
9 |
10 | This tool implements a flexible format for defining metadata in
11 | plain text files which can be stored close to the test code and
12 | structured in a hierarchical way with support for inheritance.
13 |
14 | Although the proposal initially originated from user stories
15 | centered around test execution, the format is general and thus
16 | can be used in broader scenarios, e.g. test coverage mapping.
17 |
18 | Using this approach it's also possible to combine both test
19 | execution metadata and test coverage information. Thanks to
20 | elasticity and hierarchy it provides ability to organize data
21 | into well-sized text documents while preventing duplication.
22 |
23 |
24 | Stones
25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 |
27 | These are essential corner stones for the design:
28 |
29 | * Text files under version control
30 | * Keep common uses cases simple
31 | * Use hierarchy to organize content
32 | * Prevent duplication where possible
33 | * Metadata close to the test code
34 | * Solution should be open source
35 | * Focus on essential use cases
36 |
37 |
38 | Stories
39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 |
41 | Important user stories to be covered:
42 |
43 | * As a tester or developer I want to easy read and modify metadata and see history.
44 | * As a tester I want to select a subset of test cases for execution by specifying a tag.
45 | * As a tester I want to define a maximum time for a test case to run.
46 | * As a tester I want to specify which environment is relevant for testing.
47 | * As a user I want to easily define common metadata for multiple cases to simplify maintenance.
48 | * As a user I want to provide specific metadata for selected tests to complement common metadata.
49 | * As an individual tester and test contributor I want to execute specific single test case.
50 | * As an automation tool I need a metadata storage with good api, extensible, quick for reading.
51 |
52 |
53 | Choices
54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55 |
56 | These choices have been made:
57 |
58 | * Use git for version control and history of changes.
59 | * Yaml format easily readable for both machines and humans.
60 |
61 |
62 | Files
63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
64 |
65 | A dedicated file name extension ``fmf`` as an abbreviation of
66 | Flexible Metadata Format is used to easily find all metadata
67 | files on the filesystem:
68 |
69 | * smoke.fmf
70 | * main.fmf
71 |
72 | Special file name ``main.fmf`` works similarly as ``index.html``.
73 | It can be used to define the top level data for the directory. All
74 | metadata files are expected to be using the ``utf-8`` encoding.
75 |
76 |
77 | Attributes
78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79 |
80 | The format does not define attribute naming in any way. This is up
81 | to individual projects. The only exception is the special name
82 | ``main`` which is reserved for main directory index.
83 |
84 | Attribute namespacing can be introduced as needed to prevent
85 | collisions between similar attributes. For example:
86 |
87 | * test-description, requirement-description
88 | * test:description, requirement:description
89 | * test_description, requirement_description
90 |
91 |
92 | .. _trees:
93 |
94 | Trees
95 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
96 |
97 | Metadata form a tree where inheritance is applied. The tree root
98 | is defined by an ``.fmf`` directory (similarly as ``.git``
99 | identifies top of the git repository). The ``.fmf`` directory
100 | contains at least a ``version`` file with a single integer number
101 | defining version of the format.
102 |
103 |
104 | .. _config:
105 |
106 | Config
107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108 |
109 | By default, all hidden files are ignored when exploring metadata
110 | on the disk. If a specific file or directory should be included in
111 | the search, create a simple config file ``.fmf/config`` with the
112 | following format:
113 |
114 | .. code-block:: yaml
115 |
116 | explore:
117 | include:
118 | - .plans
119 | - .tests
120 |
121 | In the example above files or directories named ``.plans`` or
122 | ``.tests`` will be included in the discovered metadata. Note that
123 | the ``.fmf`` directory cannot be used for storing metadata.
124 |
125 |
126 | Names
127 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128 |
129 | Individual tree nodes are identified by path from the metadata
130 | root directory plus optional hierarchy defined inside yaml files.
131 | For example, let's have the metadata root defined in the ``wget``
132 | directory. Below you can see node names for different files:
133 |
134 |
135 | +-------------------------------+-----------------------+
136 | | Location | Name |
137 | +===============================+=======================+
138 | | wget/main.fmf | / |
139 | +-------------------------------+-----------------------+
140 | | wget/download/main.fmf | /download |
141 | +-------------------------------+-----------------------+
142 | | wget/download/smoke.fmf | /download/smoke |
143 | +-------------------------------+-----------------------+
144 |
145 |
146 | Identifiers
147 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
148 |
149 | Node names are unique across the metadata tree and thus can be
150 | used as identifiers for local referencing across the same tree. In
151 | order to reference remote fmf nodes from other trees a full ``fmf
152 | identifier`` is defined as a dictionary containing keys with the
153 | following meaning:
154 |
155 | url
156 | Git repository containing the metadata tree. Use any format
157 | acceptable by the ``git clone`` command. Optional, if no
158 | repository url is provided, local files will be used.
159 | ref
160 | Branch, tag or commit specifying the desired git revision.
161 | This is used to perform a ``git checkout`` in the repository.
162 | If not provided, the ``default branch`` is used.
163 | path
164 | Path to the metadata tree root. Should be relative to the git
165 | repository root if ``url`` provided, absolute local filesystem
166 | path otherwise. Optional, by default ``.`` is used.
167 | name
168 | Node name as defined by the hierarchy in the metadata tree.
169 | Optional, by default the parent node ``/`` is used, which
170 | represents the whole metadata tree.
171 |
172 | Here's a full fmf identifier example::
173 |
174 | url: https://github.com/psss/fmf
175 | ref: 0.10
176 | path: /examples/wget
177 | name: /download/test
178 |
179 | Use default values for ``ref`` and ``path`` to reference the
180 | latest version of the smoke plan from the default branch::
181 |
182 | url: https://github.com/psss/fmf
183 | name: /plans/smoke
184 |
185 | If desired, it is also possible to write the identifier on a
186 | single line as supported by the ``yaml`` format::
187 |
188 | {url: "https://github.com/psss/fmf", name: "/plans/smoke"}
189 |
190 | Let's freeze the stable test version by using a specific commit::
191 |
192 | url: https://github.com/psss/fmf
193 | ref: f24ef3f
194 | name: /tests/basic/filter
195 |
196 | Reference a smoke plan from another metadata tree stored on the
197 | local filesystem::
198 |
199 | path: /home/psss/git/tmt
200 | name: /plans/smoke
201 |
202 | Local reference across the same metadata tree is also supported::
203 |
204 | name: /plans/smoke
205 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # fmf documentation build configuration file, created by
4 | # sphinx-quickstart on Mon Apr 27 17:44:03 2015.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import os
16 | import sys
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | sys.path.insert(0, os.path.abspath('../'))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | # needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be
29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
30 | # ones.
31 | extensions = [
32 | 'sphinx.ext.autodoc',
33 | 'sphinx_rtd_theme',
34 | ]
35 |
36 | # Add any paths that contain templates here, relative to this directory.
37 | templates_path = ['_templates']
38 |
39 | # The suffix(es) of source filenames.
40 | # You can specify multiple suffix as a list of string:
41 | # source_suffix = ['.rst', '.md']
42 | source_suffix = '.rst'
43 |
44 | # The encoding of source files.
45 | # source_encoding = 'utf-8-sig'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 | master_man = 'man.1'
50 |
51 | # General information about the project.
52 | project = u'fmf'
53 | copyright = u'2015, Petr Šplíchal'
54 | author = u'Petr Šplíchal'
55 |
56 | # The version info for the project you're documenting, acts as replacement for
57 | # |version| and |release|, also used in various other places throughout the
58 | # built documents.
59 | #
60 | # The short X.Y version.
61 | version = ''
62 | # The full version, including alpha/beta/rc tags.
63 | release = ''
64 |
65 | # The language for content autogenerated by Sphinx. Refer to documentation
66 | # for a list of supported languages.
67 | #
68 | # This is also used if you do content translation via gettext catalogs.
69 | # Usually you set "language" from the command line for these cases.
70 | language = "en"
71 |
72 | # There are two options for replacing |today|: either, you set today to some
73 | # non-false value, then it is used:
74 | # today = ''
75 | # Else, today_fmt is used as the format for a strftime call.
76 | # today_fmt = '%B %d, %Y'
77 |
78 | # List of patterns, relative to source directory, that match files and
79 | # directories to ignore when looking for source files.
80 | exclude_patterns = ['_build']
81 |
82 | # The reST default role (used for this markup: `text`) to use for all
83 | # documents.
84 | # default_role = None
85 |
86 | # If true, '()' will be appended to :func: etc. cross-reference text.
87 | # add_function_parentheses = True
88 |
89 | # If true, the current module name will be prepended to all description
90 | # unit titles (such as .. function::).
91 | # add_module_names = True
92 |
93 | # If true, sectionauthor and moduleauthor directives will be shown in the
94 | # output. They are ignored by default.
95 | # show_authors = False
96 |
97 | # The name of the Pygments (syntax highlighting) style to use.
98 | pygments_style = 'sphinx'
99 |
100 | # A list of ignored prefixes for module index sorting.
101 | # modindex_common_prefix = []
102 |
103 | # If true, keep warnings as "system message" paragraphs in the built documents.
104 | # keep_warnings = False
105 |
106 | # If true, `todo` and `todoList` produce output, else they produce nothing.
107 | todo_include_todos = False
108 |
109 |
110 | # -- Options for HTML output ----------------------------------------------
111 |
112 | # The theme to use for HTML and HTML Help pages. See the documentation for
113 | # a list of builtin themes.
114 | html_theme = "sphinx_rtd_theme"
115 |
116 | # Theme options are theme-specific and customize the look and feel of a theme
117 | # further. For a list of options available for each theme, see the
118 | # documentation.
119 | # html_theme_options = {}
120 |
121 | # Add any paths that contain custom themes here, relative to this directory.
122 | # html_theme_path = []
123 |
124 | # The name for this set of Sphinx documents. If None, it defaults to
125 | # " v documentation".
126 | # html_title = None
127 |
128 | # A shorter title for the navigation bar. Default is the same as html_title.
129 | # html_short_title = None
130 |
131 | # The name of an image file (relative to this directory) to place at the top
132 | # of the sidebar.
133 | # html_logo = None
134 |
135 | # The name of an image file (within the static path) to use as favicon of the
136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
137 | # pixels large.
138 | # html_favicon = None
139 |
140 | # Add any paths that contain custom static files (such as style sheets) here,
141 | # relative to this directory. They are copied after the builtin static files,
142 | # so a file named "default.css" will overwrite the builtin "default.css".
143 | html_static_path = ['_static']
144 |
145 | # Add any extra paths that contain custom files (such as robots.txt or
146 | # .htaccess) here, relative to this directory. These files are copied
147 | # directly to the root of the documentation.
148 | # html_extra_path = []
149 |
150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
151 | # using the given strftime format.
152 | # html_last_updated_fmt = '%b %d, %Y'
153 |
154 | # If true, SmartyPants will be used to convert quotes and dashes to
155 | # typographically correct entities.
156 | # html_use_smartypants = True
157 |
158 | # Custom sidebar templates, maps document names to template names.
159 | # html_sidebars = {}
160 |
161 | # Additional templates that should be rendered to pages, maps page names to
162 | # template names.
163 | # html_additional_pages = {}
164 |
165 | # If false, no module index is generated.
166 | # html_domain_indices = True
167 |
168 | # If false, no index is generated.
169 | # html_use_index = True
170 |
171 | # If true, the index is split into individual pages for each letter.
172 | # html_split_index = False
173 |
174 | # If true, links to the reST sources are added to the pages.
175 | # html_show_sourcelink = True
176 |
177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
178 | # html_show_sphinx = True
179 |
180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
181 | # html_show_copyright = True
182 |
183 | # If true, an OpenSearch description file will be output, and all pages will
184 | # contain a tag referring to it. The value of this option must be the
185 | # base URL from which the finished HTML is served.
186 | # html_use_opensearch = ''
187 |
188 | # This is the file name suffix for HTML files (e.g. ".xhtml").
189 | # html_file_suffix = None
190 |
191 | # Language to be used for generating the HTML full-text search index.
192 | # Sphinx supports the following languages:
193 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
194 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
195 | # html_search_language = 'en'
196 |
197 | # A dictionary with options for the search language support, empty by default.
198 | # Now only 'ja' uses this config value
199 | # html_search_options = {'type': 'default'}
200 |
201 | # The name of a javascript file (relative to the configuration directory) that
202 | # implements a search results scorer. If empty, the default will be used.
203 | # html_search_scorer = 'scorer.js'
204 |
205 | # Output file base name for HTML help builder.
206 | htmlhelp_basename = 'fmfdoc'
207 |
208 | # -- Options for manual page output ---------------------------------------
209 |
210 | # One entry per manual page. List of tuples
211 | # (source start file, name, description, authors, manual section).
212 | man_pages = [
213 | (master_man, 'fmf', u'fmf Documentation',
214 | [author], 1)
215 | ]
216 |
217 | # If true, show URL addresses after external links.
218 | # man_show_urls = False
219 |
--------------------------------------------------------------------------------
/docs/context.rst:
--------------------------------------------------------------------------------
1 | .. _context:
2 |
3 | ======================
4 | Context
5 | ======================
6 |
7 | Motivation
8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9 |
10 | Imagine you have a test which can run only for Fedora 33 and
11 | newer. Or your tests' require depend on which distribution you
12 | are running. For these cases you need just a slight tweak to your
13 | metadata but you can't really use the :ref:`virtual` cases as only
14 | one of them should run.
15 |
16 | This is exactly where adjusting metadata based on the given
17 | Context will help you. Let's see some examples to demonstrate the
18 | usage on a real-life use case.
19 |
20 | Disable test by setting the ``enabled`` attribute::
21 |
22 | enabled: true
23 | adjust:
24 | enabled: false
25 | when: distro < fedora-33
26 | because: The feature was added in Fedora-33
27 |
28 | Tweak the ``require`` attribute for an older distro::
29 |
30 | require:
31 | - procps-ng
32 | adjust:
33 | require: procps
34 | when: distro ~= centos-6
35 |
36 |
37 | Syntax
38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
39 |
40 | To get a better idea of the ``when`` condition syntax including
41 | supported operators consult the following grammar outline::
42 |
43 | condition ::= expression (bool expression)*
44 | bool ::= and | or
45 | expression ::= dimension binary_operator values
46 | expression ::= dimension unary_operator
47 | expression ::= 'true' | 'false'
48 | dimension ::= [[:alnum:-]]+
49 | binary_operator ::= '==' | '!=' | '<' | '<=' | '>' | '>=' |
50 | '~=' | '~!=' | '~<' | '~<=' | '~>' | '~>=' | '~' | '!~'
51 | unary_operator ::= 'is defined' | 'is not defined'
52 | values ::= value (',' value)*
53 | value ::= [[:alnum:]]+
54 |
55 | Let's demonstrate the syntax on a couple of real-life examples::
56 |
57 | # check distro, compare specific release
58 | distro == fedora
59 | distro >= fedora-33
60 |
61 | # use boolean operators to build more complex expressions
62 | distro == fedora and arch = x86_64
63 | distro >= fedora-33 or distro >= rhel-8
64 |
65 | # check whether a dimension is defined
66 | collection is not defined
67 |
68 | # search dimension value for a regular expression
69 | initiator ~ .*-ci
70 |
71 | # make sure that the value does not match given regular expression
72 | arch !~ ppc64.*
73 |
74 | # disable adjust rule (e.g. during debugging / experimenting)
75 | false and
76 |
77 | # always enabled adjust rule (same as if the `when` key is omitted)
78 | true
79 |
80 | The comma operator can be used to shorten the ``or`` expressions::
81 |
82 | # the following two lines are equivalent
83 | arch == x86_64 or arch == ppc64
84 | arch == x86_64, ppc64
85 |
86 | # works for less/greater than comparison as well
87 | distro < fedora-33 or distro < rhel-8
88 | distro < fedora-33, rhel-8
89 |
90 | .. warning::
91 |
92 | Do not use the comma operator with the ``!=`` comparison.
93 | It is currently implemented with the ``or`` logic which is a
94 | bit weird, confusing to the users and it will be most probably
95 | changed to ``and`` in the future so that it can be interpreted
96 | as "none of the values in the list is equal".
97 |
98 |
99 | Lazy Evaluation
100 | ---------------
101 |
102 | Operator ``and`` takes precedence over ``or`` and rule evaluation
103 | is lazy. It stops immediately when we know the final result.
104 |
105 | Boolean Operations
106 | ------------------
107 |
108 | When a dimension or outcome of the operation is not defined,
109 | the expression is treated as ``CannotDecide``.
110 |
111 | Boolean operations with ``CannotDecide``::
112 |
113 | CannotDecide and True == CannotDecide
114 | CannotDecide and False == False
115 | CannotDecide or True == True
116 | CannotDecide or False == CannotDecide
117 | CannotDecide and CannotDecide == CannotDecide
118 | CannotDecide or CannotDecide == CannotDecide
119 |
120 |
121 | Dimensions
122 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
123 |
124 | Each Dimension is a view on the Context in which metadata can be
125 | adjusted. For example it can be arch, distro, component, product
126 | or pipeline in which we run tests and so on.
127 |
128 | Values are always converted to a string representation. Each
129 | value is treated as if it was a component with version. Name of
130 | the dimension doesn't matter, all are treated equally.
131 |
132 | Values are case-sensitive by default, which means that values like
133 | ``centos`` and ``CentOS`` are considered different. When calling
134 | the ``adjust()`` method on the tree, ``case_sensitive=False`` can
135 | be used to make the value comparison case insensitive.
136 |
137 | The characters ``:`` or ``.`` or ``-`` are used as version
138 | separators and are handled in the same way. The following examples
139 | demonstrate how the ``name`` and ``version`` parts are parsed::
140 |
141 | centos-8.3.0
142 | name: centos
143 | version: 8, 3, 0
144 |
145 | python3-3.8.5-5.fc32
146 | name: python3
147 | version: 3, 8, 5, 5, fc32
148 |
149 | x86_64
150 | name: x86_64
151 | version: no version parts
152 |
153 |
154 | Comparison
155 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
156 |
157 | Value on the left always comes from dimension, it describes what
158 | is known about the context and should be as specific as possible
159 | (this is up to the calling tool). Value on the right comes from
160 | the rule and the creator of this rule sets how precise they want
161 | to be.
162 |
163 | When the left side is not specific enough its missing version
164 | parts are treated as if they were lower than the right side.
165 | However, the left side needs to contain at least one version
166 | part::
167 |
168 | git-2.3.4 < git-3 # True
169 | git-2 < git-3.2.1 # True
170 | git < git-3.2.1 # CannotDecide
171 |
172 |
173 | Equality vs Comparison
174 | ----------------------
175 |
176 | It is always possible to evaluate whether two values are (not)
177 | equal. When the name and common version parts requested by the
178 | right side match then the two values are equal::
179 |
180 | git-2.3.4 == git-2.3.4
181 | git-2.3.4 == git-2.3
182 | git-2.3.4 == git-2
183 | git-2.3.4 == git
184 | git-2.3.4 != git-1
185 | git-2.3.4 != fmf
186 |
187 | However, comparing order of two values is defined only if they
188 | match by name. If names don't match then values cannot be
189 | compared and the expression has ``CannotDecide`` outcome::
190 |
191 | git-2.3.4 >= git-2 # True
192 | git-2.3.4 >= git-3 # False
193 | git-2.3.4 >= fmf-2 # CannotDecide
194 |
195 |
196 | Major Version
197 | -------------
198 |
199 | Comparing distributions across their major versions can be tricky.
200 | One cannot easily say that e.g. ``centos-8.0 > centos-7.9``. In
201 | this case ``centos-8.0`` was released sooner than ``centos-7.9``
202 | so is it really newer?
203 |
204 | Quite often new features are implemented in given minor version
205 | such as ``centos-7.9`` or ``centos-8.2`` which does not mean they
206 | are available in ``centos-8.1`` so it is not possible to apply a
207 | single rule such as ``distro >= centos-7.9`` to cover this case.
208 |
209 | Another usage for this operators is to check for features specific
210 | to a particular major version or a module stream.
211 |
212 | The following operators make it possible to compare only within
213 | the same major version::
214 |
215 | '~=' | '~!=' | '~<' | '~<=' | '~>' | '~>='
216 |
217 | If their major versions are different then their minor versions
218 | cannot be compared and as such are skipped during evaluation. The
219 | following example shows how the special less than operator ``~<``
220 | would be evaluated for given `centos` versions. Note that the
221 | right side defines if the minor comparison is evaluated or not.
222 |
223 | ========== ============ ============ ==========
224 | ~< centos-7.9 centos-8.2 centos-8
225 | centos-7.8 True CannotDecide True
226 | centos-7.9 False CannotDecide True
227 | centos-7 CannotDecide CannotDecide True
228 | centos-8.1 CannotDecide True False
229 | centos-8.2 CannotDecide False False
230 | centos-8 CannotDecide CannotDecide False
231 | ========== ============ ============ ==========
232 |
233 | Here is a couple of examples to get a better idea of how the
234 | comparison works for some special cases::
235 |
236 | fedora < fedora-33 ---> cannot (left side has no version parts)
237 | fedora-33 == fedora ---> True (right side wants only name)
238 | fedora-33 < fedora-rawhide ---> True (rawhide is newer than any number)
239 |
240 | centos-8.4.0 == centos ---> True
241 | centos-8.4.0 < centos-9 ---> True
242 | centos-8.4.0 ~< centos-9 ---> True (no minor comparison requested)
243 | centos-8.4.0 ~< centos-9.2 ---> cannot (minor comparison requested)
244 |
--------------------------------------------------------------------------------
/docs/contribute.rst:
--------------------------------------------------------------------------------
1 | .. _contribute:
2 |
3 | ==================
4 | Contribute
5 | ==================
6 |
7 |
8 | Introduction
9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10 |
11 | Feel free and welcome to contribute to this project. You can start
12 | with filing issues and ideas for improvement in GitHub tracker__.
13 | Our favorite thoughts from The Zen of Python:
14 |
15 | * Beautiful is better than ugly.
16 | * Simple is better than complex.
17 | * Readability counts.
18 |
19 | We respect the `PEP8`__ Style Guide for Python Code. Here's a
20 | couple of recommendations to keep on mind when writing code:
21 |
22 | * Maximum line length is 99 for code and 72 for documentation.
23 | * Comments should be complete sentences.
24 | * The first word should be capitalized (unless identifier).
25 | * When using hanging indent, the first line should be empty.
26 | * The closing brace/bracket/parenthesis on multiline constructs
27 | is under the first non-whitespace character of the last line
28 |
29 | __ https://github.com/psss/fmf
30 | __ https://www.python.org/dev/peps/pep-0008/
31 |
32 |
33 | Commits
34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35 |
36 | It is challenging to be both concise and descriptive, but that is
37 | what a well-written summary should do. Consider the commit message
38 | as something that will be pasted into release notes:
39 |
40 | * The first line should have up to 50 characters.
41 | * Complete sentence with the first word capitalized.
42 | * Should concisely describe the purpose of the patch.
43 | * Do not prefix the message with file or module names.
44 | * Other details should be separated by a blank line.
45 |
46 | Why should I care?
47 |
48 | * It helps others (and yourself) find relevant commits quickly.
49 | * The summary line will be re-used later (e.g. for rpm changelog).
50 | * Some tools do not handle wrapping, so it is then hard to read.
51 | * You will make the maintainers happy to read beautiful commits :)
52 |
53 | You can get some more context in the `stackoverflow`__ article.
54 |
55 | __ http://stackoverflow.com/questions/2290016/
56 |
57 |
58 | Develop
59 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 |
61 | In order to experiment, play with the latest bits and develop
62 | improvements it is best to use a virtual environment::
63 |
64 | mkvirtualenv fmf
65 | git clone https://github.com/psss/fmf
66 | cd fmf
67 | pip install -e .
68 |
69 | Install ``python3-virtualenvwrapper`` to easily create and enable
70 | virtual environments using ``mkvirtualenv`` and ``workon``. Note
71 | that if you have freshly installed the package you need to open a
72 | new shell session to enable the wrapper functions.
73 |
74 | Install the ``pre-commit`` hooks to run all available checks for
75 | your commits to the project::
76 |
77 | pip install pre-commit
78 | pre-commit install
79 |
80 | Or simply install all extra dependencies to make sure you have
81 | everything needed for the development ready on your system::
82 |
83 | pip install '.[all]'
84 |
85 |
86 | Makefile
87 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
88 |
89 | There are several Makefile targets defined to make the common
90 | daily tasks easy & efficient:
91 |
92 | make test
93 | Execute the test suite.
94 |
95 | make smoke
96 | Perform quick basic functionality test.
97 |
98 | make coverage
99 | Run the test suite under coverage and report results.
100 |
101 | make docs
102 | Build documentation.
103 |
104 | make packages
105 | Build rpm and srpm packages.
106 |
107 | make tags
108 | Create or update the Vim ``tags`` file for quick searching.
109 | You might want to use ``set tags=./tags;`` in your ``.vimrc``
110 | to enable parent directory search for the tags file as well.
111 |
112 | make clean
113 | Cleanup all temporary files.
114 |
115 |
116 | Tests
117 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118 |
119 | Run the default set of tests directly on your localhost::
120 |
121 | tmt run
122 |
123 | To run tests using pytest with the test coverage overview::
124 |
125 | make coverage
126 |
127 | Install pytest and coverage using dnf or pip::
128 |
129 | dnf install python3-pytest python3-coverage
130 | pip install .[tests]
131 |
132 |
133 | Docs
134 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
135 |
136 | For building documentation locally install necessary modules::
137 |
138 | pip install .[docs]
139 |
140 | Make sure docutils are installed in order to build man pages::
141 |
142 | dnf install python3-docutils
143 |
144 | Building documentation is then quite straightforward::
145 |
146 | make docs
147 |
148 | Find the resulting html pages under the ``docs/_build/html``
149 | folder.
150 |
--------------------------------------------------------------------------------
/docs/examples.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | Examples
3 | ======================
4 |
5 | Let's have a look at a couple of real-life examples!
6 |
7 |
8 | Coverage
9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10 |
11 | Test coverage information can be stored in a single file, for
12 | example ``wget/requirements.fmf``::
13 |
14 | /protocols:
15 | priority: high
16 | /ftp:
17 | requirement: Download a file using the ftp protocol.
18 | coverage: wget/protocols/ftp
19 | /http:
20 | requirement: Download a file using the http protocol.
21 | coverage: wget/protocols/http
22 | /https:
23 | requirement: Download a file using the https protocol.
24 | coverage: wget/protocols/https
25 |
26 | /download:
27 | priority: medium
28 | /output-document-pipe:
29 | requirement: Save content to pipe.
30 | coverage: wget/download
31 | /output-document-file:
32 | requirement: Save content to a file.
33 | coverage: wget/download
34 |
35 | /upload:
36 | priority: medium
37 | /post-file:
38 | requirement: Upload a file to the server
39 | coverage: wget/protocols/http
40 | /post-data:
41 | requirement: Upload a string to the server
42 | coverage: wget/protocols/http
43 |
44 | Or split by functionality area into separate files as desired, for
45 | example ``wget/download/requirements.fmf``::
46 |
47 | priority: medium
48 | /output-document-pipe:
49 | requirement: Save content to pipe.
50 | coverage: wget/download
51 | /output-document-file:
52 | requirement: Save content to a file.
53 | coverage: wget/download
54 |
55 | Or integrated with test case metadata, e.g.
56 | ``wget/download/main.fmf``::
57 |
58 | description: Check basic download options
59 | tags: [Tier2, TierSecurity]
60 | test: runtest.sh
61 | time: 3 min
62 |
63 | /requirements:
64 | requirement: Various download options working correctly
65 | priority: low
66 | /get-file:
67 | coverage: wget/download
68 | /output-document:
69 | coverage: wget/download
70 | /continue:
71 | /timestamping:
72 | /tries:
73 | /no-clobber:
74 | coverage: wget/download
75 | /progress:
76 | /quota:
77 | /server-response:
78 | /bind-address:
79 | /spider:
80 |
81 | In the example above three requirements are already covered,
82 | the rest still await for test coverage (attributes value is null).
83 |
84 |
85 | Strategist
86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
87 |
88 | Here's an example implementation of test-strategist__ data for
89 | openscap using the Flexible Metadata Format::
90 |
91 | /probes:
92 | description: Probes
93 | /offline:
94 | description: Offline scanning
95 | /online:
96 | description: Online scanning
97 | /scanning:
98 | description: Reading and understanding source datastreams
99 | /oval:
100 | influencers:
101 | - openscap/probes/offline
102 | - openscap/probes/online
103 | /ds:
104 | influencers:
105 | - openscap/scanning/oval
106 | - openscap/scanning/cpe
107 | /cpe:
108 | influencers:
109 | - openscap/scanning/oval
110 |
111 | __ https://github.com/dahaic/test-strategist
112 |
113 |
114 | Setups
115 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
116 |
117 | This example shows how to use Flexible Metadata Format to
118 | run tests with different storage setups including cleanup.
119 | This is simplified metadata, whole example including tools
120 | can be found at storage_setup__::
121 |
122 | /setups:
123 | description: Tests to prepare and clean up devices for tests
124 | setup: True
125 | /setup_local:
126 | test: setup_local.py
127 | requires_cleanup: setups/cleanup_local
128 | /cleanup_local:
129 | test: cleanup_local.py
130 | /setup_remote:
131 | test: setup_remote.py
132 | requires_cleanup: setups/cleanup_remote
133 | /cleanup_remote:
134 | test: cleanup_remote.py
135 | /setup_vdo:
136 | test: setup_vdo.py
137 | requires_cleanup: setups/cleanup_vdo
138 | /cleanup_vdo:
139 | test: cleanup_vdo.py
140 | /tests:
141 | description: Testing 'vdo' command line tool
142 | requires_setup: [setups/setup_vdo]
143 | /create
144 | description: Testing 'vdo create'
145 | /ack_threads
146 | /activate
147 | /modify
148 | description: Testing 'vdo modify'
149 | requires_setup+: [setups/setup_remote]
150 | /block_map_cache_size
151 |
152 | __ https://github.com/jkrysl/storage_setup
153 |
154 | You can find here not only how to use FMF for setup/cleanup
155 | and group tests based on that, but also installing requirements,
156 | passing values from metadata to tests themself and much more.
157 |
158 |
159 | Format
160 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
161 |
162 | Custom format output using ``--format`` and ``value``.
163 |
164 | List object name and selected attribute::
165 |
166 | fmf examples/wget --format '{0}: {1}\n' \
167 | --value 'name' --value 'data["tester"]'
168 |
169 | Show missing attributes in red::
170 |
171 | fmf examples/wget/ --format '{}: {}\n' --value 'name' \
172 | --value 'utils.color(str(data.get("priority")),
173 | "red" if data.get("priority") is None else "green")'
174 |
175 | List all test scripts with full path::
176 |
177 | fmf examples --key test --format "{}/{}/{}\n" \
178 | --value "os.getcwd()" \
179 | --value "data.get('path') or name" \
180 | --value "data['test']"
181 |
--------------------------------------------------------------------------------
/docs/features.rst:
--------------------------------------------------------------------------------
1 |
2 | ======================
3 | Features
4 | ======================
5 |
6 | Let's demonstrate the features on a simple wget example with the
7 | following directory structure::
8 |
9 | wget
10 | ├── download
11 | ├── protocols
12 | │ ├── ftp
13 | │ ├── http
14 | │ └── https
15 | ├── recursion
16 | └── smoke
17 |
18 |
19 | Simple
20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21 |
22 | The most common use cases super simple to read & write. Test
23 | metadata for a single test look like this::
24 |
25 | description: Check basic download options
26 | tester: Petr Šplíchal
27 | tags: [Tier2, TierSecurity]
28 | test: runtest.sh
29 | time: 3 min
30 |
31 |
32 | Hierarchy
33 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
34 |
35 | Hierarchy is defined by directory structure (see example above) and
36 | explicit nesting using attributes starting with ``/``. Defining
37 | metadata for several tests in a single file is straightforward::
38 |
39 | /download:
40 | description: Check basic download options
41 | tester: Petr Šplíchal
42 | tags: [Tier2, TierSecurity]
43 | test: runtest.sh
44 | time: 3 min
45 | /recursion:
46 | description: Check recursive download options
47 | tester: Petr Šplíchal
48 | tags: [Tier2, TierSecurity]
49 | test: runtest.sh
50 | time: 20 min
51 |
52 | Content above would be stored in ``wget/main.fmf`` file.
53 |
54 |
55 | Inheritance
56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
57 |
58 | Metadata is inherited from parent objects::
59 |
60 | tester: Petr Šplíchal
61 | tags: [Tier2, TierSecurity]
62 | test: runtest.sh
63 |
64 | /download:
65 | description: Check basic download options
66 | time: 3 min
67 | /recursion:
68 | description: Check recursive download options
69 | time: 20 min
70 |
71 | This nicely prevents unnecessary duplication. Redefining an
72 | attribute in a child object will by default overwrite value
73 | inherited from the parent.
74 |
75 | If inheriting data from parent is not desired in particular node
76 | of the tree it is possible to disable it using the following
77 | directive::
78 |
79 | /:
80 | inherit: false
81 |
82 |
83 | .. _merging:
84 |
85 | Merging
86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
87 |
88 | When inheriting values from the parent it is also possible to use
89 | special attribute suffixes to merge child value with parent data.
90 | Append a ``+`` sign to the attribute name to add given value::
91 |
92 | time: 1
93 | /download:
94 | time+: 3
95 |
96 | This operation is possible only for attributes of the same type
97 | or when merging a dictionary with a list.
98 | Exception ``MergeError`` is raised if types are not compatible. When
99 | the ``+`` suffix is applied on dictionaries ``update()`` method is
100 | used to merge content of given dictionary instead of replacing it.
101 | When such parent dictionary doesn't exist, but the suffix ``+`` is used
102 | at the top level, the dictionary is created and named without the suffix.
103 | However, for dictionaries at deeper levels, the suffix remains in their name.
104 |
105 | Example: Merging dictionary with a list::
106 |
107 | discover:
108 | how: fmf
109 | filter: "tier:1"
110 |
111 | /path:
112 | discover+:
113 | - name: upstream
114 | url: https://some.url
115 | - name: downstream
116 | url: https://other.url
117 |
118 | results in::
119 |
120 | /path:
121 | discover:
122 | - name: upstream
123 | url: https://some.url
124 | how: fmf
125 | filter: "tier:1"
126 | - name: downstream
127 | url: https://other.url
128 | how: fmf
129 | filter: "tier:1"
130 |
131 | Example: Merging list with a dictionary::
132 |
133 | discover:
134 | - how: fmf
135 | url: https://github.com/project1
136 | - how: fmf
137 | url: https://github.com/project2
138 |
139 | /tier1:
140 | discover+:
141 | filter: "tier:1"
142 | /tier2:
143 | discover+:
144 | filter: "tier:2"
145 |
146 | results in::
147 |
148 | /tier1:
149 | discover:
150 | - how: fmf
151 | url: https://github.com/project1
152 | filter: "tier:1"
153 | - how: fmf
154 | url: https://github.com/project2
155 | filter: "tier:1"
156 | /tier2:
157 | discover:
158 | - how: fmf
159 | url: https://github.com/project1
160 | filter: "tier:2"
161 | - how: fmf
162 | url: https://github.com/project2
163 | filter: "tier:2"
164 |
165 | Example: Merging with not yet defined dictionaries::
166 |
167 | environment+:
168 | CLEAR: "1"
169 | adjust+:
170 | when: distro == fedora
171 | environment+:
172 | FEDORA: "1"
173 |
174 | results in (no ``+`` suffix in ``adjust`` and top ``environment`` keys)::
175 |
176 | adjust:
177 | when: distro == fedora
178 | environment+:
179 | FEDORA: "1"
180 | environment:
181 | CLEAR: "1"
182 |
183 |
184 | The special suffix ``+<`` can be used to prepend values instead of
185 | appending them. This might be handy when adjusting lists::
186 |
187 | steps:
188 | - one
189 | - two
190 | - three
191 |
192 | /complete:
193 | steps+<:
194 | - zero
195 |
196 | In a similar way, appending a ``-`` sign will reduce or remove
197 | parent value from parent's attribute (which has to be defined)::
198 |
199 | time-: 5
200 | tags-: [Tier2]
201 | desc-: details.*
202 | vars-: [z]
203 |
204 | Numbers are subtracted, list items are removed from the parent
205 | attribute, matching regular expressions are replaced by an empty
206 | string. For dictionaries it's possible to provide list of keys
207 | which should be removed.
208 |
209 | Substitution of current values can be done by appending a ``~``
210 | suffix to the key name. The pattern and replacement parameters
211 | need to be provided as values in the form of
212 | ``PATTERNREPLACEMENT``, where ```` is delimiter which
213 | can be any character however such character cannot be then used
214 | within PATTERN and REPLACEMENT text as escaping isn't supported.
215 | This input can be either a string or list of strings.
216 |
217 | The `re.sub`__ is used to do the substitution thus all features of
218 | ``re.Pattern`` can be used (named groups, back referencing...).
219 |
220 | In the fmf file it is better to use single quotes ``'`` as they do
221 | not need such intensive escaping::
222 |
223 | require~: ';^foo;foo-ng;'
224 | recommend~:
225 | - '/python2-/python3-/'
226 |
227 | __ https://docs.python.org/3/library/re.html#re.sub
228 |
229 | Remove parent value only if it matches regular expression is done
230 | using the ``-~`` suffix. If value matches any of provided
231 | `regular expressions`__ it is removed. If the parent value is a
232 | list, the matching item is removed from this list. If the parent
233 | value is a string, the value is set to an empty string. If the
234 | parent value is a dictionary, the matching key is removed. These
235 | regular expressions can be just a single item or a list of
236 | strings::
237 |
238 | description-~: '.*'
239 | require-~:
240 | - 'python2.*'
241 |
242 | __ https://docs.python.org/3/library/re.html#regular-expression-syntax
243 |
244 | .. note::
245 |
246 | When multiple merge operations are performed on a single key,
247 | they are applied in the order in which they are defined. For
248 | example, the following two definitions will have a different
249 | result::
250 |
251 | /remove-first:
252 | tag-: [two, three]
253 | tag+: [three, four]
254 |
255 | /append-first:
256 | tag+: [three, four]
257 | tag-: [two, three]
258 |
259 |
260 | Elasticity
261 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
262 |
263 | Use a single file or scatter metadata across the hierarchy,
264 | whatever is more desired for the project.
265 |
266 | File ``wget/main.fmf``::
267 |
268 | tester: Petr Šplíchal
269 | tags: [Tier2, TierSecurity]
270 | test: runtest.sh
271 |
272 | File ``wget/download/main.fmf``::
273 |
274 | description: Check basic download options
275 | time: 3 min
276 |
277 | File: ``wget/recursion/main.fmf``::
278 |
279 | description: Check recursive download options
280 | time: 20 min
281 |
282 | This allows reasonable structure for both small and large
283 | projects.
284 |
285 |
286 | Scatter
287 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
288 |
289 | Thanks to elasticity, metadata can be scattered across several
290 | files. For example ``wget/download`` metadata can be defined in
291 | the following three files:
292 |
293 | File ``wget/main.fmf``::
294 |
295 | /download:
296 | description: Check basic download options
297 | test: runtest.sh
298 |
299 | File ``wget/download.fmf``::
300 |
301 | description: Check basic download options
302 | test: runtest.sh
303 |
304 | File ``wget/download/main.fmf``::
305 |
306 | description: Check basic download options
307 | test: runtest.sh
308 |
309 | Parsing is done from top to bottom (in the order of examples
310 | above). Later/lower defined attributes replace values defined
311 | earlier/higher in the structure.
312 |
313 |
314 | Leaves
315 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
316 |
317 | When searching, **key content** is used to define which leaves
318 | from the metadata tree will be selected. For example, every test
319 | case to be executed must have the ``test`` attribute defined,
320 | every requirement to be considered for test coverage evaluation
321 | must have the ``requirement`` attribute defined. Otherwise object
322 | data is used for inheritance only::
323 |
324 | description: Check basic download options
325 | test: runtest.sh
326 | time: 3 min
327 |
328 | The key content attributes are not supposed to be hard-coded in
329 | the Flexible Metadata Format but freely configurable. Multiple key
330 | content attributes (e.g. script & backend) could be used as well.
331 |
332 | .. _select:
333 |
334 | Select
335 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
336 |
337 | Sometimes it is necessary to select node from the metadata tree
338 | even though it is not a leaf. For example, when virtual tests are
339 | created from a parent test but one wants to keep the parent available
340 | as a test as well. On the other hand, one might want to hide leaf node,
341 | instead of deleting it completely. To do so, one can set the directive::
342 |
343 | /:
344 | select: boolean
345 |
346 | By default all leaves have it set to ``true`` (such node is selected)
347 | and branches have set it to ``false`` (such node is not selected).
348 |
349 | .. _virtual:
350 |
351 | Virtual
352 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
353 |
354 | Using a single test code for testing multiple scenarios can be
355 | easily implemented using leaves inheriting from the same parent::
356 |
357 | description: Check basic download options
358 | test: runtest.sh
359 |
360 | /fast:
361 | description: Check basic download options (quick smoke test)
362 | environment: MODE=fast
363 | tags: [Tier1]
364 | time: 1 min
365 | /full:
366 | description: Check basic download options (full test set)
367 | environment: MODE=full
368 | tags: [Tier2]
369 | time: 3 min
370 |
371 | In this way we can efficiently create virtual test cases.
372 |
373 |
374 | Adjust
375 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
376 |
377 | It is possible to adjust attribute values based on the current
378 | :ref:`context`, for example disable test if it's not relevant for
379 | given environment::
380 |
381 | enabled: true
382 | adjust:
383 | enabled: false
384 | when: distro ~< fedora-33
385 | because: the feature was added in Fedora 33
386 |
387 | Note that this functionality reserves the following attributes for
388 | its usage:
389 |
390 | when
391 | An optional condition to be evaluated in order to decide if the
392 | metadata should be merged. If not specified the adjust rule is
393 | applied as if it was set to ``true``.
394 |
395 | continue
396 | By default, all provided rules are evaluated. When set to
397 | ``false``, the first successful rule finishes the evaluation
398 | and the rest is ignored.
399 |
400 | because
401 | An optional comment with justification of the adjustment.
402 | Should be a plain string.
403 |
404 | Name of the attribute which contains rules to be evaluated can be
405 | arbitrary. In the example the default key ``adjust`` is used.
406 |
407 |
408 | Format
409 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
410 |
411 | When investigating metadata using the ``fmf`` command line tool,
412 | object identifiers and all associated attributes are printed by
413 | default, each on a separate line. It is also possible to use the
414 | ``--format`` option together with ``--value`` options to generate
415 | custom output. Python syntax for expansion using ``{}`` is used to
416 | place values as desired. For example::
417 |
418 | fmf --format 'name: {0}, tester: {1}\n' \
419 | --value 'name' --value 'data["tester"]'
420 |
421 | Individual attribute values can be accessed through the ``data``
422 | dictionary, variable ``name`` contains the object identifier and
423 | ``root`` is assigned to directory where metadata tree is rooted.
424 |
425 | Python modules ``os`` and ``os.path`` as well as other python
426 | functions are available and can be used for processing attribute
427 | values as desired::
428 |
429 | fmf --format '{}' --value 'os.dirname(data["path"])'
430 |
--------------------------------------------------------------------------------
/docs/header.txt:
--------------------------------------------------------------------------------
1 | ==================================================================
2 | fmf
3 | ==================================================================
4 |
5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 | Flexible Metadata Format
7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 |
9 | :Manual section: 1
10 | :Manual group: User Commands
11 | :Date: January 2018
12 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 |
2 | =====================
3 | fmf
4 | =====================
5 |
6 | The ``fmf`` Python module and command line tool implement a
7 | flexible format for defining metadata in plain text files which
8 | can be stored close to the source code. Thanks to hierarchical
9 | structure with support for inheritance and elasticity it provides
10 | an efficient way to organize data into well-sized text documents.
11 |
12 |
13 | Table of Contents
14 | ==================
15 |
16 | .. toctree::
17 | :maxdepth: 1
18 |
19 | Overview
20 | Concept
21 | Features
22 | Context
23 | Examples
24 | Modules
25 | Contribute
26 | Releases
27 |
28 | Indices and Tables
29 | ==================
30 |
31 | * :ref:`genindex`
32 | * :ref:`modindex`
33 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 |
2 | ===============
3 | Modules
4 | ===============
5 |
6 | .. _sort:
7 |
8 | Sort
9 | ----
10 |
11 | By default, when exploring test metadata in the tree, child nodes
12 | are sorted alphabetically by node name. This applies to command
13 | line usage such as ``fmf ls`` or ``fmf show`` as well as for the
14 | :py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
15 | methods.
16 |
17 | If the tree content is not created from files on disk but created
18 | manually using the :py:meth:`fmf.Tree.child()` method, the child
19 | order can be preserved by providing the ``sort=False`` parameter
20 | to the :py:meth:`fmf.Tree.climb()` and :py:meth:`fmf.Tree.prune()`
21 | methods.
22 |
23 | .. versionadded:: 1.6
24 |
25 |
26 | fmf
27 | ---
28 |
29 | .. automodule:: fmf
30 | :members:
31 | :undoc-members:
32 |
33 | base
34 | ----
35 |
36 | .. automodule:: fmf.base
37 | :members:
38 | :undoc-members:
39 |
40 | utils
41 | -----
42 |
43 | .. automodule:: fmf.utils
44 | :members:
45 | :undoc-members:
46 |
47 | cli
48 | ---
49 |
50 | .. automodule:: fmf.cli
51 | :members:
52 | :undoc-members:
53 |
--------------------------------------------------------------------------------
/docs/overview.rst:
--------------------------------------------------------------------------------
1 | ../README.rst
--------------------------------------------------------------------------------
/docs/releases.rst:
--------------------------------------------------------------------------------
1 | .. _releases:
2 |
3 | ======================
4 | Releases
5 | ======================
6 |
7 |
8 | fmf-1.7.0
9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10 |
11 | :ref:`context` dimension names can now contain dash characters as
12 | well so that multi-word names such as ``deployment-mode`` can be
13 | used.
14 |
15 | The merge suffix, for example ``+`` in the ``environment+`` key,
16 | is now correctly stripped from the nodes defined in the root of
17 | the tree as well.
18 |
19 | A new callback ``additional_rules_callback`` can now be provided
20 | to the :py:meth:`fmf.Tree.adjust` method and affect which nodes
21 | should be adjusted using the ``additional_rules``.
22 |
23 |
24 | fmf-1.6.1
25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 |
27 | In order to allow adjust rules to merge dictionaries, nested
28 | dictionaries need to keep the suffix in their name. This behavior
29 | reverts to fmf-1.4.0 and is now documented in :ref:`merging`
30 | section.
31 |
32 |
33 | fmf-1.6.0
34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35 |
36 | In order to search :ref:`context` dimension values using regular
37 | expressions, it is now possible to use operator ``~`` for matching
38 | patterns and operator ``!~`` for non matching patterns.
39 |
40 | When exploring trees using the :py:meth:`fmf.Tree.climb()` and
41 | :py:meth:`fmf.Tree.prune()` methods, optional parameter ``sort``
42 | can be used to preserve the original order in which child nodes
43 | where inserted into the tree. See the :ref:`sort` section for more
44 | details.
45 |
46 |
47 | fmf-1.5.0
48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
49 |
50 | The fmf :ref:`trees` can now be built from hidden files and
51 | directories as well. Use a simple :ref:`config` file to specify
52 | names which should be included in the search.
53 |
54 | The ``+`` operator now can be used for merging ``list`` of
55 | dictionaries with a single ``dict``. This can be for example
56 | useful when extending the ``discover`` step config which defines
57 | several phases:
58 |
59 | .. code-block:: yaml
60 |
61 | discover:
62 | - how: fmf
63 | url: https://github.com/project/one
64 | - how: fmf
65 | url: https://github.com/project/two
66 |
67 | /tier1:
68 | summary: Run tier one tests
69 | discover+:
70 | filter: "tier:1"
71 |
72 | /tier2:
73 | summary: Run tier two tests
74 | discover+:
75 | filter: "tier:2"
76 |
77 | See the :ref:`merging` section for more details and
78 | examples.
79 |
80 | The ``-`` operator no longer raises exception when the key is not
81 | defined by the parent node. This allows reducing values even for
82 | cases where user does not have write permissions for the parent
83 | data. For example, in order to make sure that the ``mysql``
84 | package is not included in the list of required or recommended
85 | packages, you can now safely use this:
86 |
87 | .. code-block:: yaml
88 |
89 | discover:
90 | how: fmf
91 | adjust-tests:
92 | - require-: [mysql]
93 | - recommend-: [mysql]
94 |
95 | When merging inherited values from parent, merge operations are
96 | now performed in the exact order in which user specified them, the
97 | keys are no longer sorted before the merging step.
98 |
99 |
100 | fmf-1.4.0
101 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
102 |
103 | New :ref:`merging` suffixes ``~`` and ``-~`` can be used
104 | to **modify or remove data based on regular expressions**. For
105 | example, renaming all required packages can be done easily in this
106 | way::
107 |
108 | require~: /python2-/python3-/
109 |
110 | The :py:func:`fmf.filter()` function now supports **searching by
111 | node name**. Just specify the desired name instead of the ``key:
112 | value`` pair. For example, to search for all tests with the name
113 | starting with ``/test/core`` and tag ``quick`` you can do::
114 |
115 | /tests/core/.* & tag: quick
116 |
117 | It is now possible to **escape boolean operators** ``|`` and ``&``
118 | as well. This allows to use more complex regular expressions like
119 | this::
120 |
121 | tag: Tier(1\|2\|3)
122 |
123 | The new :ref:`select