Skip to content

Instantly share code, notes, and snippets.

@szobov
Last active February 9, 2023 13:08
Show Gist options
  • Save szobov/e5ee966e3bdae711c27b304d688aa05e to your computer and use it in GitHub Desktop.
Save szobov/e5ee966e3bdae711c27b304d688aa05e to your computer and use it in GitHub Desktop.
Add a fixture parameters to pytest's test function definitions
"""
Copyright © 2023 Sergei Zobov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import argparse
import logging
import os
import pathlib
import shutil
import subprocess
import typing as _t
logger = logging.getLogger(__name__)
PACKAGE_LINE_PREFIX = "<Package '"
MODULE_LINE_PREFIX = "<Module '"
FUNCTION_LINE_PREFIX = "<Function '"
LINE_TERMINATOR = "'>"
TEMPORTAL_TEST_FILE_PREFIX = "test_temporal_fixed_auto_test_"
def extract_test_info(input_data: str) -> _t.Generator[_t.Tuple[str, str, str], None, None]:
"""
Process the format of the output from `pytest --collect-only`.
The example of the output:
```
collected 42 items / 1 skipped
<Module 'tests/test_file.py'>
<Function 'test_ok'>
<Function 'test_error'>
...
```
DISCLAIMER: it improperly works with test classes since it doesn't parse it.
"""
current_package = ""
current_module: _t.Optional[str] = None
current_function: _t.Optional[str] = None
for line in map(str.strip, input_data.split("\n")):
if line.startswith(PACKAGE_LINE_PREFIX):
current_package = line[len(PACKAGE_LINE_PREFIX) : -len(LINE_TERMINATOR)]
current_module = None
if line.startswith(MODULE_LINE_PREFIX):
current_module = line[len(MODULE_LINE_PREFIX) : -len(LINE_TERMINATOR)]
package_module = pathlib.Path(current_package) / pathlib.Path(current_module)
if package_module.exists():
continue
if pathlib.Path(current_module).exists():
current_package = ""
if line.startswith(FUNCTION_LINE_PREFIX):
current_function = line[len(FUNCTION_LINE_PREFIX) : -len(LINE_TERMINATOR)]
if current_function.endswith("]"):
current_function = current_function.split("[")[0]
assert current_module
assert current_function
yield current_package, current_module, current_function
def create_fixed_file(
*,
package_module: pathlib.Path,
function: str,
fixture_parameters: _t.Tuple[str, ...],
dry_run: bool = True,
suppress_output: bool = False,
):
"""
Process a test file in a following algorithm:
1. Read an original test file by lines
2. Find a line with function name in it
3. If line containes `self` keyword igonres it
4. Parse a line and put a string with fixtures paramers between
a fuction definition and the first parameter
5. Creates a temporal test file with fixed line
6. Run fixed the test file
7. If test passes, replace the original file with the fixed copy
8. If any error occured, remove a copy and exit from a this function
"""
file_content_with_fixed_line: _t.List[str] = []
for line in package_module.read_text().split("\n"):
function_def = f"def {function}("
if function_def not in line or "self" in line:
file_content_with_fixed_line.append(line)
continue
splitted_line = line.split(function_def)
assert len(splitted_line) == 2, splitted_line
new_line = (
f"{splitted_line[0]}{function_def}{', '.join(fixture_parameters)}, {splitted_line[1]}"
)
file_content_with_fixed_line.append(new_line)
fixed_test_content = "\n".join(file_content_with_fixed_line)
fixed_test_file = package_module.parent / f"{TEMPORTAL_TEST_FILE_PREFIX}{package_module.name}"
quite_mode = []
if suppress_output:
quite_mode = ["-qq"]
try:
fixed_test_file.write_text(fixed_test_content)
result = subprocess.run(
["pytest", f"{str(fixed_test_file)}::{function}"] + quite_mode,
)
assert result.returncode == 0
logger.info(
msg={"comment": "Test was fixed", "test_file": package_module, "function": function}
)
except:
logger.exception(
msg={"comment": "Got an error on execution test", "test_file": fixed_test_file}
)
os.remove(fixed_test_file)
return
logger.info(
msg={"comment": "Replacing test files", "test_file": package_module, "dry_run": dry_run}
)
if dry_run:
if fixed_test_file.exists:
os.remove(fixed_test_file)
else:
shutil.move(str(fixed_test_file), str(package_module))
def main(
fixture_parameters: _t.Tuple[str, ...],
dry_run: bool,
suppress_output: bool,
fix_only_test_substring: _t.Optional[str],
):
logger.info(
msg={
"comment": "Start fixing tests function definitions",
"fixture_parameters": fixture_parameters,
"dry_run": dry_run,
"suppress_output": suppress_output,
"fix_only_test_substring": fix_only_test_substring,
}
)
input_from_pytest_collect: str = subprocess.check_output(["pytest", "--collect-only"]).decode()
processed_tests: _t.Set[_t.Tuple[str, str, str]] = set()
for (package, module, function) in extract_test_info(input_from_pytest_collect):
if fix_only_test_substring is not None:
if fix_only_test_substring not in module:
continue
test_key = tuple([package, module, function])
if test_key in processed_tests:
continue
processed_tests.add(test_key)
package_module = pathlib.Path(package) / pathlib.Path(module)
quite_mode = []
if suppress_output:
quite_mode = ["-qq"]
result = subprocess.run(["pytest", f"{str(package_module)}::{function}"] + quite_mode)
if result.returncode in (0, 4):
continue
create_fixed_file(
package_module=package_module,
function=function,
fixture_parameters=fixture_parameters,
dry_run=dry_run,
suppress_output=suppress_output,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser("""
Add a fixture parameters to pytest's test function definitions.
E.g. you have:
```
def test_ok(bar, baz):
assert True
```
and you want to add a new fixture `fizz` to all the test function that are
requires this fixture.
Then you can run this script in the way:
```
$ python add_fixture_to_tests.py -f fizz
```
And it will replace a test file with a new one, if it was failing before but
was fixed after adding a new fixture.
```
def test_ok(fiz, bar, baz):
assert True
```
Please check `--help` to learn more about available parameters.
Intended to be used together with automatical code formating tool.
""")
parser.add_argument(
"-f",
"--fixture-parameters",
type=str,
help="List of the names of the fixtures that will be added to the tests. E.g. "
"--fixture-parameters=fixture_name1,fixture_name2",
required=True,
)
parser.add_argument(
"-dr",
"--dry-run",
action="store_true",
help="Do not replace an original test file.",
required=False,
default=False,
)
parser.add_argument(
"-q",
"--suppress-output",
action="store_true",
help="Pass -qq argument to pytest",
required=False,
default=False,
)
parser.add_argument(
"-s",
"--test-name-substring",
type=str,
help="Ingnore all test with expect containing this substring.",
required=False,
default=None,
)
return parser.parse_args()
if __name__ == "__main__":
logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)
args = parse_args()
test_name_substring = None
if not args.test_name_substring:
test_name_substring = None
fixture_parameters = tuple(args.fixture_parameters.split(","))
assert all(map(lambda p: isinstance(p, str), fixture_parameters))
assert len(fixture_parameters) != 0
main(
fixture_parameters=fixture_parameters,
dry_run=args.dry_run,
suppress_output=args.suppress_output,
fix_only_test_substring=args.test_name_substring,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment