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.
Files changed (55) hide show
  1. {testql-0.5.2 → testql-0.6.2}/PKG-INFO +7 -7
  2. {testql-0.5.2 → testql-0.6.2}/README.md +5 -5
  3. {testql-0.5.2 → testql-0.6.2}/pyproject.toml +2 -2
  4. {testql-0.5.2 → testql-0.6.2}/testql/__init__.py +1 -1
  5. testql-0.6.2/testql/cli.py +41 -0
  6. testql-0.6.2/testql/commands/__init__.py +27 -0
  7. testql-0.6.2/testql/commands/endpoints_cmd.py +137 -0
  8. testql-0.6.2/testql/commands/generate_cmd.py +140 -0
  9. testql-0.6.2/testql/commands/misc_cmds.py +529 -0
  10. testql-0.6.2/testql/commands/run_cmd.py +56 -0
  11. testql-0.6.2/testql/commands/suite_cmd.py +314 -0
  12. {testql-0.5.2 → testql-0.6.2}/testql/endpoint_detector.py +42 -34
  13. {testql-0.5.2 → testql-0.6.2}/testql/generator.py +2 -7
  14. {testql-0.5.2 → testql-0.6.2}/testql/openapi_generator.py +16 -2
  15. testql-0.6.2/testql/report_generator.py +248 -0
  16. testql-0.6.2/testql/sumd_generator.py +208 -0
  17. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/PKG-INFO +7 -7
  18. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/SOURCES.txt +6 -0
  19. testql-0.5.2/testql/cli.py +0 -1133
  20. testql-0.5.2/testql/commands/__init__.py +0 -5
  21. testql-0.5.2/testql/sumd_generator.py +0 -200
  22. {testql-0.5.2 → testql-0.6.2}/LICENSE +0 -0
  23. {testql-0.5.2 → testql-0.6.2}/setup.cfg +0 -0
  24. {testql-0.5.2 → testql-0.6.2}/testql/__main__.py +0 -0
  25. {testql-0.5.2 → testql-0.6.2}/testql/_base_fallback.py +0 -0
  26. {testql-0.5.2 → testql-0.6.2}/testql/base.py +0 -0
  27. {testql-0.5.2 → testql-0.6.2}/testql/commands/echo.py +0 -0
  28. {testql-0.5.2 → testql-0.6.2}/testql/commands/encoder_routes.py +0 -0
  29. {testql-0.5.2 → testql-0.6.2}/testql/doql_parser.py +0 -0
  30. {testql-0.5.2 → testql-0.6.2}/testql/echo_schemas.py +0 -0
  31. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/__init__.py +0 -0
  32. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_api_runner.py +0 -0
  33. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_assertions.py +0 -0
  34. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_converter.py +0 -0
  35. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_encoder.py +0 -0
  36. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_flow.py +0 -0
  37. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_parser.py +0 -0
  38. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_testtoon_parser.py +0 -0
  39. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/_websockets.py +0 -0
  40. {testql-0.5.2 → testql-0.6.2}/testql/interpreter/interpreter.py +0 -0
  41. {testql-0.5.2 → testql-0.6.2}/testql/interpreter.py +0 -0
  42. {testql-0.5.2 → testql-0.6.2}/testql/reporters/__init__.py +0 -0
  43. {testql-0.5.2 → testql-0.6.2}/testql/reporters/console.py +0 -0
  44. {testql-0.5.2 → testql-0.6.2}/testql/reporters/json_reporter.py +0 -0
  45. {testql-0.5.2 → testql-0.6.2}/testql/reporters/junit.py +0 -0
  46. {testql-0.5.2 → testql-0.6.2}/testql/runner.py +0 -0
  47. {testql-0.5.2 → testql-0.6.2}/testql/runners/__init__.py +0 -0
  48. {testql-0.5.2 → testql-0.6.2}/testql/sumd_parser.py +0 -0
  49. {testql-0.5.2 → testql-0.6.2}/testql/toon_parser.py +0 -0
  50. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/dependency_links.txt +0 -0
  51. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/entry_points.txt +0 -0
  52. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/requires.txt +0 -0
  53. {testql-0.5.2 → testql-0.6.2}/testql.egg-info/top_level.txt +0 -0
  54. {testql-0.5.2 → testql-0.6.2}/tests/test_encoder_routes.py +0 -0
  55. {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.5.2
4
- Summary: TestQL with endpoint detection, OpenAPI, and SUMD generation
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
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.40-green) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
33
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.6.2-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
35
 
35
- This project uses AI-generated code. Total cost: **$2.4000** with **16** AI commits.
36
+ - 🤖 **LLM usage:** $2.7000 (18 commits)
37
+ - 👤 **Human dev:** ~$1715 (17.2h @ $100/h, 30min dedup)
36
38
 
37
- Generated on 2026-04-18 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
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
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.40-green) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.6.2-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
7
8
 
8
- This project uses AI-generated code. Total cost: **$2.4000** with **16** AI commits.
9
+ - 🤖 **LLM usage:** $2.7000 (18 commits)
10
+ - 👤 **Human dev:** ~$1715 (17.2h @ $100/h, 30min dedup)
9
11
 
10
- Generated on 2026-04-18 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
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.5.2"
8
- description = "TestQL with endpoint detection, OpenAPI, and SUMD generation"
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"
@@ -1,3 +1,3 @@
1
1
  """TestQL — Interface Query Language for GUI/API/encoder testing."""
2
2
 
3
- __version__ = "0.5.2"
3
+ __version__ = "0.6.2"
@@ -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']}")