| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| |
| """ |
| QIT CLI - Main entry point. |
| |
| Command-line interface for running AMQP interoperability tests. |
| """ |
| |
| import sys |
| from pathlib import Path |
| |
| import click |
| |
| from qit import __version__ |
| |
| |
| @click.group() |
| @click.version_option(version=__version__, prog_name="qit") |
| def cli() -> None: |
| """QIT - AMQP Interoperability Test Suite.""" |
| pass |
| |
| |
| @cli.command() |
| @click.option( |
| "--build-shims", |
| is_flag=True, |
| help="Build native shims (C++, Java, etc.)", |
| ) |
| def setup(build_shims: bool) -> None: |
| """Set up QIT environment and dependencies.""" |
| click.echo("Setting up QIT environment...") |
| |
| # Check Python version |
| if sys.version_info < (3, 11): |
| click.echo("❌ Python 3.11+ required", err=True) |
| sys.exit(1) |
| |
| click.echo("✓ Python version OK") |
| |
| # Check qpid-proton |
| try: |
| import proton |
| |
| click.echo(f"✓ qpid-proton found (version {proton.VERSION})") |
| except ImportError: |
| click.echo("❌ qpid-proton not found. Install with: pip install python-qpid-proton", err=True) |
| sys.exit(1) |
| |
| # Check Docker |
| import subprocess |
| |
| try: |
| subprocess.run(["docker", "--version"], check=True, capture_output=True) |
| click.echo("✓ Docker found") |
| except (subprocess.CalledProcessError, FileNotFoundError): |
| click.echo("⚠ Docker not found (required for broker management)", err=True) |
| |
| if build_shims: |
| click.echo("\nBuilding native shims...") |
| click.echo(" [TODO] C++ shim build") |
| click.echo(" [TODO] Java shim build") |
| click.echo(" [TODO] JavaScript shim build") |
| click.echo(" [TODO] .NET shim build") |
| |
| click.echo("\n✓ Setup complete!") |
| click.echo("\nNext steps:") |
| click.echo(" 1. Start broker: docker compose -f docker/compose.yaml up -d") |
| click.echo(" 2. Run tests: qit test amqp-types") |
| |
| |
| @cli.group() |
| def test() -> None: |
| """Run interoperability tests.""" |
| pass |
| |
| |
| @test.command(name="amqp-types") |
| @click.option( |
| "--sender", |
| multiple=True, |
| help="Sender shim(s) to test (default: all)", |
| ) |
| @click.option( |
| "--receiver", |
| multiple=True, |
| help="Receiver shim(s) to test (default: all)", |
| ) |
| @click.option( |
| "--type", |
| "amqp_types", |
| multiple=True, |
| help="AMQP type(s) to test (default: all)", |
| ) |
| @click.option( |
| "--broker", |
| default="amqp://localhost:5672", |
| help="Broker URL", |
| ) |
| @click.option( |
| "--mode", |
| type=click.Choice(["broker", "direct"]), |
| default="broker", |
| help="Test mode (broker or direct peer-to-peer)", |
| ) |
| @click.option( |
| "--verbose", |
| "-v", |
| is_flag=True, |
| help="Verbose output", |
| ) |
| @click.option( |
| "--junit-xml", |
| type=click.Path(), |
| help="Generate JUnit XML report (for CI/CD integration)", |
| ) |
| @click.option( |
| "--extended", |
| is_flag=True, |
| help="Include extended tier test values for complex types", |
| ) |
| @click.option( |
| "--strict", |
| is_flag=True, |
| help="Treat known failures (xfail) as real failures", |
| ) |
| @click.option( |
| "--workers", |
| "-j", |
| type=int, |
| default=1, |
| help="Number of parallel test workers (default: 1 = sequential)", |
| ) |
| def test_amqp_types( |
| sender: tuple[str, ...], |
| receiver: tuple[str, ...], |
| amqp_types: tuple[str, ...], |
| broker: str, |
| mode: str, |
| verbose: bool, |
| junit_xml: str | None, |
| extended: bool, |
| strict: bool, |
| workers: int, |
| ) -> None: |
| """Test AMQP primitive and complex types interoperability.""" |
| from pathlib import Path |
| |
| from qit.core import BrokerConfig, BrokerManager, Orchestrator, Shim, ShimConfig |
| from qit.core.shim import discover_shims |
| from qit.types import AmqpComplexTypes, AmqpPrimitiveTypes |
| |
| click.echo("QIT - AMQP Types Test") |
| click.echo("=" * 80) |
| |
| project_root = Path(__file__).parent.parent.parent.parent |
| discovered = discover_shims(project_root / "shims") |
| |
| available_shims = {} |
| for key, info in discovered.items(): |
| if info.shim_type == "jms": |
| continue |
| available_shims[key] = Shim( |
| ShimConfig( |
| name=key, |
| language=key.split("-")[0], |
| client=info.name, |
| executable=info.shim_dir / "shim.sh", |
| ) |
| ) |
| |
| if not available_shims: |
| click.echo("❌ No shims found!", err=True) |
| click.echo(f" Expected shims in: {shim_dir}", err=True) |
| sys.exit(1) |
| |
| click.echo(f"Found {len(available_shims)} shim(s): {', '.join(available_shims.keys())}") |
| |
| # Filter shims if specified |
| sender_shims = list(sender) if sender else list(available_shims.keys()) |
| receiver_shims = list(receiver) if receiver else list(available_shims.keys()) |
| |
| # Get AMQP types to test (primitives + complex) |
| all_types = {**AmqpPrimitiveTypes.get_all_types(), **AmqpComplexTypes.get_all_types(include_extended=extended)} |
| if amqp_types: |
| test_types = {k: all_types[k]["values"] for k in amqp_types if k in all_types} |
| else: |
| test_types = {k: v["values"] for k, v in all_types.items()} |
| |
| click.echo(f"Testing {len(test_types)} type(s)") |
| click.echo() |
| |
| # Set up broker if needed |
| broker_manager = None |
| if mode == "broker": |
| compose_file = project_root / "docker" / "compose.yaml" |
| if not compose_file.exists(): |
| click.echo(f"❌ Compose file not found: {compose_file}", err=True) |
| sys.exit(1) |
| |
| broker_config = BrokerConfig( |
| name="artemis", |
| type="artemis", |
| url=broker, |
| compose_file=compose_file, |
| ) |
| broker_manager = BrokerManager(broker_config) |
| |
| # Check if broker is running (don't auto-start for now) |
| click.echo("Note: Ensure broker is running:") |
| click.echo(f" docker compose -f {compose_file} up -d") |
| click.echo() |
| |
| # Run tests |
| orchestrator = Orchestrator( |
| shims=available_shims, |
| broker=broker_manager, |
| ) |
| |
| results = orchestrator.run_test_matrix( |
| amqp_types=test_types, |
| sender_shims=sender_shims, |
| receiver_shims=receiver_shims, |
| workers=workers, |
| ) |
| |
| # Print report |
| click.echo() |
| report = orchestrator.generate_report(results) |
| click.echo(report) |
| |
| # Generate JUnit XML if requested |
| if junit_xml: |
| orchestrator.generate_junit_xml(results, junit_xml, strict=strict) |
| click.echo(f"\n✓ JUnit XML report written to: {junit_xml}") |
| |
| # Exit with error if any tests failed |
| has_failures = any(not r.success for r in results) |
| has_xfails = any(r.success and r.xfail_diffs for r in results) |
| if has_failures or (strict and has_xfails): |
| sys.exit(1) |
| |
| |
| @cli.command() |
| def broker() -> None: |
| """Manage test broker.""" |
| click.echo("Broker management commands:") |
| click.echo(" Start: docker compose -f docker/compose.yaml up -d") |
| click.echo(" Stop: docker compose -f docker/compose.yaml down") |
| click.echo(" Logs: docker compose -f docker/compose.yaml logs -f") |
| click.echo(" Status: docker compose -f docker/compose.yaml ps") |
| |
| |
| if __name__ == "__main__": |
| cli() |