-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Primer tests "à la mypy" #5173
Primer tests "à la mypy" #5173
Changes from 9 commits
6e44feb
c136c5e
63de47b
aacb375
ad611a5
58bc7d2
490771e
923d070
a195860
6f60b7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,3 +20,4 @@ build-stamp | |
.pytest_cache/ | ||
.mypy_cache/ | ||
.benchmarks/ | ||
.pylint_primer_tests/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import logging | ||
from pathlib import Path | ||
from typing import Dict, List, Optional, Union | ||
|
||
import git | ||
|
||
PRIMER_DIRECTORY_PATH = Path(".pylint_primer_tests") | ||
|
||
|
||
class PackageToLint: | ||
"""Represents data about a package to be tested during primer tests""" | ||
|
||
url: str | ||
"""URL of the repository to clone""" | ||
|
||
branch: str | ||
"""Branch of the repository to clone""" | ||
|
||
directories: List[str] | ||
"""Directories within the repository to run pylint over""" | ||
|
||
commit: Optional[str] | ||
Pierre-Sassoulas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Commit hash to pin the repository on""" | ||
|
||
pylint_additional_args: List[str] | ||
"""Arguments to give to pylint""" | ||
|
||
pylintrc_relpath: Optional[str] | ||
"""Path relative to project's main directory to the pylintrc if it exists""" | ||
|
||
def __init__( | ||
self, | ||
url: str, | ||
branch: str, | ||
directories: List[str], | ||
commit: Optional[str] = None, | ||
pylint_additional_args: Optional[List[str]] = None, | ||
pylintrc_relpath: Optional[str] = None, | ||
) -> None: | ||
self.url = url | ||
self.branch = branch | ||
self.directories = directories | ||
self.commit = commit | ||
self.pylint_additional_args = pylint_additional_args or [] | ||
self.pylintrc_relpath = pylintrc_relpath | ||
|
||
@property | ||
def pylintrc(self) -> Optional[Path]: | ||
if self.pylintrc_relpath is None: | ||
return None | ||
return self.clone_directory / self.pylintrc_relpath | ||
|
||
@property | ||
def clone_directory(self) -> Path: | ||
"""Directory to clone repository into""" | ||
clone_name = "/".join(self.url.split("/")[-2:]).replace(".git", "") | ||
return PRIMER_DIRECTORY_PATH / clone_name | ||
|
||
@property | ||
def paths_to_lint(self) -> List[str]: | ||
"""The paths we need to lint""" | ||
return [str(self.clone_directory / path) for path in self.directories] | ||
|
||
@property | ||
def pylint_args(self) -> List[str]: | ||
options: List[str] = [] | ||
if self.pylintrc is not None: | ||
# There is an error if rcfile is given but does not exists | ||
options += [f"--rcfile={self.pylintrc}"] | ||
return self.paths_to_lint + options + self.pylint_additional_args | ||
|
||
def lazy_clone(self) -> None: # pragma: no cover | ||
"""Concatenates the target directory and clones the file | ||
|
||
Not expected to be tested as the primer won't work if it doesn't. | ||
It's tested in the continuous integration primers, only the coverage | ||
is not calculated on everything. If lazy clone breaks for local use | ||
we'll probably notice because we'll have a fatal when launching the | ||
primer locally. | ||
""" | ||
logging.info("Lazy cloning %s", self.url) | ||
if not self.clone_directory.exists(): | ||
options: Dict[str, Union[str, int]] = { | ||
"url": self.url, | ||
"to_path": str(self.clone_directory), | ||
"branch": self.branch, | ||
"depth": 1, | ||
} | ||
logging.info("Directory does not exists, cloning: %s", options) | ||
git.Repo.clone_from(**options) | ||
return | ||
|
||
remote_sha1_commit = ( | ||
git.cmd.Git().ls_remote(self.url, self.branch).split("\t")[0] | ||
) | ||
local_sha1_commit = git.Repo(self.clone_directory).head.object.hexsha | ||
if remote_sha1_commit != local_sha1_commit: | ||
logging.info( | ||
"Remote sha is '%s' while local sha is '%s': pulling new commits", | ||
remote_sha1_commit, | ||
local_sha1_commit, | ||
) | ||
repo = git.Repo(self.clone_directory) | ||
origin = repo.remotes.origin | ||
origin.pull() | ||
else: | ||
logging.info("Repository already up to date.") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ | |
astroid==2.9.0 # Pinned to a specific version for tests | ||
pytest~=6.2 | ||
pytest-benchmark~=3.4 | ||
gitpython>3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
{ | ||
"flask": { | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense. Especially since astroid is very pylint ready so we could check for false positive right away. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes more sense in #5364 |
||
"url": "https://github.com/pallets/flask.git", | ||
"branch": "main", | ||
"directories": ["src/flask"] | ||
}, | ||
"pytest": { | ||
"url": "https://github.com/pytest-dev/pytest.git", | ||
"branch": "main", | ||
"directories": ["src/_pytest"] | ||
}, | ||
"psycopg": { | ||
"url": "https://github.com/psycopg/psycopg.git", | ||
"branch": "master", | ||
"directories": ["psycopg/psycopg"] | ||
}, | ||
"keras": { | ||
"url": "https://github.com/keras-team/keras.git", | ||
"branch": "master", | ||
"directories": ["keras"] | ||
}, | ||
"sentry": { | ||
"url": "https://github.com/getsentry/sentry.git", | ||
"branch": "master", | ||
"directories": ["src/sentry"] | ||
}, | ||
"django": { | ||
"url": "https://github.com/django/django.git", | ||
"branch": "main", | ||
"directories": ["django"] | ||
}, | ||
"pandas": { | ||
"url": "https://github.com/pandas-dev/pandas.git", | ||
"branch": "master", | ||
"directories": ["pandas"], | ||
"pylint_additional_args": ["--ignore-patterns=\"test_"] | ||
}, | ||
"black": { | ||
"url": "https://github.com/psf/black.git", | ||
"branch": "main", | ||
"directories": ["src/black/", "src/blackd/", "src/black_primer/", "src/blib2to3/"] | ||
}, | ||
"home-assistant": { | ||
"url": "https://github.com/home-assistant/core.git", | ||
"branch": "dev", | ||
"directories": ["homeassistant"] | ||
}, | ||
"graph-explorer": { | ||
"url": "https://github.com/vimeo/graph-explorer.git", | ||
"branch": "master", | ||
"directories": ["graph_explorer"], | ||
"pylintrc_relpath": ".pylintrc" | ||
}, | ||
"pygame": { | ||
"url": "https://github.com/pygame/pygame.git", | ||
"branch": "main", | ||
"directories": ["src_py"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html | ||
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE | ||
import json | ||
import logging | ||
import subprocess | ||
from pathlib import Path | ||
from typing import Dict, Union | ||
|
||
import pytest | ||
from pytest import LogCaptureFixture | ||
|
||
from pylint.testutils.primer import PackageToLint | ||
|
||
PRIMER_DIRECTORY = Path(".pylint_primer_tests/").resolve() | ||
|
||
|
||
def get_packages_to_lint_from_json( | ||
json_path: Union[Path, str] | ||
) -> Dict[str, PackageToLint]: | ||
result: Dict[str, PackageToLint] = {} | ||
with open(json_path, encoding="utf8") as f: | ||
for name, package_data in json.load(f).items(): | ||
result[name] = PackageToLint(**package_data) | ||
return result | ||
|
||
|
||
PACKAGE_TO_LINT_JSON = Path(__file__).parent / "packages_to_lint.json" | ||
PACKAGES_TO_LINT = get_packages_to_lint_from_json(PACKAGE_TO_LINT_JSON) | ||
"""Dictionary of external packages used during the primer test""" | ||
|
||
|
||
class TestPrimer: | ||
@staticmethod | ||
@pytest.mark.primer_external | ||
@pytest.mark.parametrize("package", PACKAGES_TO_LINT.values(), ids=PACKAGES_TO_LINT) | ||
def test_primer_external_packages_no_crash( | ||
package: PackageToLint, | ||
caplog: LogCaptureFixture, | ||
) -> None: | ||
__tracebackhide__ = True # pylint: disable=unused-variable | ||
TestPrimer._primer_test(package, caplog) | ||
|
||
@staticmethod | ||
def _primer_test(package: PackageToLint, caplog: LogCaptureFixture) -> None: | ||
"""Runs pylint over external packages to check for crashes and fatal messages | ||
|
||
We only check for crashes (bit-encoded exit code 32) and fatal messages | ||
(bit-encoded exit code 1). We assume that these external repositories do not | ||
have any fatal errors in their code so that any fatal errors are pylint false | ||
positives | ||
""" | ||
caplog.set_level(logging.INFO) | ||
package.lazy_clone() | ||
|
||
try: | ||
# We want to test all the code we can | ||
enables = ["--enable-all-extensions", "--enable=all"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if I would use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're going to have to separate "finding fatal and crashes" and finding new false positives. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
# Duplicate code takes too long and is relatively safe | ||
disables = ["--disable=duplicate-code"] | ||
Pierre-Sassoulas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
command = ["pylint"] + enables + disables + package.pylint_args | ||
logging.info("Launching primer:\n%s", " ".join(command)) | ||
subprocess.run(command, check=True) | ||
except subprocess.CalledProcessError as ex: | ||
msg = f"Encountered {{}} during primer test for {package}" | ||
assert ex.returncode != 32, msg.format("a crash") | ||
assert ex.returncode % 2 == 0, msg.format("a message of category 'fatal'") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would make sense to create a new workflow file for it. That will make it easier to distinguish for all the other tests in the Github UI.
Only small downside, you need to compy the
prepare-tests-linux
job.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#5359