├── .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