included coverage.py
Some checks failed
Sonarqube Scanner / Build and analyze (push) Failing after 27s
Some checks failed
Sonarqube Scanner / Build and analyze (push) Failing after 27s
This commit is contained in:
parent
95026aac6e
commit
0b5c994957
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@ coverage/
|
|||||||
.coverage
|
.coverage
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
.python-test-deps/
|
||||||
|
|||||||
@ -108,11 +108,12 @@ Der Integrationstest prüft dabei ausdrücklich, dass lesende Requests nicht pr
|
|||||||
Die kombinierte Coverage wird mit folgenden Befehlen erzeugt:
|
Die kombinierte Coverage wird mit folgenden Befehlen erzeugt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install -r requirements-dev.txt
|
|
||||||
npm ci
|
npm ci
|
||||||
npm run test:coverage
|
npm run test:coverage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Der Python-Coverage-Runner verwendet ein bereits installiertes kompatibles `coverage`-Modul. Fehlt es, installiert er die in `requirements-dev.txt` festgelegte Version isoliert unter `.python-test-deps/`; eine globale Python-Paketinstallation ist nicht erforderlich. Installations- und Ausführungsfehler werden mit vollständigem Python-Stacktrace auf `stderr` ausgegeben.
|
||||||
|
|
||||||
Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/coverage.xml` für die Python-Werkzeuge. Die zugehörigen SonarQube-Pfade stehen in `sonar-project.properties`. Die Coverage-Skripte erzwingen selbst keinen Mindestwert; die Bewertung und die Hinweise zu nicht abgedeckten Stellen bleiben Aufgabe des SonarQube Quality Gates.
|
Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/coverage.xml` für die Python-Werkzeuge. Die zugehörigen SonarQube-Pfade stehen in `sonar-project.properties`. Die Coverage-Skripte erzwingen selbst keinen Mindestwert; die Bewertung und die Hinweise zu nicht abgedeckten Stellen bleiben Aufgabe des SonarQube Quality Gates.
|
||||||
|
|
||||||
## Technische Hinweise
|
## Technische Hinweise
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.6",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.6",
|
"version": "0.12.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@file-type/av": "0.2.0",
|
"@file-type/av": "0.2.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.6",
|
"version": "0.12.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"test:coverage": "npm run test:coverage:js && npm run test:coverage:python",
|
"test:coverage": "npm run test:coverage:js && npm run test:coverage:python",
|
||||||
"test:python": "python3 -m unittest discover -s test/python -p 'test_*.py'",
|
"test:python": "python3 -m unittest discover -s test/python -p 'test_*.py'",
|
||||||
"test:coverage:js": "c8 --exclude='test/**' --exclude='test-support/**' --reporter=text-summary --reporter=lcov --reports-dir=coverage/js node --test",
|
"test:coverage:js": "c8 --exclude='test/**' --exclude='test-support/**' --reporter=text-summary --reporter=lcov --reports-dir=coverage/js node --test",
|
||||||
"test:coverage:python": "python3 -m coverage erase && python3 -m coverage run -m unittest discover -s test/python -p 'test_*.py' && python3 -m coverage xml -o coverage/python/coverage.xml && python3 -m coverage report"
|
"test:coverage:python": "python3 test-support/python_coverage_runner.py"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@file-type/av": "0.2.0",
|
"@file-type/av": "0.2.0",
|
||||||
|
|||||||
143
test-support/python_coverage_runner.py
Normal file
143
test-support/python_coverage_runner.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Erzeugt die Python-Coverage reproduzierbar ohne globale Paketinstallation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
REQUIREMENTS_FILE = PROJECT_ROOT / "requirements-dev.txt"
|
||||||
|
DEPENDENCY_DIR = PROJECT_ROOT / ".python-test-deps"
|
||||||
|
REQUIREMENTS_MARKER = DEPENDENCY_DIR / ".requirements.sha256"
|
||||||
|
|
||||||
|
|
||||||
|
def _requirements_digest() -> str:
|
||||||
|
return hashlib.sha256(REQUIREMENTS_FILE.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _dependency_environment(include_local: bool) -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
if include_local:
|
||||||
|
existing = env.get("PYTHONPATH", "")
|
||||||
|
paths = [str(DEPENDENCY_DIR)]
|
||||||
|
if existing:
|
||||||
|
paths.append(existing)
|
||||||
|
env["PYTHONPATH"] = os.pathsep.join(paths)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _coverage_available(env: dict[str, str]) -> bool:
|
||||||
|
check = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-c",
|
||||||
|
"import coverage; assert hasattr(coverage, 'Coverage')",
|
||||||
|
],
|
||||||
|
cwd=PROJECT_ROOT,
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return check.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _local_dependencies_are_current() -> bool:
|
||||||
|
if not REQUIREMENTS_MARKER.is_file():
|
||||||
|
return False
|
||||||
|
return REQUIREMENTS_MARKER.read_text(encoding="ascii").strip() == _requirements_digest()
|
||||||
|
|
||||||
|
|
||||||
|
def _install_local_dependencies() -> dict[str, str]:
|
||||||
|
if DEPENDENCY_DIR.exists():
|
||||||
|
shutil.rmtree(DEPENDENCY_DIR)
|
||||||
|
DEPENDENCY_DIR.mkdir(parents=True)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--disable-pip-version-check",
|
||||||
|
"--no-warn-script-location",
|
||||||
|
"--target",
|
||||||
|
str(DEPENDENCY_DIR),
|
||||||
|
"--requirement",
|
||||||
|
str(REQUIREMENTS_FILE),
|
||||||
|
],
|
||||||
|
cwd=PROJECT_ROOT,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
REQUIREMENTS_MARKER.write_text(_requirements_digest() + "\n", encoding="ascii")
|
||||||
|
|
||||||
|
env = _dependency_environment(include_local=True)
|
||||||
|
if not _coverage_available(env):
|
||||||
|
raise RuntimeError("Das lokal installierte Python-Coverage-Modul kann nicht importiert werden.")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _coverage_environment() -> dict[str, str]:
|
||||||
|
system_env = _dependency_environment(include_local=False)
|
||||||
|
if _coverage_available(system_env):
|
||||||
|
return system_env
|
||||||
|
|
||||||
|
local_env = _dependency_environment(include_local=True)
|
||||||
|
if _local_dependencies_are_current() and _coverage_available(local_env):
|
||||||
|
return local_env
|
||||||
|
|
||||||
|
return _install_local_dependencies()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_coverage(env: dict[str, str]) -> None:
|
||||||
|
commands = (
|
||||||
|
[sys.executable, "-m", "coverage", "erase"],
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"coverage",
|
||||||
|
"run",
|
||||||
|
"-m",
|
||||||
|
"unittest",
|
||||||
|
"discover",
|
||||||
|
"-s",
|
||||||
|
"test/python",
|
||||||
|
"-p",
|
||||||
|
"test_*.py",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"coverage",
|
||||||
|
"xml",
|
||||||
|
"-o",
|
||||||
|
"coverage/python/coverage.xml",
|
||||||
|
],
|
||||||
|
[sys.executable, "-m", "coverage", "report"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
subprocess.run(command, cwd=PROJECT_ROOT, env=env, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
_run_coverage(_coverage_environment())
|
||||||
|
except Exception as error: # noqa: BLE001 - CI-Ausgabe soll jeden unerwarteten Fehler enthalten.
|
||||||
|
print(
|
||||||
|
f"Python-Coverage fehlgeschlagen: {type(error).__name__}: {error}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -25,6 +25,7 @@ test('upload MIME types are detected from content and not trusted request metada
|
|||||||
|
|
||||||
test('Python API tools are syntax-valid and contain reusable 401 authentication handling', async () => {
|
test('Python API tools are syntax-valid and contain reusable 401 authentication handling', async () => {
|
||||||
const toolsDir = path.join(root, 'tools/python');
|
const toolsDir = path.join(root, 'tools/python');
|
||||||
|
const coverageRunnerPath = path.join(root, 'test-support/python_coverage_runner.py');
|
||||||
const names = (await fs.readdir(toolsDir)).filter(name => name.endsWith('.py'));
|
const names = (await fs.readdir(toolsDir)).filter(name => name.endsWith('.py'));
|
||||||
const expected = [
|
const expected = [
|
||||||
'append_route.py', 'common.py', 'create_audio.py', 'create_picture.py', 'create_poi.py',
|
'append_route.py', 'common.py', 'create_audio.py', 'create_picture.py', 'create_poi.py',
|
||||||
@ -36,15 +37,21 @@ test('Python API tools are syntax-valid and contain reusable 401 authentication
|
|||||||
expected.toSorted((left, right) => left.localeCompare(right))
|
expected.toSorted((left, right) => left.localeCompare(right))
|
||||||
);
|
);
|
||||||
|
|
||||||
const compile = spawnSync('python3', ['-m', 'py_compile', ...names.map(name => path.join(toolsDir, name))], {
|
const compile = spawnSync('python3', [
|
||||||
|
'-m', 'py_compile',
|
||||||
|
...names.map(name => path.join(toolsDir, name)),
|
||||||
|
coverageRunnerPath
|
||||||
|
], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
});
|
});
|
||||||
assert.equal(compile.status, 0, compile.stderr);
|
assert.equal(compile.status, 0, compile.stderr);
|
||||||
await fs.rm(path.join(toolsDir, '__pycache__'), { recursive: true, force: true });
|
await fs.rm(path.join(toolsDir, '__pycache__'), { recursive: true, force: true });
|
||||||
|
await fs.rm(path.join(root, 'test-support/__pycache__'), { recursive: true, force: true });
|
||||||
|
|
||||||
const common = await read('tools/python/common.py');
|
const common = await read('tools/python/common.py');
|
||||||
const sonar = await read('sonar-project.properties');
|
const sonar = await read('sonar-project.properties');
|
||||||
|
const coverageRunner = await read('test-support/python_coverage_runner.py');
|
||||||
const requirements = await read('requirements-dev.txt');
|
const requirements = await read('requirements-dev.txt');
|
||||||
const pkg = JSON.parse(await read('package.json'));
|
const pkg = JSON.parse(await read('package.json'));
|
||||||
assert.match(common, /class ApiClient/);
|
assert.match(common, /class ApiClient/);
|
||||||
@ -58,7 +65,10 @@ test('Python API tools are syntax-valid and contain reusable 401 authentication
|
|||||||
assert.match(common, /traceback\.print_exception/);
|
assert.match(common, /traceback\.print_exception/);
|
||||||
assert.doesNotMatch(common, /Content-Type:.*application\/octet-stream/);
|
assert.doesNotMatch(common, /Content-Type:.*application\/octet-stream/);
|
||||||
assert.match(pkg.scripts['test:coverage'], /test:coverage:python/);
|
assert.match(pkg.scripts['test:coverage'], /test:coverage:python/);
|
||||||
assert.match(pkg.scripts['test:coverage:python'], /coverage\/python\/coverage\.xml/);
|
assert.match(pkg.scripts['test:coverage:python'], /python_coverage_runner\.py/);
|
||||||
|
assert.match(coverageRunner, /coverage\/python\/coverage\.xml/);
|
||||||
|
assert.match(coverageRunner, /--target/);
|
||||||
|
assert.match(coverageRunner, /traceback\.print_exc/);
|
||||||
assert.match(sonar, /sonar\.python\.coverage\.reportPaths=coverage\/python\/coverage\.xml/);
|
assert.match(sonar, /sonar\.python\.coverage\.reportPaths=coverage\/python\/coverage\.xml/);
|
||||||
assert.match(requirements, /^coverage==/m);
|
assert.match(requirements, /^coverage==/m);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user