Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 15 additions & 41 deletions python/cutracer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
Provides command-line interface for trace validation and analysis.
"""

import argparse
import sys
from importlib.metadata import PackageNotFoundError, version

from cutracer.validation.cli import _add_validate_args, validate_command
import click
from cutracer.validation.cli import validate_command


def _get_package_version() -> str:
Expand All @@ -21,49 +21,23 @@ def _get_package_version() -> str:
return "0+unknown"


def main() -> int:
"""Main CLI entry point."""
pkg_version = _get_package_version()

parser = argparse.ArgumentParser(
prog="cutraceross",
description="CUTracer: CUDA trace validation and analysis tools",
epilog=(
"Examples:\n"
" cutraceross validate kernel_trace.ndjson\n"
" cutraceross validate kernel_trace.ndjson.zst --verbose\n"
" cutraceross validate trace.log --format text\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {pkg_version}",
help="Show program's version number and exit",
)
EXAMPLES = """
Examples:
cutraceross validate kernel_trace.ndjson
cutraceross validate kernel_trace.ndjson.zst --verbose
cutraceross validate trace.log --format text
"""

subparsers = parser.add_subparsers(dest="command", required=True)

# validate subcommand
validate_parser = subparsers.add_parser(
"validate",
help="Validate a CUTracer trace file",
description=(
"Validate a CUTracer trace file.\n\n"
"Checks syntax and schema compliance for NDJSON, Zstd-compressed,\n"
"and text format trace files."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
_add_validate_args(validate_parser)
validate_parser.set_defaults(func=validate_command)
@click.group(epilog=EXAMPLES)
@click.version_option(version=_get_package_version(), prog_name="cutraceross")
def main() -> None:
"""CUTracer: CUDA trace validation and analysis tools."""
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotation has changed from int to None, but the existing tests in python/tests/test_cli.py expect main() to return an integer exit code. Tests like test_validate_valid_json() call exit_code = main() and assert on the value. With click, the function returns None and uses sys.exit() internally. This will break all existing CLI tests. Consider either updating the tests to use assertRaises(SystemExit) or making click's standalone_mode=False to preserve the return value behavior for testing.

Copilot uses AI. Check for mistakes.
pass

# Parse arguments
args = parser.parse_args()

# Execute command
return args.func(args)
# Register subcommands
main.add_command(validate_command)


if __name__ == "__main__":
Expand Down
132 changes: 66 additions & 66 deletions python/cutracer/validation/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,17 @@
This module provides command-line interface for validating CUTracer trace files.
"""

import argparse
import json
import sys
from pathlib import Path
from typing import Any

import click

from .json_validator import validate_json_trace
from .text_validator import validate_text_trace


def _add_validate_args(parser: argparse.ArgumentParser) -> None:
"""Add arguments for the validate subcommand."""
parser.add_argument(
"file",
type=Path,
help="Path to the trace file to validate",
)
parser.add_argument(
"--format",
"-f",
choices=["json", "text", "auto"],
default="auto",
help="File format. Default: auto-detect from extension.",
)
parser.add_argument(
"--quiet",
"-q",
action="store_true",
help="Quiet mode. Only return exit code.",
)
parser.add_argument(
"--json",
dest="json_output",
action="store_true",
help="Output results in JSON format.",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Verbose output with additional details.",
)


def _detect_format(file_path: Path) -> str:
"""Auto-detect file format from extension."""
suffixes = "".join(file_path.suffixes).lower()
Expand Down Expand Up @@ -87,43 +54,76 @@ def _format_trace_format(result: dict[str, Any]) -> str:
def _print_validation_result(result: dict[str, Any], verbose: bool = False) -> None:
"""Print validation result in human-readable format."""
if result["valid"]:
print("\u2705 Valid trace file")
print(f" Format: {_format_trace_format(result)}")
print(f" Records: {result['record_count']}")
click.echo("\u2705 Valid trace file")
click.echo(f" Format: {_format_trace_format(result)}")
click.echo(f" Records: {result['record_count']}")
if result.get("message_type"):
print(f" Message type: {result['message_type']}")
click.echo(f" Message type: {result['message_type']}")
if result.get("file_size"):
print(f" File size: {_format_size(result['file_size'])}")
click.echo(f" File size: {_format_size(result['file_size'])}")
if verbose and result.get("compression") == "zstd":
print(" Compression: zstd")
click.echo(" Compression: zstd")
else:
print("\u274c Validation failed")
click.echo("\u274c Validation failed")
for error in result.get("errors", []):
print(f" {error}")


