Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CI test for message documentation #5956

Merged
merged 2 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,37 @@ jobs:
. venv/bin/activate
pytest tests/ -k unittest_spelling

documentation:
name: checks / documentation
runs-on: ubuntu-latest
timeout-minutes: 5
needs: prepare-base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v3.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.0.0
with:
path: venv
key:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.prepare-base.outputs.python-key }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
echo "Failed to restore Python venv from cache"
exit 1
- name: Run checks on documentation code examples
run: |
. venv/bin/activate
pytest doc/test_messages_documentation.py

prepare-tests-linux:
name: tests / prepare / ${{ matrix.python-version }} / Linux
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions doc/data/messages/e/empty-docstring/bad.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
def foo():
pass # [emtpy-docstring]
def foo(): # [empty-docstring]
DanielNoord marked this conversation as resolved.
Show resolved Hide resolved
""""""
110 changes: 110 additions & 0 deletions doc/test_messages_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Functional tests for the code examples in the messages documentation."""

from collections import Counter
from pathlib import Path
from typing import Counter as CounterType
from typing import List, TextIO, Tuple

import pytest

from pylint import checkers
from pylint.config.config_initialization import _config_initialization
from pylint.lint import PyLinter
from pylint.message.message import Message
from pylint.testutils.constants import _EXPECTED_RE
from pylint.testutils.reporter_for_tests import FunctionalTestReporter

MessageCounter = CounterType[Tuple[int, str]]


def get_functional_test_files_from_directory(input_dir: Path) -> List[Tuple[str, Path]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use https://github.com/PyCQA/pylint/blob/main/pylint/testutils/functional/find_functional_tests.py#L31, right ? ( yield from get_functional_test_files_from_directory if the file is good.py or bad.py ? Maybe we don't even need to check that it's good.py or bad.py)

Copy link
Collaborator Author

@DanielNoord DanielNoord Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that yields FunctionalTestFile:
https://github.com/PyCQA/pylint/blob/80838744c6f739fbf2f49bd1bfb70c98a84531aa/pylint/testutils/functional/test_file.py#L45

That class has a lot of additional methods that do not work with the folder structure of the messages documentation. Stuff like self._parse_options().
For the purpose of these tests we only need a tuple of test name and path so I don't think it makes much sense to try and change FunctionalTestFile to also allow a completely different directory structure.

"""Get all functional tests in the input_dir."""
suite: List[Tuple[str, Path]] = []

for subdirectory in input_dir.iterdir():
for message_dir in subdirectory.iterdir():
if (message_dir / "good.py").exists():
suite.append(
(message_dir.stem, message_dir / "good.py"),
)
if (message_dir / "bad.py").exists():
suite.append(
(message_dir.stem, message_dir / "bad.py"),
)
return suite


TESTS_DIR = Path(__file__).parent.resolve() / "data" / "messages"
TESTS = get_functional_test_files_from_directory(TESTS_DIR)
TESTS_NAMES = [f"{t[0]}-{t[1].stem}" for t in TESTS]


class LintModuleTest:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, as that automatically disables some messages (that we might want to test) and can only accept FunctionalTestFile as test_file while we (I think) only want to pass a simple tuple.

def __init__(self, test_file: Tuple[str, Path]) -> None:
self._test_file = test_file

_test_reporter = FunctionalTestReporter()

self._linter = PyLinter()
self._linter.config.persistent = 0
checkers.initialize(self._linter)

_config_initialization(
self._linter,
args_list=[
str(test_file[1]),
"--disable=all",
f"--enable={test_file[0]}",
],
reporter=_test_reporter,
)

def runTest(self) -> None:
self._runTest()

@staticmethod
def get_expected_messages(stream: TextIO) -> MessageCounter:
"""Parse a file and get expected messages."""
messages: MessageCounter = Counter()
for i, line in enumerate(stream):
match = _EXPECTED_RE.search(line)
if match is None:
continue

line = match.group("line")
if line is None:
lineno = i + 1
else:
lineno = int(line)

for msg_id in match.group("msgs").split(","):
messages[lineno, msg_id.strip()] += 1
return messages

def _get_expected(self) -> MessageCounter:
"""Get the expected messages for a file."""
with open(self._test_file[1], encoding="utf8") as f:
expected_msgs = self.get_expected_messages(f)
return expected_msgs

def _get_actual(self) -> MessageCounter:
"""Get the actual messages after a run."""
messages: List[Message] = self._linter.reporter.messages
messages.sort(key=lambda m: (m.line, m.symbol, m.msg))
received_msgs: MessageCounter = Counter()
for msg in messages:
received_msgs[msg.line, msg.symbol] += 1
return received_msgs

def _runTest(self) -> None:
"""Run the test and assert message differences."""
self._linter.check([str(self._test_file[1])])
expected_messages = self._get_expected()
actual_messages = self._get_actual()
assert expected_messages == actual_messages


@pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES)
def test_code_examples(test_file: Tuple[str, Path]) -> None:
lint_test = LintModuleTest(test_file)
lint_test.runTest()