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.
- {testql-1.2.2 → testql-1.2.4}/PKG-INFO +18 -7
- {testql-1.2.2 → testql-1.2.4}/README.md +17 -6
- {testql-1.2.2 → testql-1.2.4}/pyproject.toml +1 -1
- {testql-1.2.2 → testql-1.2.4}/testql/__init__.py +1 -1
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/sql_adapter.py +32 -1
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/testtoon_adapter.py +58 -0
- {testql-1.2.2 → testql-1.2.4}/testql/cli.py +2 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/__init__.py +2 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/generate_cmd.py +58 -45
- testql-1.2.4/testql/commands/generate_topology_cmd.py +58 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/inspect_cmd.py +3 -2
- testql-1.2.4/testql/discovery/probes/browser/__init__.py +7 -0
- testql-1.2.4/testql/discovery/probes/browser/playwright_page.py +147 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/network/http_endpoint.py +19 -1
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/registry.py +8 -5
- testql-1.2.4/testql/pipeline.py +135 -0
- {testql-1.2.2 → testql-1.2.4}/testql/results/analyzer.py +222 -4
- {testql-1.2.2 → testql-1.2.4}/testql/topology/__init__.py +5 -0
- {testql-1.2.2 → testql-1.2.4}/testql/topology/builder.py +8 -4
- testql-1.2.4/testql/topology/generator.py +241 -0
- testql-1.2.4/testql/topology/sitemap.py +119 -0
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/PKG-INFO +18 -7
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/SOURCES.txt +9 -0
- testql-1.2.4/tests/test_adapter_capture_syntax.py +166 -0
- testql-1.2.4/tests/test_browser_discovery.py +110 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_generate_cmd.py +22 -28
- {testql-1.2.2 → testql-1.2.4}/tests/test_network_discovery.py +22 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_results.py +2 -2
- testql-1.2.4/tests/test_topology_generator.py +161 -0
- {testql-1.2.2 → testql-1.2.4}/LICENSE +0 -0
- {testql-1.2.2 → testql-1.2.4}/setup.cfg +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/__main__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/_base_fallback.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/graphql_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/query_executor.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/schema_introspection.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/graphql/subscription_runner.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/entity_extractor.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/grammar.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/intent_recognizer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/lexicon/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/llm_fallback.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/nl/nl_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/compatibility.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/descriptor_loader.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/message_validator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/proto/proto_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/registry.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/ddl_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/dialect_resolver.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/fixtures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/adapters/sql/query_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/discover_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/cli.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/context.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/formatters/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/formatters/text.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/doql.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo/parsers/toon.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/echo_helpers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/encoder_routes.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/endpoints_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/generate_ir_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/misc_cmds.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/run_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/run_ir_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/self_test_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/cli.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/collection.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/execution.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/listing.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite/reports.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/suite_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/content.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/templates/templates.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/commands/topology_cmd.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/config_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/django_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/express_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/fastapi_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/flask_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/graphql_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/models.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/openapi_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/test_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/unified.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/detectors/websocket_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/manifest.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/api_openapi.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/container_compose.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/container_dockerfile.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/package_node.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/filesystem/package_python.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/probes/network/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/discovery/source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/doql_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/echo_schemas.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/endpoint_detector.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/analyzers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/convenience.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/generators.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/coverage_optimizer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/llm/edge_case_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/multi.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/pipeline.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/graphql_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/nl_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/openapi_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/proto_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/sql_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/sources/ui_source.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/nl_target.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/pytest_target.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/targets/testtoon_target.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/generators/test_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_api_runner.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_assertions.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_converter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_encoder.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_flow.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_gui.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_shell.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_testtoon_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_unit.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/_websockets.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/core.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/dispatcher.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/api.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/assertions.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/encoder.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/flow.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/include.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/navigate.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/record.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/select.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/unknown.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/handlers/wait.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/models.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/parsers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/converter/renderer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/dispatcher.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter/interpreter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/interpreter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/assertions.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/captures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/fixtures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/metadata.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/plan.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir/steps.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/assertion_eval.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/context.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/engine.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/api.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/encoder.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/graphql.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/gui.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/nl.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/proto.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/shell.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/sql.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/executors/unit.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/ir_runner/interpolation.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/meta/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/meta/confidence_scorer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/meta/coverage_analyzer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/meta/mutator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/meta/self_test.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/openapi_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/report_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/reporters/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/reporters/console.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/reporters/json_reporter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/reporters/junit.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/results/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/results/artifacts.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/results/models.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/results/serializers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/runner.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/runners/__init__.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/sumd_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/sumd_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/toon_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/topology/models.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql/topology/serializers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/dependency_links.txt +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/entry_points.txt +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/requires.txt +0 -0
- {testql-1.2.2 → testql-1.2.4}/testql.egg-info/top_level.txt +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_adapters_base.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_api_handler.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_cli.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_converter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_converter_handlers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_detectors.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_discovery.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_dispatcher.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_doql_parser_sumd_gen.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_echo.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_echo_doql_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_echo_schemas_helpers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_encoder_routes.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_generate_ir_cli.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_generators.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_graphql_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_gui_execution.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_interpreter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_captures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_assertion_eval.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_captures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_engine.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_executors.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_ir_runner_interpolation.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_meta_confidence.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_meta_coverage.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_meta_mutator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_meta_self_test.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_misc_cmds.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_nl_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_nl_entity_extractor.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_nl_grammar.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_nl_intent_recognizer.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_nl_scenarios_e2e.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_openapi_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_pipeline.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_proto_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_proto_compatibility.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_proto_descriptor_loader.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_proto_graphql_scenarios_e2e.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_proto_message_validator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_report_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_reporters.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_run_ir_cli.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_runner.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_shell_execution.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sources.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_ddl_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_dialect_resolver.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_fixtures.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_query_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sql_scenarios_e2e.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_suite_cmd_helpers.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_suite_execution.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_suite_listing.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_sumd_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_targets.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_test_generator.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_testtoon_adapter.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_toon_parser.py +0 -0
- {testql-1.2.2 → testql-1.2.4}/tests/test_topology.py +0 -0
- {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.
|
|
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
|
-
    
|
|
44
|
+
  
|
|
45
45
|
|
|
46
|
-
- 🤖 **LLM usage:** $6.
|
|
47
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
-
-
|
|
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
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $6.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
-
-
|
|
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.
|
|
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"
|
|
@@ -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
|
-
"""
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|