def validate_command(args: argparse.Namespace) -> int:
"""Execute the validate subcommand."""
file_path: Path = args.file

# Check file exists
if not file_path.exists():
if not args.quiet:
print(f"Error: File not found: {file_path}", file=sys.stderr)
return 2
click.echo(f" {error}")


@click.command(name="validate")
@click.argument("file", type=click.Path(exists=True, path_type=Path))
@click.option(
"--format",
"-f",
"file_format",
type=click.Choice(["json", "text", "auto"]),
default="auto",
help="File format. Default: auto-detect from extension.",
)
@click.option(
"--quiet",
"-q",
is_flag=True,
help="Quiet mode. Only return exit code.",
)
@click.option(
"--json",
"json_output",
is_flag=True,
help="Output results in JSON format.",
)
@click.option(
"--verbose",
"-v",
is_flag=True,
help="Verbose output with additional details.",
)
def validate_command(
file: Path,
file_format: str,
quiet: bool,
json_output: bool,
verbose: bool,
) -> None:
Comment on lines +100 to +106
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotation has changed from int to None, but the function now calls sys.exit() directly instead of returning exit codes. This is a breaking change for the existing tests in python/tests/test_cli.py which expect the function to return an integer exit code. The tests call exit_code = main() and then assert on the returned value. With the current implementation using sys.exit(), these tests will fail. Consider using click's standalone_mode=False to preserve the return value behavior for testing compatibility.

Copilot uses AI. Check for mistakes.
"""Validate a CUTracer trace file.

Checks syntax and schema compliance for NDJSON, Zstd-compressed,
and text format trace files.

FILE is the path to the trace file to validate.
"""
file_path = file
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable assignment file_path = file is unnecessary since the parameter file is already a Path object and could be used directly throughout the function. This adds no value and makes the code slightly less clear.

Copilot uses AI. Check for mistakes.

# Detect format
file_format = args.format
if file_format == "auto":
file_format = _detect_format(file_path)
if file_format == "unknown":
if not args.quiet:
print(
if not quiet:
click.echo(
f"Error: Cannot auto-detect format for {file_path}. "
"Use --format to specify.",
file=sys.stderr,
err=True,
)
return 2
sys.exit(2)

# Run validation
if file_format == "json":
Expand All @@ -132,17 +132,17 @@ def validate_command(args: argparse.Namespace) -> int:
result = validate_text_trace(file_path)

# Handle quiet mode
if args.quiet:
return 0 if result["valid"] else 1
if quiet:
sys.exit(0 if result["valid"] else 1)

# Handle JSON output
if args.json_output:
if json_output:
# Convert Path objects to strings for JSON serialization
output = {k: str(v) if isinstance(v, Path) else v for k, v in result.items()}
print(json.dumps(output, indent=2))
return 0 if result["valid"] else 1
click.echo(json.dumps(output, indent=2))
sys.exit(0 if result["valid"] else 1)

# Human-readable output
_print_validation_result(result, args.verbose)
_print_validation_result(result, verbose)

return 0 if result["valid"] else 1
sys.exit(0 if result["valid"] else 1)
1 change: 1 addition & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ readme = "README.md"
license = "MIT"

dependencies = [
"click>=8.0.0",
"jsonschema>=4.0.0",
"zstandard>=0.20.0",
"importlib_resources>=5.0.0; python_version < '3.11'",
Expand Down
Loading