testql 0.5.2__tar.gz → 0.6.2__tar.gz
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.
- {testql-0.5.2 → testql-0.6.2}/PKG-INFO +7 -7
- {testql-0.5.2 → testql-0.6.2}/README.md +5 -5
- {testql-0.5.2 → testql-0.6.2}/pyproject.toml +2 -2
- {testql-0.5.2 → testql-0.6.2}/testql/__init__.py +1 -1
- testql-0.6.2/testql/cli.py +41 -0
- testql-0.6.2/testql/commands/__init__.py +27 -0
- testql-0.6.2/testql/commands/endpoints_cmd.py +137 -0
- testql-0.6.2/testql/commands/generate_cmd.py +140 -0
- testql-0.6.2/testql/commands/misc_cmds.py +529 -0
- testql-0.6.2/testql/commands/run_cmd.py +56 -0
- testql-0.6.2/testql/commands/suite_cmd.py +314 -0
- {testql-0.5.2 → testql-0.6.2}/testql/endpoint_detector.py +42 -34
- {testql-0.5.2 → testql-0.6.2}/testql/generator.py +2 -7
- {testql-0.5.2 → testql-0.6.2}/testql/openapi_generator.py +16 -2
- testql-0.6.2/testql/report_generator.py +248 -0
- testql-0.6.2/testql/sumd_generator.py +208 -0
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/PKG-INFO +7 -7
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/SOURCES.txt +6 -0
- testql-0.5.2/testql/cli.py +0 -1133
- testql-0.5.2/testql/commands/__init__.py +0 -5
- testql-0.5.2/testql/sumd_generator.py +0 -200
- {testql-0.5.2 → testql-0.6.2}/LICENSE +0 -0
- {testql-0.5.2 → testql-0.6.2}/setup.cfg +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/__main__.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/_base_fallback.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/base.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/commands/echo.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/commands/encoder_routes.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/doql_parser.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/echo_schemas.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/__init__.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_api_runner.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_assertions.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_converter.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_encoder.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_flow.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_parser.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_testtoon_parser.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_websockets.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter/interpreter.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/interpreter.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/reporters/__init__.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/reporters/console.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/reporters/json_reporter.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/reporters/junit.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/runner.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/runners/__init__.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/sumd_parser.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql/toon_parser.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/dependency_links.txt +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/entry_points.txt +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/requires.txt +0 -0
- {testql-0.5.2 → testql-0.6.2}/testql.egg-info/top_level.txt +0 -0
- {testql-0.5.2 → testql-0.6.2}/tests/test_encoder_routes.py +0 -0
- {testql-0.5.2 → testql-0.6.2}/tests/test_interpreter.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testql
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: TestQL with endpoint detection, OpenAPI,
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: TestQL with endpoint detection, OpenAPI, SUMD generation, SUMD parser and HTML report generation
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -30,16 +30,16 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
## AI Cost Tracking
|
|
32
32
|
|
|
33
|
-
   
|
|
34
|
+
  
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
- 🤖 **LLM usage:** $2.7000 (18 commits)
|
|
37
|
+
- 👤 **Human dev:** ~$1715 (17.2h @ $100/h, 30min dedup)
|
|
36
38
|
|
|
37
|
-
Generated on 2026-04-
|
|
39
|
+
Generated on 2026-04-19 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
40
|
|
|
39
41
|
---
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
43
|
TestQL is a declarative DSL (Domain Specific Language) for testing GUI, REST API, and hardware encoder interfaces. It provides a simple, readable syntax for writing automated tests without programming overhead.
|
|
44
44
|
|
|
45
45
|
## Installation
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
   
