Comprehensive static code analysis tool for Python that performs deep code inspection without executing the program
—
Testing framework for developing and validating custom checkers, including test case base classes, message validation, and functional testing support. Pylint's test utilities enable comprehensive testing of static analysis functionality and custom checker development.
Foundation classes for testing checkers and pylint functionality.
class CheckerTestCase:
"""
Base class for checker unit tests.
Provides utilities for testing individual checkers
with AST nodes and message validation.
"""
CHECKER_CLASS = None # Set to checker class being tested
def setup_method(self):
"""Setup test environment before each test method."""
def walk(self, node):
"""
Walk AST node with the checker.
Args:
node: AST node to walk
"""
def assertNoMessages(self):
"""Assert that no messages were generated."""
def assertAddsMessages(self, *messages):
"""
Assert that specific messages were added.
Args:
*messages: Expected MessageTest instances
"""
class MessageTest:
"""
Test representation of an expected message.
Used to verify that checkers generate expected
messages for specific code patterns.
"""
def __init__(self, msg_id, node=None, line=None, args=None,
confidence=None, col_offset=None, end_line=None,
end_col_offset=None):
"""
Initialize message test.
Args:
msg_id (str): Expected message ID
node: Expected AST node (optional)
line (int): Expected line number
args (tuple): Expected message arguments
confidence (str): Expected confidence level
col_offset (int): Expected column offset
end_line (int): Expected end line
end_col_offset (int): Expected end column
"""Classes and utilities for functional testing of pylint behavior.
class FunctionalTestFile:
"""
Functional test file representation.
Represents a Python file used for functional testing
with expected messages and configuration.
"""
def __init__(self, directory, filename):
"""
Initialize functional test file.
Args:
directory (str): Directory containing test file
filename (str): Test file name
"""
@property
def expected_messages(self):
"""
Get expected messages from test file.
Returns:
list: Expected message objects
"""
@property
def pylintrc(self):
"""
Get pylintrc path for test.
Returns:
str: Path to test-specific pylintrc
"""
class LintModuleTest:
"""
Module linting test utilities.
Provides utilities for testing pylint behavior
on complete modules and packages.
"""
def __init__(self, test_file):
"""
Initialize module test.
Args:
test_file: FunctionalTestFile instance
"""
def runTest(self):
"""Run the functional test."""
def _check_result(self, got_messages, expected_messages):
"""
Check test results against expectations.
Args:
got_messages (list): Actual messages from pylint
expected_messages (list): Expected messages
"""Specialized linter implementation for testing purposes.
class UnittestLinter:
"""
Test-specific linter implementation.
Simplified linter for unit testing that provides
controlled environment and message collection.
"""
def __init__(self):
"""Initialize unittest linter."""
self.config = None
self.reporter = None
def check(self, files_or_modules):
"""
Check files for testing.
Args:
files_or_modules: Files or modules to check
"""
def add_message(self, msg_id, line=None, node=None, args=None,
confidence=None, col_offset=None):
"""
Add message during testing.
Args:
msg_id (str): Message identifier
line (int): Line number
node: AST node
args (tuple): Message arguments
confidence (str): Confidence level
col_offset (int): Column offset
"""Functions for configuring tests and test environments.
def set_config(**kwargs):
"""
Set configuration for tests.
Provides a convenient way to set pylint configuration
options during test execution.
Args:
**kwargs: Configuration options to set
Example:
set_config(max_line_length=100, disable=['missing-docstring'])
"""
def tokenize_str(code):
"""
Tokenize Python code string.
Utility function for testing token-based checkers
by converting code strings to token streams.
Args:
code (str): Python code to tokenize
Returns:
list: Token objects
"""Specialized reporters for testing and validation.
class GenericTestReporter:
"""
Generic test reporter.
Collects messages during testing for validation
and provides access to test results.
"""
def __init__(self):
"""Initialize generic test reporter."""
self.messages = []
def handle_message(self, msg):
"""
Handle message during testing.
Args:
msg: Message to collect
"""
self.messages.append(msg)
def finalize(self):
"""
Finalize testing and return results.
Returns:
list: Collected messages
"""
return self.messages
class MinimalTestReporter:
"""
Minimal test reporter.
Provides minimal message collection for
lightweight testing scenarios.
"""
def __init__(self):
"""Initialize minimal reporter."""
self.messages = []
class FunctionalTestReporter:
"""
Functional test reporter.
Specialized reporter for functional testing
with enhanced message formatting and comparison.
"""
def __init__(self):
"""Initialize functional test reporter."""
self.messages = []
def display_reports(self, layout):
"""Display functional test reports."""
passimport astroid
from pylint.testutils import CheckerTestCase, MessageTest
from my_checkers import FunctionNamingChecker
class TestFunctionNamingChecker(CheckerTestCase):
"""Test cases for function naming checker."""
CHECKER_CLASS = FunctionNamingChecker
def test_function_with_good_name(self):
"""Test that functions with good names pass."""
node = astroid.extract_node('''
def get_user_name(): #@
return "test"
''')
with self.assertNoMessages():
self.walk(node)
def test_function_with_bad_name(self):
"""Test that functions with bad names fail."""
node = astroid.extract_node('''
def userName(): #@
return "test"
''')
expected_message = MessageTest(
msg_id='invalid-function-name',
node=node,
args=('userName',),
line=2,
col_offset=0
)
with self.assertAddsMessages(expected_message):
self.walk(node)
def test_function_with_config(self):
"""Test checker with custom configuration."""
with self.assertNoMessages():
# Set custom configuration
self.checker.config.allowed_function_prefixes = ['create', 'build']
node = astroid.extract_node('''
def create_object(): #@
pass
''')
self.walk(node)from pylint.testutils import FunctionalTestFile, LintModuleTest
import os
class TestMyCheckersFunctional:
"""Functional tests for custom checkers."""
def test_complex_scenarios(self):
"""Test complex scenarios with functional tests."""
test_dir = 'tests/functional'
test_file = 'my_checker_test.py'
functional_test = FunctionalTestFile(test_dir, test_file)
test_case = LintModuleTest(functional_test)
test_case.runTest()
def create_functional_test_file(self):
"""Create a functional test file."""
test_content = '''
"""Test module for custom checker."""
def bad_function_name(): # [invalid-function-name]
"""This should trigger a naming violation."""
pass
def get_value(): # No violation expected
"""This should pass naming validation."""
return 42
class BadClassName: # [invalid-class-name]
"""This should trigger a class naming violation."""
pass
'''
with open('tests/functional/my_checker_test.py', 'w') as f:
f.write(test_content)from pylint.testutils import UnittestLinter, GenericTestReporter
class CustomTestFramework:
"""Custom testing framework for specific needs."""
def __init__(self):
self.linter = UnittestLinter()
self.reporter = GenericTestReporter()
self.linter.set_reporter(self.reporter)
def test_code_snippet(self, code, expected_messages=None):
"""
Test a code snippet and validate results.
Args:
code (str): Python code to test
expected_messages (list): Expected message IDs
"""
# Create temporary file
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.py',
delete=False) as f:
f.write(code)
temp_file = f.name
try:
# Run pylint on the code
self.linter.check([temp_file])
messages = self.reporter.finalize()
# Validate results
if expected_messages:
actual_msg_ids = [msg.msg_id for msg in messages]
assert set(actual_msg_ids) == set(expected_messages), \
f"Expected {expected_messages}, got {actual_msg_ids}"
return messages
finally:
# Clean up
import os
os.unlink(temp_file)
def assert_no_violations(self, code):
"""Assert that code has no violations."""
messages = self.test_code_snippet(code)
assert len(messages) == 0, f"Unexpected violations: {messages}"
def assert_violations(self, code, expected_msg_ids):
"""Assert that code has specific violations."""
self.test_code_snippet(code, expected_msg_ids)
# Usage example
framework = CustomTestFramework()
# Test clean code
framework.assert_no_violations('''
def get_user_data():
"""Get user data from database."""
return {"name": "test"}
''')
# Test code with violations
framework.assert_violations('''
def userData(): # Missing docstring, bad naming
return {"name": "test"}
''', ['missing-docstring', 'invalid-name'])from unittest.mock import patch, MagicMock
from pylint.testutils import CheckerTestCase
class TestCheckerWithMocks(CheckerTestCase):
"""Test checker using mocks for external dependencies."""
CHECKER_CLASS = MyCustomChecker
@patch('mypackage.external_service.check_api')
def test_checker_with_external_service(self, mock_api):
"""Test checker that depends on external service."""
# Setup mock
mock_api.return_value = True
node = astroid.extract_node('''
def process_data(): #@
# This would normally call external service
pass
''')
with self.assertNoMessages():
self.walk(node)
# Verify mock was called
mock_api.assert_called_once()
def test_checker_with_mock_config(self):
"""Test checker with mocked configuration."""
# Mock configuration
mock_config = MagicMock()
mock_config.strict_mode = True
mock_config.threshold = 10
self.checker.config = mock_config
node = astroid.extract_node('''
def test_function(): #@
pass
''')
# Test behavior changes based on config
with self.assertAddsMessages(
MessageTest('strict-mode-violation', node=node)
):
self.walk(node)import time
from pylint.testutils import CheckerTestCase
class TestCheckerPerformance(CheckerTestCase):
"""Performance tests for checker efficiency."""
CHECKER_CLASS = MyPerformanceChecker
def test_checker_performance(self):
"""Test checker performance on large code."""
# Generate large test case
large_code = '''
def large_function():
"""Large function for performance testing."""
''' + '\n'.join([f' var_{i} = {i}' for i in range(1000)])
node = astroid.parse(large_code)
# Measure execution time
start_time = time.time()
self.walk(node)
end_time = time.time()
execution_time = end_time - start_time
# Assert reasonable performance
assert execution_time < 1.0, \
f"Checker too slow: {execution_time:.2f}s"
# Verify functionality still works
self.assertNoMessages() # or expected messages
def benchmark_checker(self, iterations=100):
"""Benchmark checker performance."""
node = astroid.extract_node('''
def benchmark_function(): #@
return "test"
''')
total_time = 0
for _ in range(iterations):
start = time.time()
self.walk(node)
total_time += time.time() - start
avg_time = total_time / iterations
print(f"Average execution time: {avg_time*1000:.2f}ms")
return avg_time# Configure test environment
from pylint.testutils import set_config
def setup_test_config():
"""Setup common test configuration."""
set_config(
disable=['missing-docstring'], # Disable for test simplicity
max_line_length=120, # Longer lines in tests
good_names=['i', 'j', 'k', 'x', 'y', 'z', 'test_var'],
reports=False, # Disable reports in tests
score=False # Disable scoring in tests
)
# Test-specific pylintrc
TEST_PYLINTRC = '''
[MAIN]
load-plugins=my_test_plugin
[MESSAGES CONTROL]
disable=missing-docstring,invalid-name
[BASIC]
good-names=i,j,k,x,y,z,test_var,mock_obj
[FORMAT]
max-line-length=120
[REPORTS]
reports=no
score=no
'''def run_test_suite():
"""Run complete test suite for CI/CD."""
import unittest
import sys
# Discover and run all tests
loader = unittest.TestLoader()
suite = loader.discover('tests/', pattern='test_*.py')
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Exit with error code if tests failed
if not result.wasSuccessful():
sys.exit(1)
print(f"All tests passed: {result.testsRun} tests")
# GitHub Actions example
'''
name: Test Custom Checkers
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', 3.11]
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install pylint pytest
pip install -e .
- name: Run tests
run: python -m pytest tests/
- name: Run functional tests
run: python run_test_suite.py
'''Install with Tessl CLI
npx tessl i tessl/pypi-pylint