@vasanth-mv/pqs-cli 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -0
- package/bin/pqs.entry.js +693 -0
- package/bin/pw-quality.js +18 -0
- package/bin/qc-check.js +18 -0
- package/bin/qcbot.js +18 -0
- package/build.mjs +55 -0
- package/dist/pqs.js +7056 -0
- package/package.json +32 -0
- package/pqs-cheatsheet.html +688 -0
- package/pqs-cli-1.0.0.tgz +0 -0
- package/pqs-cli-1.1.0.tgz +0 -0
- package/pqs-cli-overview.pptx +0 -0
- package/pqs-report.json +14343 -0
- package/pw-quality-cli.pptx +0 -0
- package/python/README.md +236 -0
- package/python/pw_quality/__init__.py +21 -0
- package/python/pw_quality/cli.py +177 -0
- package/python/pw_quality/runner.py +294 -0
- package/python/pyproject.toml +42 -0
- package/src/cli.js +294 -0
- package/src/prBot.js +301 -0
- package/src/remediation.js +333 -0
- package/src/reportBuilder.js +187 -0
- package/~$pqs-cli-overview.pptx +0 -0
|
Binary file
|
package/python/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# qcBot — Python package
|
|
2
|
+
|
|
3
|
+
> **Python interface for the qcBot test quality analyser.**
|
|
4
|
+
> The rule engine runs in **Node.js** (via `npx @cqs/qcbot`).
|
|
5
|
+
> Node.js 18+ must be on your PATH — the package checks for it at call time.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Python?
|
|
10
|
+
|
|
11
|
+
The rule engine is written in JavaScript/TypeScript and runs in Node.js.
|
|
12
|
+
This Python package is a **thin subprocess bridge** — the same pattern
|
|
13
|
+
Playwright itself uses for its Python SDK.
|
|
14
|
+
|
|
15
|
+
- Zero rule duplication — one engine, many language wrappers
|
|
16
|
+
- Returns structured Python objects (`PwQualityResult`, `FileResult`, `Finding`)
|
|
17
|
+
- CLI (`qcbot-py check`) produces the same colour output as the Node.js CLI
|
|
18
|
+
- Works in Python 3.9+
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
| Requirement | Version |
|
|
25
|
+
|-------------|---------|
|
|
26
|
+
| Python | 3.9+ |
|
|
27
|
+
| Node.js + npx | **18+** (checked at runtime) |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Library only (use as import)
|
|
35
|
+
pip install qcbot
|
|
36
|
+
|
|
37
|
+
# Library + command-line tool
|
|
38
|
+
pip install "qcbot[cli]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quickstart — CLI
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Analyse a folder, fail if score < 80, generate HTML report
|
|
47
|
+
qcbot-py check ./functional-tests --threshold 80 --report
|
|
48
|
+
|
|
49
|
+
# List all supported stacks
|
|
50
|
+
qcbot-py stacks
|
|
51
|
+
|
|
52
|
+
# Get JSON output (pipe into jq, scripts, etc.)
|
|
53
|
+
qcbot-py check ./tests --json | python3 -c "import sys,json; r=json.load(sys.stdin); print(r['score'], r['passed'])"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quickstart — Python API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pw_quality import check
|
|
62
|
+
|
|
63
|
+
# Basic check
|
|
64
|
+
result = check("./functional-tests", threshold=80)
|
|
65
|
+
|
|
66
|
+
print(result.score) # 87
|
|
67
|
+
print(result.passed) # True
|
|
68
|
+
print(result.grade) # "Good"
|
|
69
|
+
print(result.summary) # Summary(critical=12, warnings=34, total=284, files=168)
|
|
70
|
+
|
|
71
|
+
# Per-file breakdown
|
|
72
|
+
for file in result.files:
|
|
73
|
+
print(f"{file.score:3} {file.name} ({file.critical_count} critical)")
|
|
74
|
+
|
|
75
|
+
# All findings
|
|
76
|
+
for finding in result.findings:
|
|
77
|
+
print(f"[{finding.severity}] {finding.title} — {finding.file}:{finding.line}")
|
|
78
|
+
|
|
79
|
+
# Critical findings only
|
|
80
|
+
criticals = result.critical_findings
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### `check(path, *, stack, threshold, ...) → PwQualityResult`
|
|
88
|
+
|
|
89
|
+
| Parameter | Type | Default | Description |
|
|
90
|
+
|-----------|------|---------|-------------|
|
|
91
|
+
| `path` | str / Path | — | Directory with test files |
|
|
92
|
+
| `stack` | str | `"playwright"` | Rule set id (`"typescript"`, `"pytest_api"`, …) |
|
|
93
|
+
| `threshold` | int | `80` | Minimum passing score |
|
|
94
|
+
| `parallel` | int | `4` | Parallel workers |
|
|
95
|
+
| `report` | bool | `False` | Write HTML report to disk |
|
|
96
|
+
| `output_dir` | str | `None` | Override report output directory |
|
|
97
|
+
| `no_cross_file` | bool | `False` | Skip cross-file duplicate detection |
|
|
98
|
+
| `project` | str | `None` | Project name in HTML report |
|
|
99
|
+
| `npm_package` | str | `"@cqs/qcbot"` | Override for private npm registry |
|
|
100
|
+
| `verbose` | bool | `False` | Print raw CLI output to stdout |
|
|
101
|
+
|
|
102
|
+
### `PwQualityResult`
|
|
103
|
+
|
|
104
|
+
| Attribute | Type | Description |
|
|
105
|
+
|-----------|------|-------------|
|
|
106
|
+
| `score` | int | Average quality score 0–100 |
|
|
107
|
+
| `passed` | bool | True if score ≥ threshold |
|
|
108
|
+
| `grade` | str | "Excellent" / "Good" / "Fair" / "Needs work" |
|
|
109
|
+
| `threshold` | int | Threshold used for this run |
|
|
110
|
+
| `summary` | Summary | `.critical`, `.warnings`, `.total`, `.files` |
|
|
111
|
+
| `files` | list[FileResult] | Per-file breakdown |
|
|
112
|
+
| `findings` | list[Finding] | All findings (flat, all files) |
|
|
113
|
+
| `critical_findings` | list[Finding] | Only severity=critical |
|
|
114
|
+
| `report_path` | str or None | Path to HTML report if `report=True` |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## CI / CD Integration
|
|
119
|
+
|
|
120
|
+
### GitHub Actions
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
- name: Install qcbot
|
|
124
|
+
run: pip install "qcbot[cli]"
|
|
125
|
+
|
|
126
|
+
- name: Quality Gate
|
|
127
|
+
run: qcbot-py check ./tests --threshold 80 --report
|
|
128
|
+
|
|
129
|
+
- name: Upload report
|
|
130
|
+
if: always()
|
|
131
|
+
uses: actions/upload-artifact@v4
|
|
132
|
+
with:
|
|
133
|
+
name: quality-report
|
|
134
|
+
path: qcbot-report/report.html
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### pytest integration
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# conftest.py — add quality gate as a pytest fixture / session hook
|
|
141
|
+
import pytest
|
|
142
|
+
from pw_quality import check, PwQualityError
|
|
143
|
+
|
|
144
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
145
|
+
try:
|
|
146
|
+
result = check("./tests", threshold=80)
|
|
147
|
+
if not result.passed:
|
|
148
|
+
print(f"\n⚠ Quality gate FAILED — score {result.score} < 80")
|
|
149
|
+
except PwQualityError as e:
|
|
150
|
+
print(f"\n⚠ qcBot not available: {e}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Django / Flask API endpoint
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pw_quality import check, PwQualityResult
|
|
157
|
+
|
|
158
|
+
@app.post("/api/quality-gate")
|
|
159
|
+
def quality_gate():
|
|
160
|
+
data = request.json
|
|
161
|
+
result: PwQualityResult = check(
|
|
162
|
+
data["path"],
|
|
163
|
+
stack=data.get("stack", "playwright"),
|
|
164
|
+
threshold=data.get("threshold", 80),
|
|
165
|
+
)
|
|
166
|
+
return {
|
|
167
|
+
"score": result.score,
|
|
168
|
+
"passed": result.passed,
|
|
169
|
+
"grade": result.grade,
|
|
170
|
+
"critical": result.summary.critical,
|
|
171
|
+
"findings": [
|
|
172
|
+
{"rule": f.rule_id, "severity": f.severity, "title": f.title, "line": f.line}
|
|
173
|
+
for f in result.findings
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Jenkins (Python-based pipeline)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# Jenkinsfile (python-pipeline plugin) or a build script
|
|
182
|
+
from pw_quality import check
|
|
183
|
+
|
|
184
|
+
result = check("./functional-tests", threshold=80, report=True)
|
|
185
|
+
print(f"Quality score: {result.score} — {'PASSED' if result.passed else 'FAILED'}")
|
|
186
|
+
if not result.passed:
|
|
187
|
+
raise SystemExit(1) # Fails the Jenkins step
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Supported stacks
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
qcbot-py stacks
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Includes: `playwright`, `typescript`, `java_api`, `restassured`, `karate`,
|
|
199
|
+
`pytest_api`, `postman`, `python_api`, `ts_frontend`, `python_frontend`,
|
|
200
|
+
`playwright_java`, `playwright_python`, and more.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Error handling
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from pw_quality import check, PwQualityError
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
result = check("./tests")
|
|
211
|
+
except PwQualityError as e:
|
|
212
|
+
print(f"Analysis failed: {e}")
|
|
213
|
+
# Common causes:
|
|
214
|
+
# - Node.js not found → install Node 18+
|
|
215
|
+
# - npx not available → ships with Node; re-install
|
|
216
|
+
# - Unknown stack → run qcbot-py stacks
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## FAQ
|
|
222
|
+
|
|
223
|
+
**Q: Does it send my code anywhere?**
|
|
224
|
+
> No. All rule-based analysis runs locally in Node.js. Only if you pass `--llm` to the underlying CLI does it contact an AI provider, using your own API key.
|
|
225
|
+
|
|
226
|
+
**Q: Do I need to install `@cqs/qcbot` separately?**
|
|
227
|
+
> No — `npx` downloads and caches it automatically on first run.
|
|
228
|
+
|
|
229
|
+
**Q: Can I use a private npm registry?**
|
|
230
|
+
> Yes: `check(path, npm_package="@cqs/qcbot")` and set `NPM_TOKEN` in your environment.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Licence
|
|
235
|
+
|
|
236
|
+
MIT — © Your Company
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pw_quality — Python interface for the qcBot CLI.
|
|
3
|
+
|
|
4
|
+
The rule engine lives in Node.js (npm: @cqs/qcbot).
|
|
5
|
+
This package is a thin wrapper that:
|
|
6
|
+
1. Checks Node.js ≥ 18 is available
|
|
7
|
+
2. Optionally bootstraps the npm package via npx
|
|
8
|
+
3. Calls the CLI as a subprocess and returns structured Python objects
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
-----
|
|
12
|
+
from pw_quality import check, stacks
|
|
13
|
+
|
|
14
|
+
result = check("./functional-tests", threshold=80, report=True)
|
|
15
|
+
print(result.score, result.passed, result.findings)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .runner import check, stacks, PwQualityResult, PwQualityError
|
|
19
|
+
|
|
20
|
+
__all__ = ["check", "stacks", "PwQualityResult", "PwQualityError"]
|
|
21
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py — Click-based command-line interface for the pw_quality Python package.
|
|
3
|
+
|
|
4
|
+
Installed as `qcbot-py` by pyproject.toml.
|
|
5
|
+
Thin shell over runner.py — all analysis runs in Node.js via npx.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import click
|
|
12
|
+
except ImportError:
|
|
13
|
+
print(
|
|
14
|
+
"ERROR: 'click' is required. Install it with: pip install qcbot[cli]",
|
|
15
|
+
file=sys.stderr,
|
|
16
|
+
)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
from . import runner
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── Formatting helpers ────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
def _grade_color(score: int) -> str:
|
|
25
|
+
if score >= 90: return "bright_green"
|
|
26
|
+
if score >= 75: return "cyan"
|
|
27
|
+
if score >= 60: return "yellow"
|
|
28
|
+
return "red"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sev_color(sev: str) -> str:
|
|
32
|
+
return {"critical": "red", "warning": "yellow"}.get(sev, "cyan")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _bar(score: int, width: int = 20) -> str:
|
|
36
|
+
filled = round(score / 100 * width)
|
|
37
|
+
return "█" * filled + "░" * (width - filled)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── CLI definition ────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
@click.group()
|
|
43
|
+
@click.version_option("1.0.0", prog_name="qcbot-py")
|
|
44
|
+
def cli():
|
|
45
|
+
"""qcBot — Playwright & multi-stack test quality analyser (Python interface).\n
|
|
46
|
+
The rule engine runs in Node.js (via npx @cqs/qcbot).
|
|
47
|
+
Node.js 18+ must be available on your PATH.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.command()
|
|
52
|
+
@click.argument("path")
|
|
53
|
+
@click.option("-s", "--stack", default="playwright", show_default=True, help="Rule set id. Run `qcbot-py stacks` for options.")
|
|
54
|
+
@click.option("-t", "--threshold", default=80, show_default=True, type=int, help="Fail if score is below this (0–100).")
|
|
55
|
+
@click.option("-p", "--parallel", default=4, show_default=True, type=int, help="Max files analysed in parallel.")
|
|
56
|
+
@click.option("--report", is_flag=True, help="Generate self-contained HTML report.")
|
|
57
|
+
@click.option("--output", default=None, help="Output directory for HTML/JSON reports.")
|
|
58
|
+
@click.option("--no-cross-file", is_flag=True, help="Skip cross-file duplicate detection.")
|
|
59
|
+
@click.option("--project", default=None, help="Project name shown in the HTML report header.")
|
|
60
|
+
@click.option("--json", "json_out",is_flag=True, help="Print the full JSON result to stdout instead of the summary table.")
|
|
61
|
+
@click.option("--package", default="@cqs/qcbot", help="Override npm package name (for private registries).")
|
|
62
|
+
def check(path, stack, threshold, parallel, report, output, no_cross_file, project, json_out, package):
|
|
63
|
+
"""Analyse test files in PATH and report quality findings.
|
|
64
|
+
|
|
65
|
+
PATH is a directory or glob that contains the spec/test files.
|
|
66
|
+
|
|
67
|
+
\b
|
|
68
|
+
Examples:
|
|
69
|
+
qcbot-py check ./functional-tests
|
|
70
|
+
qcbot-py check ./tests --stack pytest_api --threshold 75 --report
|
|
71
|
+
qcbot-py check ./src/tests --json | jq '.score'
|
|
72
|
+
"""
|
|
73
|
+
click.echo(f"\n⚡ qcBot · stack: {stack} · threshold: {threshold}\n")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
result = runner.check(
|
|
77
|
+
path,
|
|
78
|
+
stack=stack,
|
|
79
|
+
threshold=threshold,
|
|
80
|
+
parallel=parallel,
|
|
81
|
+
report=report,
|
|
82
|
+
output_dir=output,
|
|
83
|
+
no_cross_file=no_cross_file,
|
|
84
|
+
project=project,
|
|
85
|
+
npm_package=package,
|
|
86
|
+
verbose=False,
|
|
87
|
+
)
|
|
88
|
+
except runner.PwQualityError as exc:
|
|
89
|
+
click.secho(f"✗ Error: {exc}", fg="red", err=True)
|
|
90
|
+
sys.exit(2)
|
|
91
|
+
|
|
92
|
+
if json_out:
|
|
93
|
+
import json as _json, dataclasses
|
|
94
|
+
def _serial(obj):
|
|
95
|
+
if dataclasses.is_dataclass(obj):
|
|
96
|
+
return dataclasses.asdict(obj)
|
|
97
|
+
raise TypeError(f"Not serialisable: {type(obj)}")
|
|
98
|
+
click.echo(_json.dumps(dataclasses.asdict(result), indent=2, default=_serial))
|
|
99
|
+
sys.exit(0 if result.passed else 1)
|
|
100
|
+
|
|
101
|
+
# ── Pretty terminal output ────────────────────────────────────────────────
|
|
102
|
+
gc = _grade_color(result.score)
|
|
103
|
+
click.echo("Results")
|
|
104
|
+
click.echo("─" * 52)
|
|
105
|
+
click.secho(f" Score {_bar(result.score)} {result.score} {result.grade}", fg=gc, bold=True)
|
|
106
|
+
click.echo(f" Files {result.summary.files}")
|
|
107
|
+
crit_color = "red" if result.summary.critical > 0 else "green"
|
|
108
|
+
warn_color = "yellow" if result.summary.warnings > 0 else "green"
|
|
109
|
+
click.secho(f" Critical {result.summary.critical}", fg=crit_color, nl=False)
|
|
110
|
+
click.echo(" ", nl=False)
|
|
111
|
+
click.secho(f"Warnings {result.summary.warnings}", fg=warn_color, nl=False)
|
|
112
|
+
click.echo(f" Total {result.summary.total}")
|
|
113
|
+
status_color = "green" if result.passed else "red"
|
|
114
|
+
status_text = "✓ PASSED" if result.passed else "✗ FAILED"
|
|
115
|
+
click.secho(f" Status {status_text} (threshold {threshold})", fg=status_color, bold=True)
|
|
116
|
+
click.echo("─" * 52)
|
|
117
|
+
|
|
118
|
+
# Per-file
|
|
119
|
+
click.echo()
|
|
120
|
+
click.echo("File breakdown")
|
|
121
|
+
for fr in result.files:
|
|
122
|
+
fc = _grade_color(fr.score)
|
|
123
|
+
crit = fr.critical_count
|
|
124
|
+
crit_label = click.style(f"{crit} crit ", fg="red") if crit > 0 else click.style("✓ ", fg="green")
|
|
125
|
+
click.echo(
|
|
126
|
+
f" {click.style(str(fr.score).rjust(3), fg=fc)} "
|
|
127
|
+
f"{_bar(fr.score, 14)} "
|
|
128
|
+
f"{crit_label}"
|
|
129
|
+
f"{click.style(fr.name, fg='bright_black')}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Top findings
|
|
133
|
+
top = [f for f in result.findings if f.severity in ("critical", "warning")][:10]
|
|
134
|
+
if top:
|
|
135
|
+
click.echo()
|
|
136
|
+
click.echo("Top findings")
|
|
137
|
+
for f in top:
|
|
138
|
+
click.secho(f" {f.severity.upper():<9}", fg=_sev_color(f.severity), bold=True, nl=False)
|
|
139
|
+
click.echo(f" {f.title}")
|
|
140
|
+
loc = f.file or ""
|
|
141
|
+
if f.line:
|
|
142
|
+
loc += f":{f.line}"
|
|
143
|
+
click.secho(f" {loc} {f.rule_id}", fg="bright_black")
|
|
144
|
+
|
|
145
|
+
# Report link
|
|
146
|
+
if result.report_path:
|
|
147
|
+
click.echo()
|
|
148
|
+
click.secho(f"✓ HTML report → {result.report_path}", fg="green")
|
|
149
|
+
click.echo(f" Open: open {result.report_path}")
|
|
150
|
+
|
|
151
|
+
click.echo()
|
|
152
|
+
sys.exit(0 if result.passed else 1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command()
|
|
156
|
+
@click.option("--package", default="@cqs/qcbot", help="Override npm package name.")
|
|
157
|
+
def stacks(package):
|
|
158
|
+
"""List all supported stacks (language/framework rule sets)."""
|
|
159
|
+
try:
|
|
160
|
+
all_stacks = runner.stacks(npm_package=package)
|
|
161
|
+
except runner.PwQualityError as exc:
|
|
162
|
+
click.secho(f"✗ Error: {exc}", fg="red", err=True)
|
|
163
|
+
sys.exit(2)
|
|
164
|
+
|
|
165
|
+
click.echo("\nSupported stacks\n")
|
|
166
|
+
for s in all_stacks:
|
|
167
|
+
click.secho(f" {s['id']:<22}", fg="cyan", bold=True, nl=False)
|
|
168
|
+
click.echo(s.get("label", ""))
|
|
169
|
+
click.echo()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
cli()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|