Skip to content

Commit ed7a981

Browse files
authored
update workflows and unit tests (#84)
now uses the following reusable workflows: - pre-commit.yml - py-coverage.yml ## Adjusting Unit tests - codecov-action v4 now requires a CODECOV token (which I added to repo secrets) - `test_download_file` was passing unexpectedly (should have expectedly failed for `"latest"` release_tag). - `test_path_warning` wouldn't pass when run in a venv (which is how I run tests locally). - `test_get_sha` was using outdated sha512sum for Windows. - CI now runs unit tests on all 3 OS and the reusable workflow will combine coverage data for the coverage reports. * update pre-commit hooks; switch to ruff for linting and formatting in a pre-commit hook * improve test runtime and coverage - now includes test code in coverage - does not download every supported version just to trigger lines in coverage data; only 1 version will suffice. - only download clang-format when testing `install_tool()` because it gets same coverage and downloads faster on all supported platforms.
1 parent fdb016d commit ed7a981

15 files changed

+110
-77
lines changed

.github/workflows/bump-version.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import requests
22
import sys
3-
sys.path.append('../../')
4-
from clang_tools import release_tag
3+
4+
sys.path.append("../../")
5+
from clang_tools import release_tag # noqa E402
56

67

78
def get_latest_tag() -> str:
8-
response = requests.get("https://github.com/api/repos/cpp-linter/clang-tools-static-binaries/releases/latest")
9-
return response.json()['tag_name']
9+
response = requests.get(
10+
"https://github.com/api/repos/cpp-linter/clang-tools-static-binaries/releases/latest"
11+
)
12+
return response.json()["tag_name"]
1013

1114

1215
def update_tag(current_tag, latest_tag) -> None:
@@ -16,7 +19,7 @@ def update_tag(current_tag, latest_tag) -> None:
1619

1720
updated_content = file_content.replace(current_tag, latest_tag)
1821

19-
with open(file_path, 'w') as file:
22+
with open(file_path, "w") as file:
2023
file.write(updated_content)
2124

2225

.github/workflows/python-publish.yml

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ name: Upload Python Package
1010

1111
on:
1212
release:
13-
branches: [main]
1413
types: [published]
1514
workflow_dispatch:
1615

.github/workflows/python-test.yml

+26-13
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ on:
1717

1818

1919
jobs:
20-
build:
21-
runs-on: ubuntu-latest
20+
test:
21+
strategy:
22+
matrix:
23+
os: [ ubuntu-latest, macos-latest, windows-latest ]
24+
fail-fast: false
25+
runs-on: ${{ matrix.os }}
2226
steps:
2327
- uses: actions/checkout@v4
2428

@@ -32,20 +36,29 @@ jobs:
3236
python3 -m pip install --upgrade pip
3337
python3 -m pip install . -r requirements-dev.txt
3438
35-
- name: Run pre-commit
36-
run: pre-commit run --all-files
39+
- name: Collect Coverage
40+
run: coverage run -m pytest -vv
3741

38-
- name: Collect coverage
39-
run: |
40-
coverage run -m pytest -vv
41-
coverage report -m
42-
coverage xml
42+
- name: Upload coverage data
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: coverage-data-${{ runner.os }}
46+
path: .coverage*
4347

44-
- name: Upload coverage reports to Codecov
45-
uses: codecov/codecov-action@v4
48+
coverage-report:
49+
needs: [test]
50+
uses: cpp-linter/.github/.github/workflows/py-coverage.yml@main
51+
secrets: inherit
52+
53+
build:
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- name: Set up Python 3.10
59+
uses: actions/setup-python@v5
4660
with:
47-
files: ./coverage.xml
48-
verbose: true # optional (default = false)
61+
python-version: "3.10"
4962

5063
- name: Build wheel
5164
run: python -m pip wheel -w dist .

.github/workflows/run-pre-commit.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: Run pre-commit
2+
3+
on:
4+
push:
5+
pull_request:
6+
types: opened
7+
8+
jobs:
9+
pre-commit:
10+
uses: cpp-linter/.github/.github/workflows/pre-commit.yml@main

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ clang_tools.egg-info
44
clang_tools/__pycache__
55
dist
66
clang_tools/llvm-project*
7-
.coverage
7+
.coverage*
88
coverage.xml
99
htmlcov/
1010
.vscode/

.pre-commit-config.yaml

+5-6
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ repos:
1212
- id: debug-statements
1313
- id: requirements-txt-fixer
1414
- repo: https://github.com/asottile/pyupgrade
15-
rev: v3.15.0
15+
rev: v3.15.1
1616
hooks:
1717
- id: pyupgrade
18-
- repo: https://github.com/pycqa/flake8
19-
rev: '7.0.0'
18+
- repo: https://github.com/astral-sh/ruff-pre-commit
19+
rev: v0.2.2
2020
hooks:
21-
- id: flake8
22-
args: [--max-line-length=120]
23-
exclude: ^(.github/workflows/bump-version.py)
21+
- id: ruff
22+
- id: ruff-format
2423
# - repo: local
2524
# hooks:
2625
# - id: pytest

clang_tools/install.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,32 @@ def is_installed(tool_name: str, version: str) -> Optional[Path]:
3939
)
4040
try:
4141
result = subprocess.run(
42-
[exe_name, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
42+
[exe_name, "--version"],
43+
stdout=subprocess.PIPE,
44+
stderr=subprocess.PIPE,
45+
check=True,
4346
)
4447
except (FileNotFoundError, subprocess.CalledProcessError):
4548
return None # tool is not installed
4649
ver_num = RE_PARSE_VERSION.search(result.stdout)
4750
print(
4851
f"Found a installed version of {tool_name}:",
4952
ver_num.groups(0)[0].decode(encoding="utf-8"),
50-
end=" "
53+
end=" ",
5154
)
5255
path = shutil.which(exe_name) # find the installed binary
5356
if path is None:
5457
print() # print end-of-line
5558
return None # failed to locate the binary
5659
path = Path(path).resolve()
5760
print("at", str(path))
58-
ver_num = ver_num.groups(0)[0].decode(encoding="utf-8").split(".") # pragma: no cover
59-
if (
60-
ver_num is None or ver_num[0] != ver_major
61-
):
61+
ver_num = ver_num.groups(0)[0].decode(encoding="utf-8").split(".")
62+
if ver_num is None or ver_num[0] != ver_major:
6263
return None # version is unknown or not the desired major release
6364
return path
6465

6566

66-
def clang_tools_binary_url(
67-
tool: str, version: str, tag: str = release_tag
68-
) -> str:
67+
def clang_tools_binary_url(tool: str, version: str, tag: str = release_tag) -> str:
6968
"""Assemble the URL to the binary.
7069
7170
:param tool: The name of the tool to download.

clang_tools/main.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def get_parser() -> argparse.ArgumentParser:
2323
parser.add_argument(
2424
"-t",
2525
"--tool",
26-
nargs='+',
27-
default=['clang-format', 'clang-tidy'],
26+
nargs="+",
27+
default=["clang-format", "clang-tidy"],
2828
metavar="TOOL",
2929
help="Specify which tool(s) to install.",
3030
)
@@ -67,12 +67,16 @@ def main():
6767
uninstall_clang_tools(args.uninstall, args.directory)
6868
if args.install:
6969
install_clang_tools(
70-
args.install, args.tool, args.directory, args.overwrite, args.no_progress_bar
70+
args.install,
71+
args.tool,
72+
args.directory,
73+
args.overwrite,
74+
args.no_progress_bar,
7175
)
7276
else:
7377
print(
7478
f"{YELLOW}Nothing to do because `--install` and `--uninstall`",
75-
f"was not specified.{RESET_COLOR}"
79+
f"was not specified.{RESET_COLOR}",
7680
)
7781
parser.print_help()
7882

clang_tools/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def download_file(url: str, file_name: str, no_progress_bar: bool) -> Optional[s
4848
return None
4949
assert response.length is not None
5050
length = response.length
51-
buffer = b''
51+
buffer = b""
5252
progress_bar = "=" if check_install_os() == "windows" else "█"
5353
while len(buffer) < length:
5454
block_size = int(length / 20)

docs/conf.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"sphinx.ext.intersphinx",
3838
]
3939

40-
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
40+
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
4141

4242
# Add any paths that contain templates here, relative to this directory.
4343
templates_path = ["_templates"]
@@ -78,7 +78,7 @@
7878
"toggle": {
7979
"icon": "material/lightbulb-outline",
8080
"name": "Switch to dark mode",
81-
}
81+
},
8282
},
8383
{
8484
"media": "(prefers-color-scheme: dark)",
@@ -88,7 +88,7 @@
8888
"toggle": {
8989
"icon": "material/lightbulb",
9090
"name": "Switch to light mode",
91-
}
91+
},
9292
},
9393
],
9494
"features": [
@@ -98,7 +98,7 @@
9898
"toc.sticky",
9999
"toc.follow",
100100
"search.share",
101-
]
101+
],
102102
}
103103

104104
object_description_options = [
@@ -122,9 +122,7 @@ def setup(app: Sphinx):
122122
if arg.default != "==SUPPRESS==":
123123
doc += f" :Default: ``{repr(arg.default)}``\n\n"
124124
description = (
125-
""
126-
if arg.help is None
127-
else " %s\n" % (arg.help.replace('\n', '\n '))
125+
"" if arg.help is None else " %s\n" % (arg.help.replace("\n", "\n "))
128126
)
129127
doc += description
130128
cli_doc = Path(app.srcdir, "cli_args.rst")

pyproject.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ show_column_numbers = true
6161
[tool.coverage]
6262
[tool.coverage.run]
6363
dynamic_context = "test_function"
64+
parallel = true
65+
relative_files = true
6466
omit = [
6567
# don't include tests in coverage
66-
"tests/*",
68+
# "tests/*",
6769
]
6870

6971
[tool.coverage.json]
@@ -79,7 +81,5 @@ exclude_lines = [
7981
"pragma: no cover",
8082
# ignore any branch that makes the module executable
8183
'if __name__ == "__main__"',
82-
# we test parts of these algorithms separately
83-
"def uninstall_clang_tools",
8484
"def main",
8585
]
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4b96e93eedcbceafc0dea62cf46a002ba3d2a165215a83fbfe1e2a73d21888a3b36bc7da092fcad38fa56e615d7cb9d375a34eddc642ee08a4a29b836f938762 *clang-format-12_windows-amd64
1+
8c03bcc6f4166cd05039a6217f0c0834ed2a321e0a2a64a16b87923e94ee33a980c09b1e78e1aba5e9ea045ac843d8d51b32fcfe44fc5d98d840efb16b359bb6 *clang-format-12_windows-amd64

tests/test_install.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99
create_sym_link,
1010
install_tool,
1111
install_clang_tools,
12-
uninstall_tool,
12+
is_installed,
13+
uninstall_clang_tools,
1314
)
1415

1516

1617
@pytest.mark.parametrize("version", [str(v) for v in range(7, 17)] + ["12.0.1"])
17-
@pytest.mark.parametrize("tool_name", ["clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"])
18+
@pytest.mark.parametrize(
19+
"tool_name",
20+
["clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"],
21+
)
1822
def test_clang_tools_binary_url(tool_name: str, version: str):
1923
"""Test `clang_tools_binary_url()`"""
2024
url = clang_tools_binary_url(tool_name, version)
@@ -50,19 +54,27 @@ def test_create_symlink(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
5054
assert not create_sym_link(tool_name, version, str(tmp_path), True)
5155

5256

53-
@pytest.mark.parametrize("version", [str(v) for v in range(10, 17)] + ["12.0.1"])
57+
@pytest.mark.parametrize("version", ["12"])
5458
def test_install_tools(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, version: str):
5559
"""Test install tools to a temp directory."""
5660
monkeypatch.chdir(tmp_path)
57-
for tool_name in ("clang-format", "clang-tidy"):
58-
assert install_tool(tool_name, version, str(tmp_path), False)
59-
# invoking again should return False
60-
assert not install_tool(tool_name, version, str(tmp_path), False)
61-
# uninstall the tool deliberately
62-
uninstall_tool(tool_name, version, str(tmp_path))
63-
assert f"{tool_name}-{version}{suffix}" not in [
64-
fd.name for fd in tmp_path.iterdir()
65-
]
61+
tool_name = "clang-format"
62+
63+
assert install_tool(tool_name, version, str(tmp_path), False)
64+
# invoking again should return False
65+
assert not install_tool(tool_name, version, str(tmp_path), False)
66+
# uninstall the tool deliberately
67+
uninstall_clang_tools(version, str(tmp_path))
68+
assert f"{tool_name}-{version}{suffix}" not in [
69+
fd.name for fd in tmp_path.iterdir()
70+
]
71+
72+
73+
@pytest.mark.parametrize("version", ["0"])
74+
def test_is_installed(version: str):
75+
"""Test if installed version matches specified ``version``"""
76+
tool_path = is_installed("clang-format", version=version)
77+
assert tool_path is None
6678

6779

6880
def test_path_warning(capsys: pytest.CaptureFixture):
@@ -74,6 +86,8 @@ def test_path_warning(capsys: pytest.CaptureFixture):
7486
try:
7587
install_clang_tools("x", "x", ".", False, False)
7688
except OSError as exc:
77-
result = capsys.readouterr()
78-
assert "directory is not in your environment variable PATH" in result.out
89+
if install_dir_name(".") not in os.environ.get("PATH"): # pragma: no cover
90+
# this warning does not happen in an activated venv
91+
result = capsys.readouterr()
92+
assert "directory is not in your environment variable PATH" in result.out
7993
assert "Failed to download" in exc.args[0]

tests/test_main.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Tests that relate to the main.py module."""
2-
from typing import Optional
2+
from typing import Optional, List
33
from argparse import ArgumentParser
44
import pytest
55
from clang_tools.main import get_parser
@@ -14,6 +14,7 @@ class Args:
1414
overwrite: bool = False
1515
no_progress_bar: bool = False
1616
uninstall: Optional[str] = None
17+
tool: List[str] = ["clang-format", "clang-tidy"]
1718

1819

1920
@pytest.fixture
@@ -37,15 +38,8 @@ def test_cli_switch(switch_name: str, parser: ArgumentParser):
3738
assert getattr(args, switch_name.replace("-", "_"))
3839

3940

40-
@pytest.mark.parametrize("name, default_value", [
41-
("install", None),
42-
("uninstall", None),
43-
("overwrite", False),
44-
("no_progress_bar", False),
45-
("directory", ""),
46-
("tool", ['clang-format', 'clang-tidy'])
47-
])
48-
def test_default_args(parser: ArgumentParser, name, default_value):
41+
def test_default_args(parser: ArgumentParser):
4942
"""Test the default values of CLI args"""
5043
args = parser.parse_args([])
51-
assert getattr(args, name) == default_value
44+
for name, value in args.__dict__.items():
45+
assert getattr(Args, name) == value

tests/test_util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_check_install_os():
1919
def test_download_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, tag: str):
2020
"""Test that deliberately fails to download a file."""
2121
monkeypatch.chdir(str(tmp_path))
22-
url = clang_tools_binary_url("clang-format", "12", tag=release_tag)
22+
url = clang_tools_binary_url("clang-format", "12", tag=tag)
2323
file_name = download_file(url, "file.tar.gz", True)
2424
assert file_name is not None
2525

0 commit comments

Comments
 (0)