Skip to content

Instantly share code, notes, and snippets.

@qstokkink
Last active January 11, 2024 07:23
Show Gist options
  • Save qstokkink/3da0625628a66337eece8a82fb85f5fa to your computer and use it in GitHub Desktop.
Save qstokkink/3da0625628a66337eece8a82fb85f5fa to your computer and use it in GitHub Desktop.
How pytest fixtures + redefined-outer-name (W0621) can be dangerous
"""
In this example we have the `important_protection` fixture,
that SHOULD be called. It triggers "redefined-outer-name (W0621)" and
"Redefining name '...' from outer scope".
This example shows that two types of errors could have been prevented
by following the suggested Pylint definition, instead of ignoring the
warning.
## The suggested definition
Ignoring redefined-outer-name (W0621), we define the fixture as:
@pytest.fixture
def important_protection() -> Generator[int, None, None]:
The suggested definition according to https://pylint.readthedocs.io/en/
latest/user_guide/messages/warning/redefined-outer-name.html is:
@pytest.fixture(name="important_protection")
def fixture_important_protection() -> Generator[int, None, None]:
## How this example fails to detect errors
When running the tests without the suggested definition,
the degraded tests (case 2a/2b) pass unexpectedly (wrong).
When running the tests with the suggested definition,
the degraded tests (case 2a/2b) fail as expected (correct).
"""
import os
from typing import Generator
import pytest
FAILURE_CODES = [1, 139]
@pytest.fixture
def important_protection() -> Generator[int, None, None]:
"""
Overwrite private key info and return an exit code to signal success.
"""
original_key = os.environ["PRIVATE_KEY"]
try:
os.environ["PRIVATE_KEY"] = "" # Remove the private key!
error_code = 0
except Exception:
error_code = 1
yield error_code
os.environ["PRIVATE_KEY"] = original_key
@pytest.fixture
def environment() -> None:
"""
Set the private key in our environment. Don't leak this!
"""
os.environ["PRIVATE_KEY"] = "pwnd"
def test_case_1(environment, important_protection) -> None:
"""
Case 1: In this first test everything is fine and works as expected.
"""
fixture_exit_code = important_protection
assert fixture_exit_code not in FAILURE_CODES
print(os.environ["PRIVATE_KEY"]) # Cleared, as expected
# Some time may pass until the above case 1 degrades into, or spawns,
# a test of type case 2. For example, after sloppy refactoring.
def test_case_2(environment) -> None:
"""
Case 2a: The programmer copy-pasted the safety check from case 1
BUT forgot to add important_protection as a parameter.
Case 2b: Yet another programmer accidentally removed a parameter.
"""
fixture_exit_code = important_protection
assert fixture_exit_code not in FAILURE_CODES
print(os.environ["PRIVATE_KEY"]) # Uh, oh! Pwnd!
# The above is an example of an unnecessary and easily preventable
# risk to your code base. It could have been avoided by simply
# following the suggested definition of fixtures using a name.
# However, the consequences of may not always be as severe as leaking
# private keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment