testql 1.2.2__tar.gz → 1.2.4__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 (286) hide show
  1. {testql-1.2.2 → testql-1.2.4}/PKG-INFO +18 -7
  2. {testql-1.2.2 → testql-1.2.4}/README.md +17 -6
  3. {testql-1.2.2 → testql-1.2.4}/pyproject.toml +1 -1
  4. {testql-1.2.2 → testql-1.2.4}/testql/__init__.py +1 -1
  5. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/sql_adapter.py +32 -1
  6. {testql-1.2.2 → testql-1.2.4}/testql/adapters/testtoon_adapter.py +58 -0
  7. {testql-1.2.2 → testql-1.2.4}/testql/cli.py +2 -0
  8. {testql-1.2.2 → testql-1.2.4}/testql/commands/__init__.py +2 -0
  9. {testql-1.2.2 → testql-1.2.4}/testql/commands/generate_cmd.py +58 -45
  10. testql-1.2.4/testql/commands/generate_topology_cmd.py +58 -0
  11. {testql-1.2.2 → testql-1.2.4}/testql/commands/inspect_cmd.py +3 -2
  12. testql-1.2.4/testql/discovery/probes/browser/__init__.py +7 -0
  13. testql-1.2.4/testql/discovery/probes/browser/playwright_page.py +147 -0
  14. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/network/http_endpoint.py +19 -1
  15. {testql-1.2.2 → testql-1.2.4}/testql/discovery/registry.py +8 -5
  16. testql-1.2.4/testql/pipeline.py +135 -0
  17. {testql-1.2.2 → testql-1.2.4}/testql/results/analyzer.py +222 -4
  18. {testql-1.2.2 → testql-1.2.4}/testql/topology/__init__.py +5 -0
  19. {testql-1.2.2 → testql-1.2.4}/testql/topology/builder.py +8 -4
  20. testql-1.2.4/testql/topology/generator.py +241 -0
  21. testql-1.2.4/testql/topology/sitemap.py +119 -0
  22. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/PKG-INFO +18 -7
  23. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/SOURCES.txt +9 -0
  24. testql-1.2.4/tests/test_adapter_capture_syntax.py +166 -0
  25. testql-1.2.4/tests/test_browser_discovery.py +110 -0
  26. {testql-1.2.2 → testql-1.2.4}/tests/test_generate_cmd.py +22 -28
  27. {testql-1.2.2 → testql-1.2.4}/tests/test_network_discovery.py +22 -0
  28. {testql-1.2.2 → testql-1.2.4}/tests/test_results.py +2 -2
  29. testql-1.2.4/tests/test_topology_generator.py +161 -0
  30. {testql-1.2.2 → testql-1.2.4}/LICENSE +0 -0
  31. {testql-1.2.2 → testql-1.2.4}/setup.cfg +0 -0
  32. {testql-1.2.2 → testql-1.2.4}/testql/__main__.py +0 -0
  33. {testql-1.2.2 → testql-1.2.4}/testql/_base_fallback.py +0 -0
  34. {testql-1.2.2 → testql-1.2.4}/testql/adapters/__init__.py +0 -0
  35. {testql-1.2.2 → testql-1.2.4}/testql/adapters/base.py +0 -0
  36. {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/__init__.py +0 -0
  37. {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/graphql_adapter.py +0 -0
  38. {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/query_executor.py +0 -0
  39. {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/schema_introspection.py +0 -0
  40. {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/subscription_runner.py +0 -0
  41. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/__init__.py +0 -0
  42. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/entity_extractor.py +0 -0
  43. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/grammar.py +0 -0
  44. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/intent_recognizer.py +0 -0
  45. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/lexicon/__init__.py +0 -0
  46. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/llm_fallback.py +0 -0
  47. {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/nl_adapter.py +0 -0
  48. {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/__init__.py +0 -0
  49. {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/compatibility.py +0 -0
  50. {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/descriptor_loader.py +0 -0
  51. {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/message_validator.py +0 -0
  52. {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/proto_adapter.py +0 -0
  53. {testql-1.2.2 → testql-1.2.4}/testql/adapters/registry.py +0 -0
  54. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/__init__.py +0 -0
  55. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/ddl_parser.py +0 -0
  56. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/dialect_resolver.py +0 -0
  57. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/fixtures.py +0 -0
  58. {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/query_parser.py +0 -0
  59. {testql-1.2.2 → testql-1.2.4}/testql/base.py +0 -0
  60. {testql-1.2.2 → testql-1.2.4}/testql/commands/discover_cmd.py +0 -0
  61. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/__init__.py +0 -0
  62. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/cli.py +0 -0
  63. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/context.py +0 -0
  64. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/formatters/__init__.py +0 -0
  65. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/formatters/text.py +0 -0
  66. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/__init__.py +0 -0
  67. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/doql.py +0 -0
  68. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/toon.py +0 -0
  69. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo.py +0 -0
  70. {testql-1.2.2 → testql-1.2.4}/testql/commands/echo_helpers.py +0 -0
  71. {testql-1.2.2 → testql-1.2.4}/testql/commands/encoder_routes.py +0 -0
  72. {testql-1.2.2 → testql-1.2.4}/testql/commands/endpoints_cmd.py +0 -0
  73. {testql-1.2.2 → testql-1.2.4}/testql/commands/generate_ir_cmd.py +0 -0
  74. {testql-1.2.2 → testql-1.2.4}/testql/commands/misc_cmds.py +0 -0
  75. {testql-1.2.2 → testql-1.2.4}/testql/commands/run_cmd.py +0 -0
  76. {testql-1.2.2 → testql-1.2.4}/testql/commands/run_ir_cmd.py +0 -0
  77. {testql-1.2.2 → testql-1.2.4}/testql/commands/self_test_cmd.py +0 -0
  78. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/__init__.py +0 -0
  79. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/cli.py +0 -0
  80. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/collection.py +0 -0
  81. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/execution.py +0 -0
  82. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/listing.py +0 -0
  83. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/reports.py +0 -0
  84. {testql-1.2.2 → testql-1.2.4}/testql/commands/suite_cmd.py +0 -0
  85. {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/__init__.py +0 -0
  86. {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/content.py +0 -0
  87. {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/templates.py +0 -0
  88. {testql-1.2.2 → testql-1.2.4}/testql/commands/topology_cmd.py +0 -0
  89. {testql-1.2.2 → testql-1.2.4}/testql/detectors/__init__.py +0 -0
  90. {testql-1.2.2 → testql-1.2.4}/testql/detectors/base.py +0 -0
  91. {testql-1.2.2 → testql-1.2.4}/testql/detectors/config_detector.py +0 -0
  92. {testql-1.2.2 → testql-1.2.4}/testql/detectors/django_detector.py +0 -0
  93. {testql-1.2.2 → testql-1.2.4}/testql/detectors/express_detector.py +0 -0
  94. {testql-1.2.2 → testql-1.2.4}/testql/detectors/fastapi_detector.py +0 -0
  95. {testql-1.2.2 → testql-1.2.4}/testql/detectors/flask_detector.py +0 -0
  96. {testql-1.2.2 → testql-1.2.4}/testql/detectors/graphql_detector.py +0 -0
  97. {testql-1.2.2 → testql-1.2.4}/testql/detectors/models.py +0 -0
  98. {testql-1.2.2 → testql-1.2.4}/testql/detectors/openapi_detector.py +0 -0
  99. {testql-1.2.2 → testql-1.2.4}/testql/detectors/test_detector.py +0 -0
  100. {testql-1.2.2 → testql-1.2.4}/testql/detectors/unified.py +0 -0
  101. {testql-1.2.2 → testql-1.2.4}/testql/detectors/websocket_detector.py +0 -0
  102. {testql-1.2.2 → testql-1.2.4}/testql/discovery/__init__.py +0 -0
  103. {testql-1.2.2 → testql-1.2.4}/testql/discovery/manifest.py +0 -0
  104. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/__init__.py +0 -0
  105. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/base.py +0 -0
  106. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/__init__.py +0 -0
  107. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/api_openapi.py +0 -0
  108. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/container_compose.py +0 -0
  109. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/container_dockerfile.py +0 -0
  110. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/package_node.py +0 -0
  111. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/package_python.py +0 -0
  112. {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/network/__init__.py +0 -0
  113. {testql-1.2.2 → testql-1.2.4}/testql/discovery/source.py +0 -0
  114. {testql-1.2.2 → testql-1.2.4}/testql/doql_parser.py +0 -0
  115. {testql-1.2.2 → testql-1.2.4}/testql/echo_schemas.py +0 -0
  116. {testql-1.2.2 → testql-1.2.4}/testql/endpoint_detector.py +0 -0
  117. {testql-1.2.2 → testql-1.2.4}/testql/generator.py +0 -0
  118. {testql-1.2.2 → testql-1.2.4}/testql/generators/__init__.py +0 -0
  119. {testql-1.2.2 → testql-1.2.4}/testql/generators/analyzers.py +0 -0
  120. {testql-1.2.2 → testql-1.2.4}/testql/generators/base.py +0 -0
  121. {testql-1.2.2 → testql-1.2.4}/testql/generators/convenience.py +0 -0
  122. {testql-1.2.2 → testql-1.2.4}/testql/generators/generators.py +0 -0
  123. {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/__init__.py +0 -0
  124. {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/coverage_optimizer.py +0 -0
  125. {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/edge_case_generator.py +0 -0
  126. {testql-1.2.2 → testql-1.2.4}/testql/generators/multi.py +0 -0
  127. {testql-1.2.2 → testql-1.2.4}/testql/generators/pipeline.py +0 -0
  128. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/__init__.py +0 -0
  129. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/base.py +0 -0
  130. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/graphql_source.py +0 -0
  131. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/nl_source.py +0 -0
  132. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/openapi_source.py +0 -0
  133. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/proto_source.py +0 -0
  134. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/sql_source.py +0 -0
  135. {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/ui_source.py +0 -0
  136. {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/__init__.py +0 -0
  137. {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/base.py +0 -0
  138. {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/nl_target.py +0 -0
  139. {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/pytest_target.py +0 -0
  140. {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/testtoon_target.py +0 -0
  141. {testql-1.2.2 → testql-1.2.4}/testql/generators/test_generator.py +0 -0
  142. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/__init__.py +0 -0
  143. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_api_runner.py +0 -0
  144. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_assertions.py +0 -0
  145. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_converter.py +0 -0
  146. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_encoder.py +0 -0
  147. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_flow.py +0 -0
  148. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_gui.py +0 -0
  149. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_parser.py +0 -0
  150. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_shell.py +0 -0
  151. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_testtoon_parser.py +0 -0
  152. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_unit.py +0 -0
  153. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_websockets.py +0 -0
  154. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/__init__.py +0 -0
  155. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/core.py +0 -0
  156. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/dispatcher.py +0 -0
  157. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/__init__.py +0 -0
  158. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/api.py +0 -0
  159. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/assertions.py +0 -0
  160. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/encoder.py +0 -0
  161. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/flow.py +0 -0
  162. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/include.py +0 -0
  163. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/navigate.py +0 -0
  164. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/record.py +0 -0
  165. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/select.py +0 -0
  166. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/unknown.py +0 -0
  167. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/wait.py +0 -0
  168. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/models.py +0 -0
  169. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/parsers.py +0 -0
  170. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/renderer.py +0 -0
  171. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/dispatcher.py +0 -0
  172. {testql-1.2.2 → testql-1.2.4}/testql/interpreter/interpreter.py +0 -0
  173. {testql-1.2.2 → testql-1.2.4}/testql/interpreter.py +0 -0
  174. {testql-1.2.2 → testql-1.2.4}/testql/ir/__init__.py +0 -0
  175. {testql-1.2.2 → testql-1.2.4}/testql/ir/assertions.py +0 -0
  176. {testql-1.2.2 → testql-1.2.4}/testql/ir/captures.py +0 -0
  177. {testql-1.2.2 → testql-1.2.4}/testql/ir/fixtures.py +0 -0
  178. {testql-1.2.2 → testql-1.2.4}/testql/ir/metadata.py +0 -0
  179. {testql-1.2.2 → testql-1.2.4}/testql/ir/plan.py +0 -0
  180. {testql-1.2.2 → testql-1.2.4}/testql/ir/steps.py +0 -0
  181. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/__init__.py +0 -0
  182. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/assertion_eval.py +0 -0
  183. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/context.py +0 -0
  184. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/engine.py +0 -0
  185. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/__init__.py +0 -0
  186. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/api.py +0 -0
  187. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/base.py +0 -0
  188. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/encoder.py +0 -0
  189. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/graphql.py +0 -0
  190. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/gui.py +0 -0
  191. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/nl.py +0 -0
  192. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/proto.py +0 -0
  193. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/shell.py +0 -0
  194. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/sql.py +0 -0
  195. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/unit.py +0 -0
  196. {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/interpolation.py +0 -0
  197. {testql-1.2.2 → testql-1.2.4}/testql/meta/__init__.py +0 -0
  198. {testql-1.2.2 → testql-1.2.4}/testql/meta/confidence_scorer.py +0 -0
  199. {testql-1.2.2 → testql-1.2.4}/testql/meta/coverage_analyzer.py +0 -0
  200. {testql-1.2.2 → testql-1.2.4}/testql/meta/mutator.py +0 -0
  201. {testql-1.2.2 → testql-1.2.4}/testql/meta/self_test.py +0 -0
  202. {testql-1.2.2 → testql-1.2.4}/testql/openapi_generator.py +0 -0
  203. {testql-1.2.2 → testql-1.2.4}/testql/report_generator.py +0 -0
  204. {testql-1.2.2 → testql-1.2.4}/testql/reporters/__init__.py +0 -0
  205. {testql-1.2.2 → testql-1.2.4}/testql/reporters/console.py +0 -0
  206. {testql-1.2.2 → testql-1.2.4}/testql/reporters/json_reporter.py +0 -0
  207. {testql-1.2.2 → testql-1.2.4}/testql/reporters/junit.py +0 -0
  208. {testql-1.2.2 → testql-1.2.4}/testql/results/__init__.py +0 -0
  209. {testql-1.2.2 → testql-1.2.4}/testql/results/artifacts.py +0 -0
  210. {testql-1.2.2 → testql-1.2.4}/testql/results/models.py +0 -0
  211. {testql-1.2.2 → testql-1.2.4}/testql/results/serializers.py +0 -0
  212. {testql-1.2.2 → testql-1.2.4}/testql/runner.py +0 -0
  213. {testql-1.2.2 → testql-1.2.4}/testql/runners/__init__.py +0 -0
  214. {testql-1.2.2 → testql-1.2.4}/testql/sumd_generator.py +0 -0
  215. {testql-1.2.2 → testql-1.2.4}/testql/sumd_parser.py +0 -0
  216. {testql-1.2.2 → testql-1.2.4}/testql/toon_parser.py +0 -0
  217. {testql-1.2.2 → testql-1.2.4}/testql/topology/models.py +0 -0
  218. {testql-1.2.2 → testql-1.2.4}/testql/topology/serializers.py +0 -0
  219. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/dependency_links.txt +0 -0
  220. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/entry_points.txt +0 -0
  221. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/requires.txt +0 -0
  222. {testql-1.2.2 → testql-1.2.4}/testql.egg-info/top_level.txt +0 -0
  223. {testql-1.2.2 → testql-1.2.4}/tests/test_adapters_base.py +0 -0
  224. {testql-1.2.2 → testql-1.2.4}/tests/test_api_handler.py +0 -0
  225. {testql-1.2.2 → testql-1.2.4}/tests/test_cli.py +0 -0
  226. {testql-1.2.2 → testql-1.2.4}/tests/test_converter.py +0 -0
  227. {testql-1.2.2 → testql-1.2.4}/tests/test_converter_handlers.py +0 -0
  228. {testql-1.2.2 → testql-1.2.4}/tests/test_detectors.py +0 -0
  229. {testql-1.2.2 → testql-1.2.4}/tests/test_discovery.py +0 -0
  230. {testql-1.2.2 → testql-1.2.4}/tests/test_dispatcher.py +0 -0
  231. {testql-1.2.2 → testql-1.2.4}/tests/test_doql_parser_sumd_gen.py +0 -0
  232. {testql-1.2.2 → testql-1.2.4}/tests/test_echo.py +0 -0
  233. {testql-1.2.2 → testql-1.2.4}/tests/test_echo_doql_parser.py +0 -0
  234. {testql-1.2.2 → testql-1.2.4}/tests/test_echo_schemas_helpers.py +0 -0
  235. {testql-1.2.2 → testql-1.2.4}/tests/test_encoder_routes.py +0 -0
  236. {testql-1.2.2 → testql-1.2.4}/tests/test_generate_ir_cli.py +0 -0
  237. {testql-1.2.2 → testql-1.2.4}/tests/test_generators.py +0 -0
  238. {testql-1.2.2 → testql-1.2.4}/tests/test_graphql_adapter.py +0 -0
  239. {testql-1.2.2 → testql-1.2.4}/tests/test_gui_execution.py +0 -0
  240. {testql-1.2.2 → testql-1.2.4}/tests/test_interpreter.py +0 -0
  241. {testql-1.2.2 → testql-1.2.4}/tests/test_ir.py +0 -0
  242. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_captures.py +0 -0
  243. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_assertion_eval.py +0 -0
  244. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_captures.py +0 -0
  245. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_engine.py +0 -0
  246. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_executors.py +0 -0
  247. {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_interpolation.py +0 -0
  248. {testql-1.2.2 → testql-1.2.4}/tests/test_meta_confidence.py +0 -0
  249. {testql-1.2.2 → testql-1.2.4}/tests/test_meta_coverage.py +0 -0
  250. {testql-1.2.2 → testql-1.2.4}/tests/test_meta_mutator.py +0 -0
  251. {testql-1.2.2 → testql-1.2.4}/tests/test_meta_self_test.py +0 -0
  252. {testql-1.2.2 → testql-1.2.4}/tests/test_misc_cmds.py +0 -0
  253. {testql-1.2.2 → testql-1.2.4}/tests/test_nl_adapter.py +0 -0
  254. {testql-1.2.2 → testql-1.2.4}/tests/test_nl_entity_extractor.py +0 -0
  255. {testql-1.2.2 → testql-1.2.4}/tests/test_nl_grammar.py +0 -0
  256. {testql-1.2.2 → testql-1.2.4}/tests/test_nl_intent_recognizer.py +0 -0
  257. {testql-1.2.2 → testql-1.2.4}/tests/test_nl_scenarios_e2e.py +0 -0
  258. {testql-1.2.2 → testql-1.2.4}/tests/test_openapi_generator.py +0 -0
  259. {testql-1.2.2 → testql-1.2.4}/tests/test_pipeline.py +0 -0
  260. {testql-1.2.2 → testql-1.2.4}/tests/test_proto_adapter.py +0 -0
  261. {testql-1.2.2 → testql-1.2.4}/tests/test_proto_compatibility.py +0 -0
  262. {testql-1.2.2 → testql-1.2.4}/tests/test_proto_descriptor_loader.py +0 -0
  263. {testql-1.2.2 → testql-1.2.4}/tests/test_proto_graphql_scenarios_e2e.py +0 -0
  264. {testql-1.2.2 → testql-1.2.4}/tests/test_proto_message_validator.py +0 -0
  265. {testql-1.2.2 → testql-1.2.4}/tests/test_report_generator.py +0 -0
  266. {testql-1.2.2 → testql-1.2.4}/tests/test_reporters.py +0 -0
  267. {testql-1.2.2 → testql-1.2.4}/tests/test_run_ir_cli.py +0 -0
  268. {testql-1.2.2 → testql-1.2.4}/tests/test_runner.py +0 -0
  269. {testql-1.2.2 → testql-1.2.4}/tests/test_shell_execution.py +0 -0
  270. {testql-1.2.2 → testql-1.2.4}/tests/test_sources.py +0 -0
  271. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_adapter.py +0 -0
  272. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_ddl_parser.py +0 -0
  273. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_dialect_resolver.py +0 -0
  274. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_fixtures.py +0 -0
  275. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_query_parser.py +0 -0
  276. {testql-1.2.2 → testql-1.2.4}/tests/test_sql_scenarios_e2e.py +0 -0
  277. {testql-1.2.2 → testql-1.2.4}/tests/test_suite_cmd_helpers.py +0 -0
  278. {testql-1.2.2 → testql-1.2.4}/tests/test_suite_execution.py +0 -0
  279. {testql-1.2.2 → testql-1.2.4}/tests/test_suite_listing.py +0 -0
  280. {testql-1.2.2 → testql-1.2.4}/tests/test_sumd_parser.py +0 -0
  281. {testql-1.2.2 → testql-1.2.4}/tests/test_targets.py +0 -0
  282. {testql-1.2.2 → testql-1.2.4}/tests/test_test_generator.py +0 -0
  283. {testql-1.2.2 → testql-1.2.4}/tests/test_testtoon_adapter.py +0 -0
  284. {testql-1.2.2 → testql-1.2.4}/tests/test_toon_parser.py +0 -0
  285. {testql-1.2.2 → testql-1.2.4}/tests/test_topology.py +0 -0
  286. {testql-1.2.2 → testql-1.2.4}/tests/test_unit_execution.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testql
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: TestQL — Multi-DSL Test Platform: TestTOON / NL / SQL / Proto / GraphQL adapters with Unified IR, generator engine, and meta-testing
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -40,11 +40,11 @@ Dynamic: license-file
40
40
 
41
41
  ## AI Cost Tracking
42
42
 
43
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-1.2.2-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
43
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-1.2.4-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
44
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
45
45
 
46
- - 🤖 **LLM usage:** $6.6000 (44 commits)
47
- - 👤 **Human dev:** ~$2786 (27.9h @ $100/h, 30min dedup)
46
+ - 🤖 **LLM usage:** $6.7500 (45 commits)
47
+ - 👤 **Human dev:** ~$2791 (27.9h @ $100/h, 30min dedup)
48
48
 
49
49
  Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
50
50
 
@@ -52,7 +52,7 @@ Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrou
52
52
 
53
53
  ## AI Cost Tracking
54
54
 
55
- ![PyPI](https://img.shields.io/badge/pypi-testql-blue) ![Version](https://img.shields.io/badge/version-1.2.2-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
+ ![PyPI](https://img.shields.io/badge/pypi-testql-blue) ![Version](https://img.shields.io/badge/version-1.2.4-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
56
56
 
57
57
  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.
58
58
 
@@ -150,11 +150,22 @@ Example:
150
150
  examples/web-inspection-dot-testql/run.sh https://tom.sapletta.com/
151
151
  ```
152
152
 
153
+ Current capabilities:
154
+
155
+ - **Asset classification**: script, stylesheet, image, icon, preload, link.
156
+ - **Bounded link validation**: HEAD checks for all internal links (up to 100).
157
+ - **Bounded asset validation**: HEAD checks for all extracted assets.
158
+ - **Broken resource detection**: assets or links returning error status are flagged as findings.
159
+ - **Bounded sitemap crawl**: fetches up to 10 internal subpages, extracts titles and link counts, adds `subpage` nodes to the topology.
160
+ - **Sitemap checks**: crawl coverage, broken subpage detection, duplicate title warnings.
161
+ - **Playwright browser inspection** (`--browser`): renders the page in a headless browser, captures console errors, network calls (REST/GraphQL/WebSocket), and JS-rendered DOM.
162
+ - **Browser checks**: render detection, console error count, network call capture.
163
+
153
164
  Current limitations:
154
165
 
155
166
  - Browser execution is not yet Playwright-backed.
156
167
  - JavaScript-rendered DOM is not evaluated yet.
157
- - Links/assets are extracted but not individually fetched and validated yet.
168
+ - Per-resource validation uses HEAD requests only; full page content is not fetched for linked pages.
158
169
  - Console errors, screenshots, performance, accessibility, REST/GraphQL/WebSocket network logs, and auth flows are planned next.
159
170
 
160
171
  ## API Endpoint Detection
@@ -3,11 +3,11 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-1.2.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-$6.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-1.2.4-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-$6.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $6.6000 (44 commits)
10
- - 👤 **Human dev:** ~$2786 (27.9h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $6.7500 (45 commits)
10
+ - 👤 **Human dev:** ~$2791 (27.9h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
@@ -15,7 +15,7 @@ Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrou
15
15
 
16
16
  ## AI Cost Tracking
17
17
 
18
- ![PyPI](https://img.shields.io/badge/pypi-testql-blue) ![Version](https://img.shields.io/badge/version-1.2.2-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
18
+ ![PyPI](https://img.shields.io/badge/pypi-testql-blue) ![Version](https://img.shields.io/badge/version-1.2.4-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
19
19
 
20
20
  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.
21
21
 
@@ -113,11 +113,22 @@ Example:
113
113
  examples/web-inspection-dot-testql/run.sh https://tom.sapletta.com/
114
114
  ```
115
115
 
116
+ Current capabilities:
117
+
118
+ - **Asset classification**: script, stylesheet, image, icon, preload, link.
119
+ - **Bounded link validation**: HEAD checks for all internal links (up to 100).
120
+ - **Bounded asset validation**: HEAD checks for all extracted assets.
121
+ - **Broken resource detection**: assets or links returning error status are flagged as findings.
122
+ - **Bounded sitemap crawl**: fetches up to 10 internal subpages, extracts titles and link counts, adds `subpage` nodes to the topology.
123
+ - **Sitemap checks**: crawl coverage, broken subpage detection, duplicate title warnings.
124
+ - **Playwright browser inspection** (`--browser`): renders the page in a headless browser, captures console errors, network calls (REST/GraphQL/WebSocket), and JS-rendered DOM.
125
+ - **Browser checks**: render detection, console error count, network call capture.
126
+
116
127
  Current limitations:
117
128
 
118
129
  - Browser execution is not yet Playwright-backed.
119
130
  - JavaScript-rendered DOM is not evaluated yet.
120
- - Links/assets are extracted but not individually fetched and validated yet.
131
+ - Per-resource validation uses HEAD requests only; full page content is not fetched for linked pages.
121
132
  - Console errors, screenshots, performance, accessibility, REST/GraphQL/WebSocket network logs, and auth flows are planned next.
122
133
 
123
134
  ## API Endpoint Detection
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "testql"
7
- version = "1.2.2"
7
+ version = "1.2.4"
8
8
  description = "TestQL — Multi-DSL Test Platform: TestTOON / NL / SQL / Proto / GraphQL adapters with Unified IR, generator engine, and meta-testing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -6,4 +6,4 @@ external artifacts into IR; meta-testing (`testql.meta`) analyses the
6
6
  generated plans for coverage, confidence and mutation resilience.
7
7
  """
8
8
 
9
- __version__ = "1.2.2"
9
+ __version__ = "1.2.4"
@@ -37,7 +37,7 @@ from testql.interpreter._testtoon_parser import (
37
37
  ToonSection,
38
38
  parse_testtoon as _parse_testtoon,
39
39
  )
40
- from testql.ir import Assertion, Fixture, ScenarioMetadata, SqlStep, Step, TestPlan
40
+ from testql.ir import Assertion, Capture, Fixture, ScenarioMetadata, SqlStep, Step, TestPlan
41
41
 
42
42
  from ..base import BaseDSLAdapter, DSLDetectionResult, SourceLike, read_source
43
43
  from .dialect_resolver import DEFAULT_DIALECT, normalize_dialect
@@ -170,11 +170,25 @@ def _h_assert(section: ToonSection, plan: TestPlan,
170
170
  plan.steps.extend(_assert_section(section, {s.name: s for s in sql_steps if s.name}))
171
171
 
172
172
 
173
+ def _h_capture(section: ToonSection, plan: TestPlan,
174
+ sql_steps: list[SqlStep], dialect: str) -> None:
175
+ """Attach `Capture`s to SqlSteps by `query` name (matches assert lookup style)."""
176
+ by_name = {s.name: s for s in sql_steps if s.name}
177
+ for row in section.rows:
178
+ target = str(row.get("query", "") or row.get("step", "")).strip()
179
+ var = str(row.get("var", "")).strip()
180
+ from_path = str(row.get("from", "")).strip()
181
+ owner = by_name.get(target)
182
+ if owner is not None and var and from_path:
183
+ owner.captures.append(Capture(var_name=var, from_path=from_path))
184
+
185
+
173
186
  _SECTION_HANDLERS = {
174
187
  "CONFIG": _h_config,
175
188
  "SCHEMA": _h_schema,
176
189
  "QUERY": _h_query,
177
190
  "ASSERT": _h_assert,
191
+ "CAPTURE": _h_capture,
178
192
  }
179
193
 
180
194
 
@@ -246,6 +260,22 @@ def _render_asserts(plan: TestPlan) -> list[str]:
246
260
  return lines
247
261
 
248
262
 
263
+ def _render_captures(plan: TestPlan) -> list[str]:
264
+ """Emit `CAPTURE[N]{query, var, from}` rows referencing the SqlStep name."""
265
+ rows: list[tuple[str, str, str]] = []
266
+ for s in plan.steps:
267
+ if not isinstance(s, SqlStep) or not s.name:
268
+ continue
269
+ for c in s.captures:
270
+ rows.append((s.name, c.var_name, c.from_path))
271
+ if not rows:
272
+ return []
273
+ lines = [f"CAPTURE[{len(rows)}]" + "{query, var, from}:"]
274
+ for q, var, frm in rows:
275
+ lines.append(f" {q}, {var}, {frm}")
276
+ return lines
277
+
278
+
249
279
  def _render_plan(plan: TestPlan) -> str:
250
280
  parts = _render_meta(plan.metadata)
251
281
  if parts:
@@ -254,6 +284,7 @@ def _render_plan(plan: TestPlan) -> str:
254
284
  parts.extend(_render_schema(plan))
255
285
  parts.extend(_render_queries(plan))
256
286
  parts.extend(_render_asserts(plan))
287
+ parts.extend(_render_captures(plan))
257
288
  return "\n".join(parts) + ("\n" if parts else "")
258
289
 
259
290
 
@@ -20,6 +20,7 @@ from testql.interpreter._testtoon_parser import (
20
20
  from testql.ir import (
21
21
  ApiStep,
22
22
  Assertion,
23
+ Capture,
23
24
  EncoderStep,
24
25
  GuiStep,
25
26
  ScenarioMetadata,
@@ -46,7 +47,9 @@ def _api_section_to_steps(section: ToonSection) -> list[Step]:
46
47
  ak, av = row.get("assert_key"), row.get("assert_value") or row.get("assert_val")
47
48
  if ak and av:
48
49
  asserts.append(Assertion(field=str(ak), op="==", expected=av))
50
+ name = str(row.get("name", "")).strip() or None
49
51
  steps.append(ApiStep(
52
+ name=name,
50
53
  method=str(row.get("method", "GET")).upper(),
51
54
  path=str(row.get("endpoint", "/")),
52
55
  expect_status=int(status) if isinstance(status, (int, str)) and str(status).isdigit() else None,
@@ -95,6 +98,36 @@ def _assert_section_to_steps(section: ToonSection) -> list[Step]:
95
98
  return [Step(kind="assert", name="ASSERT", asserts=asserts)]
96
99
 
97
100
 
101
+ def _capture_section_apply(section: ToonSection, plan: TestPlan) -> None:
102
+ """`CAPTURE[N]{step, var, from}` rows attach `Capture`s to existing plan steps.
103
+
104
+ `step` matches `Step.name` if non-numeric; otherwise it is treated as a
105
+ 1-based index into `plan.steps`. Unresolved references are silently dropped
106
+ (mirrors the orphan-assert behaviour of `_assert_section_to_steps`).
107
+ """
108
+ by_name = {s.name: s for s in plan.steps if s.name}
109
+ for row in section.rows:
110
+ target = str(row.get("step", "")).strip()
111
+ var_name = str(row.get("var", "")).strip()
112
+ from_path = str(row.get("from", "")).strip()
113
+ if not (target and var_name and from_path):
114
+ continue
115
+ owner = _resolve_capture_target(target, by_name, plan.steps)
116
+ if owner is not None:
117
+ owner.captures.append(Capture(var_name=var_name, from_path=from_path))
118
+
119
+
120
+ def _resolve_capture_target(target: str, by_name: dict[str, Step],
121
+ steps: list[Step]) -> Step | None:
122
+ if target in by_name:
123
+ return by_name[target]
124
+ if target.isdigit():
125
+ idx = int(target) - 1
126
+ if 0 <= idx < len(steps):
127
+ return steps[idx]
128
+ return None
129
+
130
+
98
131
  def _generic_section_to_steps(section: ToonSection) -> list[Step]:
99
132
  """Fallback for section types we don't yet model in the IR (FLOW, OQL, ...)."""
100
133
  steps: list[Step] = []
@@ -129,10 +162,16 @@ def _toon_to_plan(toon: ToonScript) -> TestPlan:
129
162
  extra={k: v for k, v in toon.meta.items() if k not in {"scenario", "type", "version", "lang"}},
130
163
  )
131
164
  plan = TestPlan(metadata=md)
165
+ capture_sections: list[ToonSection] = []
132
166
  for section in toon.sections:
167
+ if section.type == "CAPTURE":
168
+ capture_sections.append(section) # apply after all steps are loaded
169
+ continue
133
170
  steps, cfg = _translate_section(section)
134
171
  plan.steps.extend(steps)
135
172
  plan.config.update(cfg)
173
+ for section in capture_sections:
174
+ _capture_section_apply(section, plan)
136
175
  return plan
137
176
 
138
177
 
@@ -204,6 +243,24 @@ def _render_assertions(steps: list[Step]) -> list[str]:
204
243
  return lines
205
244
 
206
245
 
246
+ def _render_captures(steps: list[Step]) -> list[str]:
247
+ """Emit a `CAPTURE[N]{step, var, from}` section for any step with captures.
248
+
249
+ Uses the 1-based step index so round-trip is lossless even when the API
250
+ renderer (which has fixed columns) doesn't emit step names.
251
+ """
252
+ rows: list[tuple[str, str, str]] = []
253
+ for idx, step in enumerate(steps, start=1):
254
+ for c in step.captures:
255
+ rows.append((str(idx), c.var_name, c.from_path))
256
+ if not rows:
257
+ return []
258
+ lines = [f"CAPTURE[{len(rows)}]" + "{step, var, from}:"]
259
+ for step_ref, var, frm in rows:
260
+ lines.append(f" {step_ref}, {var}, {frm}")
261
+ return lines
262
+
263
+
207
264
  def _render_plan(plan: TestPlan) -> str:
208
265
  """Lossy renderer covering CONFIG / API / NAVIGATE / ENCODER / ASSERT.
209
266
 
@@ -223,6 +280,7 @@ def _render_plan(plan: TestPlan) -> str:
223
280
  encoder_steps = [s for s in plan.steps if isinstance(s, EncoderStep)]
224
281
  parts.extend(_render_encoder_steps(encoder_steps))
225
282
  parts.extend(_render_assertions(plan.steps))
283
+ parts.extend(_render_captures(plan.steps))
226
284
  return "\n".join(parts) + ("\n" if parts else "")
227
285
 
228
286
 
@@ -8,6 +8,7 @@ from testql.commands.discover_cmd import discover
8
8
  from testql.commands.endpoints_cmd import endpoints, openapi
9
9
  from testql.commands.generate_cmd import analyze, generate
10
10
  from testql.commands.generate_ir_cmd import generate_ir
11
+ from testql.commands.generate_topology_cmd import generate_topology
11
12
  from testql.commands.inspect_cmd import inspect
12
13
  from testql.commands.misc_cmds import create, echo, from_sumd, init, report, watch
13
14
  from testql.commands.run_cmd import run
@@ -27,6 +28,7 @@ def cli():
27
28
  cli.add_command(run)
28
29
  cli.add_command(run_ir)
29
30
  cli.add_command(generate)
31
+ cli.add_command(generate_topology)
30
32
  cli.add_command(generate_ir)
31
33
  cli.add_command(discover)
32
34
  cli.add_command(topology)
@@ -4,6 +4,7 @@ from testql.commands.discover_cmd import discover
4
4
  from testql.commands.echo import echo
5
5
  from testql.commands.endpoints_cmd import endpoints, openapi
6
6
  from testql.commands.generate_cmd import analyze, generate
7
+ from testql.commands.generate_topology_cmd import generate_topology
7
8
  from testql.commands.inspect_cmd import inspect
8
9
  from testql.commands.misc_cmds import create
9
10
  from testql.commands.misc_cmds import echo as cli_echo
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "openapi",
23
24
  "analyze",
24
25
  "generate",
26
+ "generate_topology",
25
27
  "create",
26
28
  "from_sumd",
27
29
  "init",
@@ -7,75 +7,88 @@ from pathlib import Path
7
7
 
8
8
  import click
9
9
 
10
+ from testql.pipeline import GenerationPipeline
11
+
10
12
 
11
13
  def _is_workspace(target_path: Path) -> bool:
12
- """True only when target_path is a monorepo workspace, not a single Python project."""
13
- # If it has its own pyproject.toml, it's a standalone project
14
- if (target_path / "pyproject.toml").exists() or (target_path / "setup.py").exists():
15
- return False
16
- workspace_dirs = ["doql", "oql", "oqlos", "testql", "weboql", "www"]
17
- return any(
18
- (target_path / d).exists() and not (target_path / d / "__init__.py").exists()
19
- for d in workspace_dirs
20
- )
14
+ """Backward-compat re-export delegates to GenerationPipeline."""
15
+ return GenerationPipeline._is_workspace(target_path) # type: ignore[arg-type]
21
16
 
22
17
 
23
- @click.command()
24
- @click.argument("path", type=click.Path(exists=True), default=".")
25
- @click.option("--output-dir", "-o", help="Output directory for generated tests")
26
- @click.option("--analyze-only", is_flag=True, help="Only analyze, don't generate")
27
- @click.option("--format", "fmt", type=click.Choice(["testql", "json"]), default="testql")
28
- def generate(path: str, output_dir: str | None, analyze_only: bool, fmt: str) -> None:
29
- """Generate TestQL scenarios from project structure."""
30
- from testql.generator import TestGenerator, MultiProjectTestGenerator
31
-
32
- target_path = Path(path)
33
-
34
- if _is_workspace(target_path):
18
+ def _echo_analysis(ctx, target_path: Path) -> None:
19
+ if ctx.is_workspace:
35
20
  click.echo(f"🔄 Analyzing workspace: {target_path}")
36
- gen = MultiProjectTestGenerator(target_path)
37
- profiles = gen.analyze_all()
38
-
21
+ profiles = ctx.workspace_profiles
39
22
  click.echo(f"📊 Discovered {len(profiles)} projects:")
40
23
  for name, profile in profiles.items():
41
24
  click.echo(f" • {name}: {profile.project_type}")
42
25
  click.echo(f" - Test patterns: {len(profile.test_patterns)}")
43
26
  click.echo(f" - Config files: {len(profile.config)}")
44
-
45
- if analyze_only:
46
- return
47
-
48
- results = gen.generate_all()
49
- total = sum(len(files) for files in results.values())
50
- click.echo(f"\n✅ Generated {total} test files across {len(results)} projects")
51
-
52
- cross_file = gen.generate_cross_project_tests(target_path / "testql-scenarios")
53
- click.echo(f"🌐 Cross-project tests: {cross_file}")
54
-
55
27
  else:
56
28
  click.echo(f"🔄 Analyzing project: {target_path}")
57
- gen = TestGenerator(target_path)
58
- profile = gen.analyze()
59
- if profile is None:
60
- profile = gen.profile
61
-
29
+ profile = ctx.profile
62
30
  click.echo(f"📊 Project type: {profile.project_type}")
63
31
  click.echo(f"📊 Test patterns: {len(profile.test_patterns)}")
64
32
  click.echo(f"📊 Discovered files: {sum(len(v) for v in profile.discovered_files.values())}")
65
33
 
66
- if analyze_only:
67
- return
68
-
69
- out_dir = Path(output_dir) if output_dir else None
70
- generated = gen.generate_tests(out_dir)
71
34
 
35
+ def _echo_generation(ctx, generated: list[Path]) -> None:
36
+ if ctx.is_workspace:
37
+ click.echo(f"\n✅ Generated {len(generated)} test files")
38
+ else:
72
39
  click.echo(f"\n✅ Generated {len(generated)} test files:")
73
40
  for f in generated:
74
41
  click.echo(f" • {f}")
75
42
 
43
+
44
+ @click.command()
45
+ @click.argument("path", type=click.Path(exists=True), default=".")
46
+ @click.option("--output-dir", "-o", help="Output directory for generated tests")
47
+ @click.option("--analyze-only", is_flag=True, help="Only analyze, don't generate")
48
+ @click.option("--format", "fmt", type=click.Choice(["testql", "json"]), default="testql")
49
+ @click.option("--to-ir", is_flag=True, help="Parse generated TestTOON into IR JSON")
50
+ def generate(path: str, output_dir: str | None, analyze_only: bool, fmt: str, to_ir: bool) -> None:
51
+ """Generate TestQL scenarios from project structure."""
52
+ from testql.pipeline import GenerationPipeline
53
+
54
+ target_path = Path(path)
55
+ pipeline = GenerationPipeline(target_path)
56
+ ctx = pipeline._collect()
57
+ _echo_analysis(ctx, target_path)
58
+
59
+ if analyze_only:
60
+ return
61
+
62
+ out_dir = Path(output_dir) if output_dir else None
63
+ generated = pipeline.run(output_dir=out_dir, analyze_only=False)
64
+ _echo_generation(ctx, generated)
65
+
66
+ if to_ir and generated:
67
+ _emit_ir_json(generated, fmt)
68
+
76
69
  sys.exit(0)
77
70
 
78
71
 
72
+ def _emit_ir_json(paths: list[Path], fmt: str) -> None:
73
+ """Parse each generated TestTOON file into IR and re-emit as JSON."""
74
+ import json
75
+ from testql.adapters.testtoon_adapter import TestToonAdapter
76
+
77
+ adapter = TestToonAdapter()
78
+ for p in paths:
79
+ if not str(p).endswith(".testql.toon.yaml"):
80
+ continue
81
+ try:
82
+ plan = adapter.parse(str(p))
83
+ ir_path = p.with_suffix("").with_suffix(".ir.json")
84
+ ir_path.write_text(
85
+ json.dumps(plan.to_dict(), indent=2, default=str), encoding="utf-8"
86
+ )
87
+ click.echo(f" 📦 IR: {ir_path}")
88
+ except Exception as exc:
89
+ click.echo(f" ⚠️ IR conversion failed for {p}: {exc}")
90
+
91
+
79
92
  @click.command()
80
93
  @click.argument("path", type=click.Path(exists=True), default=".")
81
94
  def analyze(path: str) -> None:
@@ -0,0 +1,58 @@
1
+ """CLI: testql generate-topology — convert topology traces to executable scenarios."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from testql.topology import build_topology
11
+ from testql.topology.generator import TopologyScenarioGenerator
12
+
13
+
14
+ @click.command(name="generate-topology")
15
+ @click.argument("source", type=click.Path(exists=True), default=".")
16
+ @click.option("--trace-id", "-t", help="Trace ID to convert (default: first trace)")
17
+ @click.option("--output", "-o", type=click.Path(), help="Output file path")
18
+ @click.option("--format", "fmt", type=click.Choice(["testtoon", "ir-json"]), default="testtoon")
19
+ @click.option("--scan-network", is_flag=True, help="Include live network scanning")
20
+ def generate_topology(source: str, trace_id: str | None, output: str | None, fmt: str, scan_network: bool) -> None:
21
+ """Generate an executable scenario from a topology trace."""
22
+ import json
23
+
24
+ topology = build_topology(source, scan_network=scan_network)
25
+
26
+ trace = _pick_trace(topology, trace_id)
27
+ if trace is None:
28
+ click.echo(f"❌ No trace found (requested: {trace_id or 'first'})")
29
+ sys.exit(1)
30
+
31
+ gen = TopologyScenarioGenerator(topology)
32
+ plan = gen.from_trace(trace)
33
+
34
+ if fmt == "testtoon":
35
+ content = gen.to_testtoon(plan)
36
+ else:
37
+ content = json.dumps(plan.to_dict(), indent=2, default=str) + "\n"
38
+
39
+ if output:
40
+ out_path = Path(output)
41
+ out_path.parent.mkdir(parents=True, exist_ok=True)
42
+ out_path.write_text(content, encoding="utf-8")
43
+ click.echo(f"✅ Written {out_path}")
44
+ else:
45
+ click.echo(content, nl=False)
46
+
47
+ sys.exit(0)
48
+
49
+
50
+ def _pick_trace(topology, trace_id: str | None):
51
+ if not topology.traces:
52
+ return None
53
+ if trace_id is None:
54
+ return topology.traces[0]
55
+ return next((t for t in topology.traces if t.id == trace_id), None)
56
+
57
+
58
+ __all__ = ["generate_topology"]
@@ -12,12 +12,13 @@ from testql.results import inspect_source, render_inspection, render_refactor_pl
12
12
  @click.option("--format", "fmt", type=click.Choice(["toon", "yaml", "json", "nlp"]), default="toon")
13
13
  @click.option("--artifact", type=click.Choice(["inspection", "result", "refactor-plan"]), default="inspection")
14
14
  @click.option("--scan-network", is_flag=True, help="Enable network probes for URL sources")
15
+ @click.option("--browser", is_flag=True, help="Enable Playwright browser probe for URL sources")
15
16
  @click.option("--out-dir", type=click.Path(file_okay=False, dir_okay=True), default=None, help="Write full inspection bundle, e.g. .testql")
16
- def inspect(source: str, fmt: str, artifact: str, scan_network: bool, out_dir: str | None) -> None:
17
+ def inspect(source: str, fmt: str, artifact: str, scan_network: bool, browser: bool, out_dir: str | None) -> None:
17
18
  source_path = Path(source)
18
19
  if not source.startswith(("http://", "https://")) and not source_path.exists():
19
20
  raise click.ClickException(f"source does not exist: {source}")
20
- topology, envelope, plan = inspect_source(source, scan_network=scan_network)
21
+ topology, envelope, plan = inspect_source(source, scan_network=scan_network, use_browser=browser)
21
22
  if out_dir:
22
23
  written = write_inspection_artifacts(topology, envelope, plan, out_dir)
23
24
  click.echo(f"wrote {len(written)} files to {out_dir}")
@@ -0,0 +1,7 @@
1
+ """Browser probes using Playwright."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .playwright_page import PlaywrightPageProbe
6
+
7
+ __all__ = ["PlaywrightPageProbe"]