testql 1.2.2__tar.gz → 1.2.5__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.5}/PKG-INFO +50 -10
  2. {testql-1.2.2 → testql-1.2.5}/README.md +49 -9
  3. {testql-1.2.2 → testql-1.2.5}/pyproject.toml +1 -1
  4. {testql-1.2.2 → testql-1.2.5}/testql/__init__.py +1 -1
  5. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/sql_adapter.py +32 -1
  6. {testql-1.2.2 → testql-1.2.5}/testql/adapters/testtoon_adapter.py +60 -0
  7. {testql-1.2.2 → testql-1.2.5}/testql/cli.py +2 -0
  8. {testql-1.2.2 → testql-1.2.5}/testql/commands/__init__.py +2 -0
  9. {testql-1.2.2 → testql-1.2.5}/testql/commands/generate_cmd.py +58 -45
  10. testql-1.2.5/testql/commands/generate_topology_cmd.py +58 -0
  11. {testql-1.2.2 → testql-1.2.5}/testql/commands/inspect_cmd.py +3 -2
  12. testql-1.2.5/testql/discovery/probes/browser/__init__.py +7 -0
  13. testql-1.2.5/testql/discovery/probes/browser/playwright_page.py +147 -0
  14. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/network/http_endpoint.py +19 -1
  15. {testql-1.2.2 → testql-1.2.5}/testql/discovery/registry.py +8 -5
  16. {testql-1.2.2 → testql-1.2.5}/testql/generators/base.py +1 -0
  17. {testql-1.2.2 → testql-1.2.5}/testql/generators/targets/testtoon_target.py +1 -0
  18. {testql-1.2.2 → testql-1.2.5}/testql/generators/test_generator.py +2 -0
  19. {testql-1.2.2 → testql-1.2.5}/testql/ir/plan.py +2 -0
  20. testql-1.2.5/testql/pipeline.py +135 -0
  21. {testql-1.2.2 → testql-1.2.5}/testql/report_generator.py +2 -0
  22. {testql-1.2.2 → testql-1.2.5}/testql/results/analyzer.py +222 -4
  23. {testql-1.2.2 → testql-1.2.5}/testql/topology/__init__.py +5 -0
  24. {testql-1.2.2 → testql-1.2.5}/testql/topology/builder.py +8 -4
  25. testql-1.2.5/testql/topology/generator.py +241 -0
  26. testql-1.2.5/testql/topology/sitemap.py +119 -0
  27. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/PKG-INFO +50 -10
  28. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/SOURCES.txt +9 -0
  29. testql-1.2.5/tests/test_adapter_capture_syntax.py +166 -0
  30. testql-1.2.5/tests/test_browser_discovery.py +110 -0
  31. {testql-1.2.2 → testql-1.2.5}/tests/test_generate_cmd.py +22 -28
  32. {testql-1.2.2 → testql-1.2.5}/tests/test_gui_execution.py +8 -2
  33. {testql-1.2.2 → testql-1.2.5}/tests/test_network_discovery.py +22 -0
  34. {testql-1.2.2 → testql-1.2.5}/tests/test_results.py +2 -2
  35. testql-1.2.5/tests/test_topology_generator.py +161 -0
  36. {testql-1.2.2 → testql-1.2.5}/LICENSE +0 -0
  37. {testql-1.2.2 → testql-1.2.5}/setup.cfg +0 -0
  38. {testql-1.2.2 → testql-1.2.5}/testql/__main__.py +0 -0
  39. {testql-1.2.2 → testql-1.2.5}/testql/_base_fallback.py +0 -0
  40. {testql-1.2.2 → testql-1.2.5}/testql/adapters/__init__.py +0 -0
  41. {testql-1.2.2 → testql-1.2.5}/testql/adapters/base.py +0 -0
  42. {testql-1.2.2 → testql-1.2.5}/testql/adapters/graphql/__init__.py +0 -0
  43. {testql-1.2.2 → testql-1.2.5}/testql/adapters/graphql/graphql_adapter.py +0 -0
  44. {testql-1.2.2 → testql-1.2.5}/testql/adapters/graphql/query_executor.py +0 -0
  45. {testql-1.2.2 → testql-1.2.5}/testql/adapters/graphql/schema_introspection.py +0 -0
  46. {testql-1.2.2 → testql-1.2.5}/testql/adapters/graphql/subscription_runner.py +0 -0
  47. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/__init__.py +0 -0
  48. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/entity_extractor.py +0 -0
  49. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/grammar.py +0 -0
  50. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/intent_recognizer.py +0 -0
  51. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/lexicon/__init__.py +0 -0
  52. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/llm_fallback.py +0 -0
  53. {testql-1.2.2 → testql-1.2.5}/testql/adapters/nl/nl_adapter.py +0 -0
  54. {testql-1.2.2 → testql-1.2.5}/testql/adapters/proto/__init__.py +0 -0
  55. {testql-1.2.2 → testql-1.2.5}/testql/adapters/proto/compatibility.py +0 -0
  56. {testql-1.2.2 → testql-1.2.5}/testql/adapters/proto/descriptor_loader.py +0 -0
  57. {testql-1.2.2 → testql-1.2.5}/testql/adapters/proto/message_validator.py +0 -0
  58. {testql-1.2.2 → testql-1.2.5}/testql/adapters/proto/proto_adapter.py +0 -0
  59. {testql-1.2.2 → testql-1.2.5}/testql/adapters/registry.py +0 -0
  60. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/__init__.py +0 -0
  61. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/ddl_parser.py +0 -0
  62. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/dialect_resolver.py +0 -0
  63. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/fixtures.py +0 -0
  64. {testql-1.2.2 → testql-1.2.5}/testql/adapters/sql/query_parser.py +0 -0
  65. {testql-1.2.2 → testql-1.2.5}/testql/base.py +0 -0
  66. {testql-1.2.2 → testql-1.2.5}/testql/commands/discover_cmd.py +0 -0
  67. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/__init__.py +0 -0
  68. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/cli.py +0 -0
  69. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/context.py +0 -0
  70. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/formatters/__init__.py +0 -0
  71. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/formatters/text.py +0 -0
  72. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/parsers/__init__.py +0 -0
  73. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/parsers/doql.py +0 -0
  74. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo/parsers/toon.py +0 -0
  75. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo.py +0 -0
  76. {testql-1.2.2 → testql-1.2.5}/testql/commands/echo_helpers.py +0 -0
  77. {testql-1.2.2 → testql-1.2.5}/testql/commands/encoder_routes.py +0 -0
  78. {testql-1.2.2 → testql-1.2.5}/testql/commands/endpoints_cmd.py +0 -0
  79. {testql-1.2.2 → testql-1.2.5}/testql/commands/generate_ir_cmd.py +0 -0
  80. {testql-1.2.2 → testql-1.2.5}/testql/commands/misc_cmds.py +0 -0
  81. {testql-1.2.2 → testql-1.2.5}/testql/commands/run_cmd.py +0 -0
  82. {testql-1.2.2 → testql-1.2.5}/testql/commands/run_ir_cmd.py +0 -0
  83. {testql-1.2.2 → testql-1.2.5}/testql/commands/self_test_cmd.py +0 -0
  84. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/__init__.py +0 -0
  85. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/cli.py +0 -0
  86. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/collection.py +0 -0
  87. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/execution.py +0 -0
  88. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/listing.py +0 -0
  89. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite/reports.py +0 -0
  90. {testql-1.2.2 → testql-1.2.5}/testql/commands/suite_cmd.py +0 -0
  91. {testql-1.2.2 → testql-1.2.5}/testql/commands/templates/__init__.py +0 -0
  92. {testql-1.2.2 → testql-1.2.5}/testql/commands/templates/content.py +0 -0
  93. {testql-1.2.2 → testql-1.2.5}/testql/commands/templates/templates.py +0 -0
  94. {testql-1.2.2 → testql-1.2.5}/testql/commands/topology_cmd.py +0 -0
  95. {testql-1.2.2 → testql-1.2.5}/testql/detectors/__init__.py +0 -0
  96. {testql-1.2.2 → testql-1.2.5}/testql/detectors/base.py +0 -0
  97. {testql-1.2.2 → testql-1.2.5}/testql/detectors/config_detector.py +0 -0
  98. {testql-1.2.2 → testql-1.2.5}/testql/detectors/django_detector.py +0 -0
  99. {testql-1.2.2 → testql-1.2.5}/testql/detectors/express_detector.py +0 -0
  100. {testql-1.2.2 → testql-1.2.5}/testql/detectors/fastapi_detector.py +0 -0
  101. {testql-1.2.2 → testql-1.2.5}/testql/detectors/flask_detector.py +0 -0
  102. {testql-1.2.2 → testql-1.2.5}/testql/detectors/graphql_detector.py +0 -0
  103. {testql-1.2.2 → testql-1.2.5}/testql/detectors/models.py +0 -0
  104. {testql-1.2.2 → testql-1.2.5}/testql/detectors/openapi_detector.py +0 -0
  105. {testql-1.2.2 → testql-1.2.5}/testql/detectors/test_detector.py +0 -0
  106. {testql-1.2.2 → testql-1.2.5}/testql/detectors/unified.py +0 -0
  107. {testql-1.2.2 → testql-1.2.5}/testql/detectors/websocket_detector.py +0 -0
  108. {testql-1.2.2 → testql-1.2.5}/testql/discovery/__init__.py +0 -0
  109. {testql-1.2.2 → testql-1.2.5}/testql/discovery/manifest.py +0 -0
  110. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/__init__.py +0 -0
  111. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/base.py +0 -0
  112. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/__init__.py +0 -0
  113. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/api_openapi.py +0 -0
  114. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/container_compose.py +0 -0
  115. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/container_dockerfile.py +0 -0
  116. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/package_node.py +0 -0
  117. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/filesystem/package_python.py +0 -0
  118. {testql-1.2.2 → testql-1.2.5}/testql/discovery/probes/network/__init__.py +0 -0
  119. {testql-1.2.2 → testql-1.2.5}/testql/discovery/source.py +0 -0
  120. {testql-1.2.2 → testql-1.2.5}/testql/doql_parser.py +0 -0
  121. {testql-1.2.2 → testql-1.2.5}/testql/echo_schemas.py +0 -0
  122. {testql-1.2.2 → testql-1.2.5}/testql/endpoint_detector.py +0 -0
  123. {testql-1.2.2 → testql-1.2.5}/testql/generator.py +0 -0
  124. {testql-1.2.2 → testql-1.2.5}/testql/generators/__init__.py +0 -0
  125. {testql-1.2.2 → testql-1.2.5}/testql/generators/analyzers.py +0 -0
  126. {testql-1.2.2 → testql-1.2.5}/testql/generators/convenience.py +0 -0
  127. {testql-1.2.2 → testql-1.2.5}/testql/generators/generators.py +0 -0
  128. {testql-1.2.2 → testql-1.2.5}/testql/generators/llm/__init__.py +0 -0
  129. {testql-1.2.2 → testql-1.2.5}/testql/generators/llm/coverage_optimizer.py +0 -0
  130. {testql-1.2.2 → testql-1.2.5}/testql/generators/llm/edge_case_generator.py +0 -0
  131. {testql-1.2.2 → testql-1.2.5}/testql/generators/multi.py +0 -0
  132. {testql-1.2.2 → testql-1.2.5}/testql/generators/pipeline.py +0 -0
  133. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/__init__.py +0 -0
  134. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/base.py +0 -0
  135. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/graphql_source.py +0 -0
  136. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/nl_source.py +0 -0
  137. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/openapi_source.py +0 -0
  138. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/proto_source.py +0 -0
  139. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/sql_source.py +0 -0
  140. {testql-1.2.2 → testql-1.2.5}/testql/generators/sources/ui_source.py +0 -0
  141. {testql-1.2.2 → testql-1.2.5}/testql/generators/targets/__init__.py +0 -0
  142. {testql-1.2.2 → testql-1.2.5}/testql/generators/targets/base.py +0 -0
  143. {testql-1.2.2 → testql-1.2.5}/testql/generators/targets/nl_target.py +0 -0
  144. {testql-1.2.2 → testql-1.2.5}/testql/generators/targets/pytest_target.py +0 -0
  145. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/__init__.py +0 -0
  146. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_api_runner.py +0 -0
  147. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_assertions.py +0 -0
  148. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_converter.py +0 -0
  149. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_encoder.py +0 -0
  150. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_flow.py +0 -0
  151. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_gui.py +0 -0
  152. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_parser.py +0 -0
  153. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_shell.py +0 -0
  154. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_testtoon_parser.py +0 -0
  155. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_unit.py +0 -0
  156. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/_websockets.py +0 -0
  157. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/__init__.py +0 -0
  158. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/core.py +0 -0
  159. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/dispatcher.py +0 -0
  160. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/__init__.py +0 -0
  161. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/api.py +0 -0
  162. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/assertions.py +0 -0
  163. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/encoder.py +0 -0
  164. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/flow.py +0 -0
  165. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/include.py +0 -0
  166. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/navigate.py +0 -0
  167. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/record.py +0 -0
  168. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/select.py +0 -0
  169. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/unknown.py +0 -0
  170. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/handlers/wait.py +0 -0
  171. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/models.py +0 -0
  172. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/parsers.py +0 -0
  173. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/converter/renderer.py +0 -0
  174. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/dispatcher.py +0 -0
  175. {testql-1.2.2 → testql-1.2.5}/testql/interpreter/interpreter.py +0 -0
  176. {testql-1.2.2 → testql-1.2.5}/testql/interpreter.py +0 -0
  177. {testql-1.2.2 → testql-1.2.5}/testql/ir/__init__.py +0 -0
  178. {testql-1.2.2 → testql-1.2.5}/testql/ir/assertions.py +0 -0
  179. {testql-1.2.2 → testql-1.2.5}/testql/ir/captures.py +0 -0
  180. {testql-1.2.2 → testql-1.2.5}/testql/ir/fixtures.py +0 -0
  181. {testql-1.2.2 → testql-1.2.5}/testql/ir/metadata.py +0 -0
  182. {testql-1.2.2 → testql-1.2.5}/testql/ir/steps.py +0 -0
  183. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/__init__.py +0 -0
  184. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/assertion_eval.py +0 -0
  185. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/context.py +0 -0
  186. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/engine.py +0 -0
  187. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/__init__.py +0 -0
  188. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/api.py +0 -0
  189. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/base.py +0 -0
  190. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/encoder.py +0 -0
  191. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/graphql.py +0 -0
  192. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/gui.py +0 -0
  193. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/nl.py +0 -0
  194. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/proto.py +0 -0
  195. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/shell.py +0 -0
  196. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/sql.py +0 -0
  197. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/executors/unit.py +0 -0
  198. {testql-1.2.2 → testql-1.2.5}/testql/ir_runner/interpolation.py +0 -0
  199. {testql-1.2.2 → testql-1.2.5}/testql/meta/__init__.py +0 -0
  200. {testql-1.2.2 → testql-1.2.5}/testql/meta/confidence_scorer.py +0 -0
  201. {testql-1.2.2 → testql-1.2.5}/testql/meta/coverage_analyzer.py +0 -0
  202. {testql-1.2.2 → testql-1.2.5}/testql/meta/mutator.py +0 -0
  203. {testql-1.2.2 → testql-1.2.5}/testql/meta/self_test.py +0 -0
  204. {testql-1.2.2 → testql-1.2.5}/testql/openapi_generator.py +0 -0
  205. {testql-1.2.2 → testql-1.2.5}/testql/reporters/__init__.py +0 -0
  206. {testql-1.2.2 → testql-1.2.5}/testql/reporters/console.py +0 -0
  207. {testql-1.2.2 → testql-1.2.5}/testql/reporters/json_reporter.py +0 -0
  208. {testql-1.2.2 → testql-1.2.5}/testql/reporters/junit.py +0 -0
  209. {testql-1.2.2 → testql-1.2.5}/testql/results/__init__.py +0 -0
  210. {testql-1.2.2 → testql-1.2.5}/testql/results/artifacts.py +0 -0
  211. {testql-1.2.2 → testql-1.2.5}/testql/results/models.py +0 -0
  212. {testql-1.2.2 → testql-1.2.5}/testql/results/serializers.py +0 -0
  213. {testql-1.2.2 → testql-1.2.5}/testql/runner.py +0 -0
  214. {testql-1.2.2 → testql-1.2.5}/testql/runners/__init__.py +0 -0
  215. {testql-1.2.2 → testql-1.2.5}/testql/sumd_generator.py +0 -0
  216. {testql-1.2.2 → testql-1.2.5}/testql/sumd_parser.py +0 -0
  217. {testql-1.2.2 → testql-1.2.5}/testql/toon_parser.py +0 -0
  218. {testql-1.2.2 → testql-1.2.5}/testql/topology/models.py +0 -0
  219. {testql-1.2.2 → testql-1.2.5}/testql/topology/serializers.py +0 -0
  220. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/dependency_links.txt +0 -0
  221. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/entry_points.txt +0 -0
  222. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/requires.txt +0 -0
  223. {testql-1.2.2 → testql-1.2.5}/testql.egg-info/top_level.txt +0 -0
  224. {testql-1.2.2 → testql-1.2.5}/tests/test_adapters_base.py +0 -0
  225. {testql-1.2.2 → testql-1.2.5}/tests/test_api_handler.py +0 -0
  226. {testql-1.2.2 → testql-1.2.5}/tests/test_cli.py +0 -0
  227. {testql-1.2.2 → testql-1.2.5}/tests/test_converter.py +0 -0
  228. {testql-1.2.2 → testql-1.2.5}/tests/test_converter_handlers.py +0 -0
  229. {testql-1.2.2 → testql-1.2.5}/tests/test_detectors.py +0 -0
  230. {testql-1.2.2 → testql-1.2.5}/tests/test_discovery.py +0 -0
  231. {testql-1.2.2 → testql-1.2.5}/tests/test_dispatcher.py +0 -0
  232. {testql-1.2.2 → testql-1.2.5}/tests/test_doql_parser_sumd_gen.py +0 -0
  233. {testql-1.2.2 → testql-1.2.5}/tests/test_echo.py +0 -0
  234. {testql-1.2.2 → testql-1.2.5}/tests/test_echo_doql_parser.py +0 -0
  235. {testql-1.2.2 → testql-1.2.5}/tests/test_echo_schemas_helpers.py +0 -0
  236. {testql-1.2.2 → testql-1.2.5}/tests/test_encoder_routes.py +0 -0
  237. {testql-1.2.2 → testql-1.2.5}/tests/test_generate_ir_cli.py +0 -0
  238. {testql-1.2.2 → testql-1.2.5}/tests/test_generators.py +0 -0
  239. {testql-1.2.2 → testql-1.2.5}/tests/test_graphql_adapter.py +0 -0
  240. {testql-1.2.2 → testql-1.2.5}/tests/test_interpreter.py +0 -0
  241. {testql-1.2.2 → testql-1.2.5}/tests/test_ir.py +0 -0
  242. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_captures.py +0 -0
  243. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_runner_assertion_eval.py +0 -0
  244. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_runner_captures.py +0 -0
  245. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_runner_engine.py +0 -0
  246. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_runner_executors.py +0 -0
  247. {testql-1.2.2 → testql-1.2.5}/tests/test_ir_runner_interpolation.py +0 -0
  248. {testql-1.2.2 → testql-1.2.5}/tests/test_meta_confidence.py +0 -0
  249. {testql-1.2.2 → testql-1.2.5}/tests/test_meta_coverage.py +0 -0
  250. {testql-1.2.2 → testql-1.2.5}/tests/test_meta_mutator.py +0 -0
  251. {testql-1.2.2 → testql-1.2.5}/tests/test_meta_self_test.py +0 -0
  252. {testql-1.2.2 → testql-1.2.5}/tests/test_misc_cmds.py +0 -0
  253. {testql-1.2.2 → testql-1.2.5}/tests/test_nl_adapter.py +0 -0
  254. {testql-1.2.2 → testql-1.2.5}/tests/test_nl_entity_extractor.py +0 -0
  255. {testql-1.2.2 → testql-1.2.5}/tests/test_nl_grammar.py +0 -0
  256. {testql-1.2.2 → testql-1.2.5}/tests/test_nl_intent_recognizer.py +0 -0
  257. {testql-1.2.2 → testql-1.2.5}/tests/test_nl_scenarios_e2e.py +0 -0
  258. {testql-1.2.2 → testql-1.2.5}/tests/test_openapi_generator.py +0 -0
  259. {testql-1.2.2 → testql-1.2.5}/tests/test_pipeline.py +0 -0
  260. {testql-1.2.2 → testql-1.2.5}/tests/test_proto_adapter.py +0 -0
  261. {testql-1.2.2 → testql-1.2.5}/tests/test_proto_compatibility.py +0 -0
  262. {testql-1.2.2 → testql-1.2.5}/tests/test_proto_descriptor_loader.py +0 -0
  263. {testql-1.2.2 → testql-1.2.5}/tests/test_proto_graphql_scenarios_e2e.py +0 -0
  264. {testql-1.2.2 → testql-1.2.5}/tests/test_proto_message_validator.py +0 -0
  265. {testql-1.2.2 → testql-1.2.5}/tests/test_report_generator.py +0 -0
  266. {testql-1.2.2 → testql-1.2.5}/tests/test_reporters.py +0 -0
  267. {testql-1.2.2 → testql-1.2.5}/tests/test_run_ir_cli.py +0 -0
  268. {testql-1.2.2 → testql-1.2.5}/tests/test_runner.py +0 -0
  269. {testql-1.2.2 → testql-1.2.5}/tests/test_shell_execution.py +0 -0
  270. {testql-1.2.2 → testql-1.2.5}/tests/test_sources.py +0 -0
  271. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_adapter.py +0 -0
  272. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_ddl_parser.py +0 -0
  273. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_dialect_resolver.py +0 -0
  274. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_fixtures.py +0 -0
  275. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_query_parser.py +0 -0
  276. {testql-1.2.2 → testql-1.2.5}/tests/test_sql_scenarios_e2e.py +0 -0
  277. {testql-1.2.2 → testql-1.2.5}/tests/test_suite_cmd_helpers.py +0 -0
  278. {testql-1.2.2 → testql-1.2.5}/tests/test_suite_execution.py +0 -0
  279. {testql-1.2.2 → testql-1.2.5}/tests/test_suite_listing.py +0 -0
  280. {testql-1.2.2 → testql-1.2.5}/tests/test_sumd_parser.py +0 -0
  281. {testql-1.2.2 → testql-1.2.5}/tests/test_targets.py +0 -0
  282. {testql-1.2.2 → testql-1.2.5}/tests/test_test_generator.py +0 -0
  283. {testql-1.2.2 → testql-1.2.5}/tests/test_testtoon_adapter.py +0 -0
  284. {testql-1.2.2 → testql-1.2.5}/tests/test_toon_parser.py +0 -0
  285. {testql-1.2.2 → testql-1.2.5}/tests/test_topology.py +0 -0
  286. {testql-1.2.2 → testql-1.2.5}/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.5
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.5-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-$7.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.2h-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:** $7.2000 (48 commits)
47
+ - 👤 **Human dev:** ~$3025 (30.2h @ $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.5-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,12 +150,21 @@ 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, title extraction, link/asset/form enumeration.
163
+
153
164
  Current limitations:
154
165
 
155
- - Browser execution is not yet Playwright-backed.
156
- - JavaScript-rendered DOM is not evaluated yet.
157
- - Links/assets are extracted but not individually fetched and validated yet.
158
- - Console errors, screenshots, performance, accessibility, REST/GraphQL/WebSocket network logs, and auth flows are planned next.
166
+ - Per-resource validation uses HEAD requests only; full page content is not fetched for linked pages.
167
+ - Screenshots, performance metrics, accessibility checks, and auth flows are planned next.
159
168
 
160
169
  ## API Endpoint Detection
161
170
 
@@ -460,6 +469,21 @@ Options:
460
469
  --timeout <ms> Default timeout for operations
461
470
  ```
462
471
 
472
+ ### Generate from Topology
473
+
474
+ Generate executable scenarios from discovered topology paths:
475
+
476
+ ```bash
477
+ # Generate TestTOON from the first topology trace
478
+ testql generate-topology ./project
479
+
480
+ # Generate IR JSON from a specific trace
481
+ testql generate-topology ./project --trace-id trace.001 --format ir-json
482
+
483
+ # Write to file with live network scanning
484
+ testql generate-topology ./project --scan-network -o scenario.testql.toon.yaml
485
+ ```
486
+
463
487
  ## Testing
464
488
 
465
489
  ```bash
@@ -493,6 +517,22 @@ ASSERT[2]{field, op, expected}:
493
517
  data.id, !=, null
494
518
  ```
495
519
 
520
+ ## Examples
521
+
522
+ | Example | Description |
523
+ |---------|-------------|
524
+ | [API Testing](examples/api-testing/) | REST API testing with `API GET/POST` and assertions |
525
+ | [Artifact Bundle](examples/artifact-bundle/) | Generate `.testql/` bundles via CLI or Python script |
526
+ | [Browser Inspection](examples/browser-inspection/) | Headless browser inspection with Playwright |
527
+ | [Discovery](examples/discovery/) | Discover artifacts and build project topology |
528
+ | [GUI Testing](examples/gui-testing/) | Playwright-based GUI navigation and assertions |
529
+ | [Project Echo](examples/project-echo/) | Generate AI context from TestQL + DOQL models |
530
+ | [Shell Testing](examples/shell-testing/) | Run shell commands and assert exit codes/output |
531
+ | [TestTOON Basics](examples/testtoon-basics/) | Tabular TestTOON format walkthrough |
532
+ | [Topology](examples/topology/) | Generate topology graphs from codebases |
533
+ | [Web Inspection](examples/web-inspection/) | Inspect live URLs and generate structured reports |
534
+ | [Web Inspection + Bundle](examples/web-inspection-dot-testql/) | Full web inspection writing `.testql` artifact bundle |
535
+
496
536
  ## Documentation
497
537
 
498
538
  - [TestQL Specification](docs/testql-spec.md) — Complete language reference
@@ -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.5-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-$7.20-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.2h-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:** $7.2000 (48 commits)
10
+ - 👤 **Human dev:** ~$3025 (30.2h @ $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.5-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,12 +113,21 @@ 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, title extraction, link/asset/form enumeration.
126
+
116
127
  Current limitations:
117
128
 
118
- - Browser execution is not yet Playwright-backed.
119
- - JavaScript-rendered DOM is not evaluated yet.
120
- - Links/assets are extracted but not individually fetched and validated yet.
121
- - Console errors, screenshots, performance, accessibility, REST/GraphQL/WebSocket network logs, and auth flows are planned next.
129
+ - Per-resource validation uses HEAD requests only; full page content is not fetched for linked pages.
130
+ - Screenshots, performance metrics, accessibility checks, and auth flows are planned next.
122
131
 
123
132
  ## API Endpoint Detection
124
133
 
@@ -423,6 +432,21 @@ Options:
423
432
  --timeout <ms> Default timeout for operations
424
433
  ```
425
434
 
435
+ ### Generate from Topology
436
+
437
+ Generate executable scenarios from discovered topology paths:
438
+
439
+ ```bash
440
+ # Generate TestTOON from the first topology trace
441
+ testql generate-topology ./project
442
+
443
+ # Generate IR JSON from a specific trace
444
+ testql generate-topology ./project --trace-id trace.001 --format ir-json
445
+
446
+ # Write to file with live network scanning
447
+ testql generate-topology ./project --scan-network -o scenario.testql.toon.yaml
448
+ ```
449
+
426
450
  ## Testing
427
451
 
428
452
  ```bash
@@ -456,6 +480,22 @@ ASSERT[2]{field, op, expected}:
456
480
  data.id, !=, null
457
481
  ```
458
482
 
483
+ ## Examples
484
+
485
+ | Example | Description |
486
+ |---------|-------------|
487
+ | [API Testing](examples/api-testing/) | REST API testing with `API GET/POST` and assertions |
488
+ | [Artifact Bundle](examples/artifact-bundle/) | Generate `.testql/` bundles via CLI or Python script |
489
+ | [Browser Inspection](examples/browser-inspection/) | Headless browser inspection with Playwright |
490
+ | [Discovery](examples/discovery/) | Discover artifacts and build project topology |
491
+ | [GUI Testing](examples/gui-testing/) | Playwright-based GUI navigation and assertions |
492
+ | [Project Echo](examples/project-echo/) | Generate AI context from TestQL + DOQL models |
493
+ | [Shell Testing](examples/shell-testing/) | Run shell commands and assert exit codes/output |
494
+ | [TestTOON Basics](examples/testtoon-basics/) | Tabular TestTOON format walkthrough |
495
+ | [Topology](examples/topology/) | Generate topology graphs from codebases |
496
+ | [Web Inspection](examples/web-inspection/) | Inspect live URLs and generate structured reports |
497
+ | [Web Inspection + Bundle](examples/web-inspection-dot-testql/) | Full web inspection writing `.testql` artifact bundle |
498
+
459
499
  ## Documentation
460
500
 
461
501
  - [TestQL Specification](docs/testql-spec.md) — Complete language reference
@@ -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.5"
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.5"
@@ -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
 
@@ -232,6 +290,8 @@ def _render_plan(plan: TestPlan) -> str:
232
290
  class TestToonAdapter(BaseDSLAdapter):
233
291
  """Adapter for the legacy `*.testql.toon.yaml` format (TestTOON)."""
234
292
 
293
+ __test__ = False # Not a pytest test class
294
+
235
295
  name: str = "testtoon"
236
296
  file_extensions: tuple[str, ...] = field(default_factory=lambda: (
237
297
  ".testql.toon.yaml",
@@ -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"]