|
|
7
|
+
  
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
- 🤖 **LLM usage:** $2.7000 (18 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1715 (17.2h @ $100/h, 30min dedup)
|
|
9
11
|
|
|
10
|
-
Generated on 2026-04-
|
|
12
|
+
Generated on 2026-04-19 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
11
13
|
|
|
12
14
|
---
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
16
|
TestQL is a declarative DSL (Domain Specific Language) for testing GUI, REST API, and hardware encoder interfaces. It provides a simple, readable syntax for writing automated tests without programming overhead.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "testql"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "TestQL with endpoint detection, OpenAPI,
|
|
7
|
+
version = "0.6.2"
|
|
8
|
+
description = "TestQL with endpoint detection, OpenAPI, SUMD generation, SUMD parser and HTML report generation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""TestQL CLI — run .testql.toon.yaml scenarios from the command line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from testql.commands.endpoints_cmd import endpoints, openapi
|
|
8
|
+
from testql.commands.generate_cmd import analyze, generate
|
|
9
|
+
from testql.commands.misc_cmds import create, echo, from_sumd, init, report, watch
|
|
10
|
+
from testql.commands.run_cmd import run
|
|
11
|
+
from testql.commands.suite_cmd import list_tests, suite
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
@click.version_option(version="0.2.0")
|
|
16
|
+
def cli():
|
|
17
|
+
"""TestQL — Interface Query Language for Testing."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli.add_command(run)
|
|
22
|
+
cli.add_command(generate)
|
|
23
|
+
cli.add_command(analyze)
|
|
24
|
+
cli.add_command(endpoints)
|
|
25
|
+
cli.add_command(openapi)
|
|
26
|
+
cli.add_command(suite)
|
|
27
|
+
cli.add_command(list_tests, name="list")
|
|
28
|
+
cli.add_command(init)
|
|
29
|
+
cli.add_command(create)
|
|
30
|
+
cli.add_command(watch)
|
|
31
|
+
cli.add_command(from_sumd)
|
|
32
|
+
cli.add_command(report)
|
|
33
|
+
cli.add_command(echo)
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
"""Entry point for console script."""
|
|
37
|
+
cli()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""TestQL commands package."""
|
|
2
|
+
|
|
3
|
+
from testql.commands.echo import echo
|
|
4
|
+
from testql.commands.endpoints_cmd import endpoints, openapi
|
|
5
|
+
from testql.commands.generate_cmd import analyze, generate
|
|
6
|
+
from testql.commands.misc_cmds import create
|
|
7
|
+
from testql.commands.misc_cmds import echo as cli_echo
|
|
8
|
+
from testql.commands.misc_cmds import from_sumd, init, report, watch
|
|
9
|
+
from testql.commands.run_cmd import run
|
|
10
|
+
from testql.commands.suite_cmd import list_tests, suite
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"echo",
|
|
14
|
+
"cli_echo",
|
|
15
|
+
"endpoints",
|
|
16
|
+
"openapi",
|
|
17
|
+
"analyze",
|
|
18
|
+
"generate",
|
|
19
|
+
"create",
|
|
20
|
+
"from_sumd",
|
|
21
|
+
"init",
|
|
22
|
+
"report",
|
|
23
|
+
"watch",
|
|
24
|
+
"run",
|
|
25
|
+
"list_tests",
|
|
26
|
+
"suite",
|
|
27
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""CLI commands: testql endpoints, testql openapi — endpoint detection & OpenAPI generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
13
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]), default="table")
|
|
14
|
+
@click.option("--framework", help="Filter by framework (fastapi, flask, django, express)")
|
|
15
|
+
@click.option("--type", "endpoint_type", help="Filter by type (rest, graphql, websocket)")
|
|
16
|
+
@click.option("--output", "-o", type=click.Path(), help="Save to file")
|
|
17
|
+
def endpoints(
|
|
18
|
+
path: str,
|
|
19
|
+
fmt: str,
|
|
20
|
+
framework: str | None,
|
|
21
|
+
endpoint_type: str | None,
|
|
22
|
+
output: str | None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""List all detected API endpoints in a project."""
|
|
25
|
+
import csv
|
|
26
|
+
import io
|
|
27
|
+
import json
|
|
28
|
+
|
|
29
|
+
from testql.endpoint_detector import UnifiedEndpointDetector
|
|
30
|
+
|
|
31
|
+
target_path = Path(path)
|
|
32
|
+
detector = UnifiedEndpointDetector(target_path)
|
|
33
|
+
eps = detector.detect_all()
|
|
34
|
+
|
|
35
|
+
if framework:
|
|
36
|
+
eps = [ep for ep in eps if ep.framework == framework]
|
|
37
|
+
if endpoint_type:
|
|
38
|
+
eps = [ep for ep in eps if ep.endpoint_type == endpoint_type]
|
|
39
|
+
|
|
40
|
+
if not eps:
|
|
41
|
+
click.echo("❌ No endpoints detected")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
output_str = _format_endpoints(eps, fmt, target_path, detector)
|
|
45
|
+
|
|
46
|
+
if output:
|
|
47
|
+
Path(output).write_text(output_str)
|
|
48
|
+
click.echo(f"✅ Saved {len(eps)} endpoints to {output}")
|
|
49
|
+
else:
|
|
50
|
+
click.echo(output_str)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _format_endpoints(eps, fmt: str, target_path: Path, detector) -> str:
|
|
54
|
+
import csv
|
|
55
|
+
import io
|
|
56
|
+
import json
|
|
57
|
+
|
|
58
|
+
if fmt == "json":
|
|
59
|
+
data = [
|
|
60
|
+
{
|
|
61
|
+
"path": ep.path,
|
|
62
|
+
"method": ep.method,
|
|
63
|
+
"framework": ep.framework,
|
|
64
|
+
"type": ep.endpoint_type,
|
|
65
|
+
"handler": ep.handler_name,
|
|
66
|
+
"file": str(ep.source_file.relative_to(target_path)) if ep.source_file else None,
|
|
67
|
+
"line": ep.line_number,
|
|
68
|
+
"summary": ep.summary,
|
|
69
|
+
}
|
|
70
|
+
for ep in eps
|
|
71
|
+
]
|
|
72
|
+
return json.dumps(data, indent=2)
|
|
73
|
+
|
|
74
|
+
if fmt == "csv":
|
|
75
|
+
buf = io.StringIO()
|
|
76
|
+
writer = csv.writer(buf)
|
|
77
|
+
writer.writerow(["method", "path", "framework", "type", "handler", "file", "line", "summary"])
|
|
78
|
+
for ep in eps:
|
|
79
|
+
writer.writerow([
|
|
80
|
+
ep.method,
|
|
81
|
+
ep.path,
|
|
82
|
+
ep.framework,
|
|
83
|
+
ep.endpoint_type,
|
|
84
|
+
ep.handler_name or "",
|
|
85
|
+
str(ep.source_file.relative_to(target_path)) if ep.source_file else "",
|
|
86
|
+
ep.line_number,
|
|
87
|
+
ep.summary or "",
|
|
88
|
+
])
|
|
89
|
+
return buf.getvalue()
|
|
90
|
+
|
|
91
|
+
lines = [f"{'Method':<8} {'Path':<40} {'Framework':<12} {'Handler':<30}", "-" * 90]
|
|
92
|
+
for ep in eps:
|
|
93
|
+
handler = ep.handler_name or "-"
|
|
94
|
+
if len(handler) > 28:
|
|
95
|
+
handler = handler[:25] + "..."
|
|
96
|
+
ep_path = ep.path if len(ep.path) < 38 else ep.path[:35] + "..."
|
|
97
|
+
lines.append(f"{ep.method:<8} {ep_path:<40} {ep.framework:<12} {handler:<30}")
|
|
98
|
+
lines.append("")
|
|
99
|
+
lines.append(f"Total: {len(eps)} endpoints (detectors: {', '.join(detector.detectors_used)})")
|
|
100
|
+
return "\n".join(lines)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@click.command()
|
|
104
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
105
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file (default: openapi.yaml)")
|
|
106
|
+
@click.option("--format", type=click.Choice(["yaml", "json"]), default="yaml", help="Output format")
|
|
107
|
+
@click.option("--title", help="API title (default: project name)")
|
|
108
|
+
@click.option("--version", default="1.0.0", help="API version")
|
|
109
|
+
@click.option("--contract-tests", is_flag=True, help="Also generate contract tests")
|
|
110
|
+
def openapi(
|
|
111
|
+
path: str,
|
|
112
|
+
output: str | None,
|
|
113
|
+
format: str,
|
|
114
|
+
title: str | None,
|
|
115
|
+
version: str,
|
|
116
|
+
contract_tests: bool,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Generate OpenAPI spec from detected endpoints."""
|
|
119
|
+
from testql.openapi_generator import ContractTestGenerator, OpenAPIGenerator
|
|
120
|
+
|
|
121
|
+
target_path = Path(path)
|
|
122
|
+
generator = OpenAPIGenerator(target_path)
|
|
123
|
+
|
|
124
|
+
click.echo(f"🔍 Detecting endpoints in {target_path}...")
|
|
125
|
+
spec = generator.generate(title=title, version=version)
|
|
126
|
+
|
|
127
|
+
out_path = Path(output) if output else target_path / f"openapi.{format}"
|
|
128
|
+
generator.save(out_path, format)
|
|
129
|
+
click.echo(f"✅ OpenAPI spec saved: {out_path}")
|
|
130
|
+
click.echo(f" Endpoints: {len(spec.paths)}")
|
|
131
|
+
click.echo(f" Format: {format}")
|
|
132
|
+
|
|
133
|
+
if contract_tests:
|
|
134
|
+
test_file = out_path.parent / "testql-contracts.testql.toon.yaml"
|
|
135
|
+
contract_gen = ContractTestGenerator(spec)
|
|
136
|
+
contract_gen.generate_contract_tests(test_file)
|
|
137
|
+
click.echo(f"✅ Contract tests saved: {test_file}")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""CLI commands: testql generate, testql analyze — project test generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
13
|
+
@click.option("--output-dir", "-o", help="Output directory for generated tests")
|
|
14
|
+
@click.option("--analyze-only", is_flag=True, help="Only analyze, don't generate")
|
|
15
|
+
@click.option("--format", "fmt", type=click.Choice(["testql", "json"]), default="testql")
|
|
16
|
+
def generate(path: str, output_dir: str | None, analyze_only: bool, fmt: str) -> None:
|
|
17
|
+
"""Generate TestQL scenarios from project structure."""
|
|
18
|
+
from testql.generator import TestGenerator, MultiProjectTestGenerator
|
|
19
|
+
|
|
20
|
+
target_path = Path(path)
|
|
21
|
+
|
|
22
|
+
if any((target_path / d).exists() for d in ["doql", "oql", "oqlos", "testql", "weboql", "www"]):
|
|
23
|
+
click.echo(f"🔄 Analyzing workspace: {target_path}")
|
|
24
|
+
gen = MultiProjectTestGenerator(target_path)
|
|
25
|
+
profiles = gen.analyze_all()
|
|
26
|
+
|
|
27
|
+
click.echo(f"📊 Discovered {len(profiles)} projects:")
|
|
28
|
+
for name, profile in profiles.items():
|
|
29
|
+
click.echo(f" • {name}: {profile.project_type}")
|
|
30
|
+
click.echo(f" - Test patterns: {len(profile.test_patterns)}")
|
|
31
|
+
click.echo(f" - Config files: {len(profile.config)}")
|
|
32
|
+
|
|
33
|
+
if analyze_only:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
results = gen.generate_all()
|
|
37
|
+
total = sum(len(files) for files in results.values())
|
|
38
|
+
click.echo(f"\n✅ Generated {total} test files across {len(results)} projects")
|
|
39
|
+
|
|
40
|
+
cross_file = gen.generate_cross_project_tests(target_path / "testql-scenarios")
|
|
41
|
+
click.echo(f"🌐 Cross-project tests: {cross_file}")
|
|
42
|
+
|
|
43
|
+
else:
|
|
44
|
+
click.echo(f"🔄 Analyzing project: {target_path}")
|
|
45
|
+
gen = TestGenerator(target_path)
|
|
46
|
+
profile = gen.analyze()
|
|
47
|
+
|
|
48
|
+
click.echo(f"📊 Project type: {profile.project_type}")
|
|
49
|
+
click.echo(f"📊 Test patterns: {len(profile.test_patterns)}")
|
|
50
|
+
click.echo(f"📊 Discovered files: {sum(len(v) for v in profile.discovered_files.values())}")
|
|
51
|
+
|
|
52
|
+
if analyze_only:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
out_dir = Path(output_dir) if output_dir else None
|
|
56
|
+
generated = gen.generate_tests(out_dir)
|
|
57
|
+
|
|
58
|
+
click.echo(f"\n✅ Generated {len(generated)} test files:")
|
|
59
|
+
for f in generated:
|
|
60
|
+
click.echo(f" • {f}")
|
|
61
|
+
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.command()
|
|
66
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
67
|
+
def analyze(path: str) -> None:
|
|
68
|
+
"""Analyze project structure and show testability report."""
|
|
69
|
+
from testql.generator import TestGenerator
|
|
70
|
+
|
|
71
|
+
target_path = Path(path)
|
|
72
|
+
gen = TestGenerator(target_path)
|
|
73
|
+
profile = gen.analyze()
|
|
74
|
+
|
|
75
|
+
click.echo(f"\n📁 Project: {profile.name}")
|
|
76
|
+
click.echo(f"📂 Path: {profile.root_path}")
|
|
77
|
+
click.echo(f"🔧 Type: {profile.project_type}")
|
|
78
|
+
click.echo("\n📊 Discovered Files:")
|
|
79
|
+
|
|
80
|
+
for category, files in sorted(profile.discovered_files.items()):
|
|
81
|
+
click.echo(f" • {category}: {len(files)} files")
|
|
82
|
+
|
|
83
|
+
click.echo(f"\n🧪 Test Patterns Found: {len(profile.test_patterns)}")
|
|
84
|
+
for pattern in profile.test_patterns[:10]:
|
|
85
|
+
click.echo(f" • {pattern.name} ({pattern.pattern_type})")
|
|
86
|
+
if len(profile.test_patterns) > 10:
|
|
87
|
+
click.echo(f" ... and {len(profile.test_patterns) - 10} more")
|
|
88
|
+
|
|
89
|
+
_print_routes_section(profile)
|
|
90
|
+
_print_scenarios_section(profile)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_routes_section(profile) -> None:
|
|
94
|
+
if not profile.config.get("discovered_routes"):
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
routes = profile.config["discovered_routes"]
|
|
98
|
+
frameworks = profile.config.get("endpoint_frameworks", [])
|
|
99
|
+
|
|
100
|
+
click.echo(f"\n🌐 API Routes Discovered: {len(routes)}")
|
|
101
|
+
if frameworks:
|
|
102
|
+
click.echo(f" Detectors used: {', '.join(frameworks)}")
|
|
103
|
+
|
|
104
|
+
routes_by_fw: dict[str, int] = {}
|
|
105
|
+
routes_by_type: dict[str, int] = {}
|
|
106
|
+
for route in routes:
|
|
107
|
+
fw = route.get("framework", "unknown")
|
|
108
|
+
et = route.get("endpoint_type", "rest")
|
|
109
|
+
routes_by_fw[fw] = routes_by_fw.get(fw, 0) + 1
|
|
110
|
+
routes_by_type[et] = routes_by_type.get(et, 0) + 1
|
|
111
|
+
|
|
112
|
+
if routes_by_fw:
|
|
113
|
+
click.echo(" By Framework:")
|
|
114
|
+
for fw, count in sorted(routes_by_fw.items(), key=lambda x: -x[1]):
|
|
115
|
+
click.echo(f" • {fw}: {count} endpoints")
|
|
116
|
+
|
|
117
|
+
if len(routes_by_type) > 1:
|
|
118
|
+
click.echo(" By Type:")
|
|
119
|
+
for et, count in routes_by_type.items():
|
|
120
|
+
click.echo(f" • {et}: {count}")
|
|
121
|
+
|
|
122
|
+
click.echo(" Sample Endpoints:")
|
|
123
|
+
for route in routes[:8]:
|
|
124
|
+
fw = route.get("framework", "?")
|
|
125
|
+
handler = route.get("handler", "")
|
|
126
|
+
info = f"{route['method']} {route['path']}"
|
|
127
|
+
if handler:
|
|
128
|
+
info += f" → {handler}"
|
|
129
|
+
click.echo(f" • [{fw}] {info}")
|
|
130
|
+
if len(routes) > 8:
|
|
131
|
+
click.echo(f" ... and {len(routes) - 8} more")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _print_scenarios_section(profile) -> None:
|
|
135
|
+
if not profile.config.get("scenario_patterns"):
|
|
136
|
+
return
|
|
137
|
+
scenarios = profile.config["scenario_patterns"]
|
|
138
|
+
click.echo(f"\n⚙️ OQL/CQL Scenarios: {len(scenarios)}")
|
|
139
|
+
for s in scenarios[:5]:
|
|
140
|
+
click.echo(f" • {s['name']}")
|