601 lines
22 KiB
Python
601 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""GPU Training Server Test Suite - Main CLI Entry Point."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
from rich import box
|
|
|
|
from modules.gpu_info import GPUInfo
|
|
from modules.health_check import HealthCheck
|
|
from modules.benchmark import Benchmark
|
|
from modules.nccl_test import NCCLTest
|
|
from modules.training_sim import TrainingSim
|
|
from modules.stress_test import StressTest
|
|
from modules.rdma_test import RDMATest
|
|
from modules.nvlink_test import NVLinkTest
|
|
from modules.dcgm_test import DCGMTest
|
|
from modules.multinode_nccl_test import MultiNodeNCCLTest
|
|
from modules.report import ReportGenerator
|
|
from modules.gpu_specs import detect_gpu_type, get_gpu_specs, get_gpu_label, get_supported_gpus, validate_driver_compatibility
|
|
|
|
DEFAULT_CONFIG = {
|
|
"benchmark": {
|
|
"memory": {"size_mb": 4096, "iterations": 10, "nvbandwidth_buffer_mb": 512, "nvbandwidth_samples": 3},
|
|
"compute": {
|
|
"dtypes": ["fp32", "tf32", "fp16", "bf16", "fp8", "fp64", "int8"],
|
|
"matrix_size": 8192,
|
|
"warmup": 50,
|
|
"iterations": 500,
|
|
"use_compile": True,
|
|
},
|
|
},
|
|
"health": {"temp_warning": 75, "temp_critical": 85, "power_limit": None},
|
|
"nccl": {
|
|
"min_bandwidth_gbps": None,
|
|
"test_allreduce": True,
|
|
"test_alltoall": True,
|
|
"test_broadcast": True,
|
|
"test_reduce_scatter": True,
|
|
"test_allgather": True,
|
|
"test_sendrecv": True,
|
|
"message_sizes": ["1M", "256M", "2G"],
|
|
"repeats": 3,
|
|
"max_stddev_pct": 3,
|
|
},
|
|
"multinode_nccl": {
|
|
"enabled": False,
|
|
"mode": "sweep",
|
|
"hosts": [
|
|
{"name": "nccl-gpu-1", "addr": "172.72.8.12", "slots": 8},
|
|
{"name": "nccl-gpu-2", "addr": "172.72.8.16", "slots": 8},
|
|
],
|
|
"ssh_user": "root",
|
|
"ssh_preflight": True,
|
|
"mpirun_path": "/usr/mpi/gcc/openmpi-4.1.9a1/bin/mpirun",
|
|
"mpi_ld_preload": None,
|
|
"extra_ld_library_path": [
|
|
"/usr/mpi/gcc/openmpi-4.1.9a1/lib",
|
|
"/root/gpu-test-venv/lib/python3.10/site-packages/nvidia/nccl/lib",
|
|
"/usr/local/cuda-12.4/targets/x86_64-linux/lib",
|
|
],
|
|
"nccl_tests_dir": None,
|
|
"tests": ["all_reduce_perf", "alltoall_perf"],
|
|
"topologies": [{"nodes": 2, "gpus_per_node": 8}],
|
|
"begin_size": "1k",
|
|
"end_size": "16g",
|
|
"step_factor": 2,
|
|
"warmup_iters": 10,
|
|
"gpus_per_rank": 1,
|
|
"timeout_sec": 1800,
|
|
"socket_ifname": "bond0",
|
|
"ib_gid_index": 3,
|
|
"ib_sl": 5,
|
|
"ib_tc": 136,
|
|
"ib_hca": "mlx5_0,mlx5_1,mlx5_6,mlx5_7",
|
|
"ib_timeout": 22,
|
|
"qps_per_connection": 4,
|
|
"min_nchannels": 4,
|
|
"net_plugin": "none",
|
|
"nvls_enable": 1,
|
|
"split_data_on_qps": 1,
|
|
"min_peak_busbw_gbps": {"allreduce": 480, "alltoall": 75},
|
|
},
|
|
"stress": {
|
|
"duration_sec": 1800,
|
|
"production_duration_sec": 1800,
|
|
"use_gpu_burn": False,
|
|
"use_doubles": False,
|
|
"use_tensor_cores": True,
|
|
"memory_pct": 90,
|
|
"gpus": "all",
|
|
"dtype": "bf16",
|
|
"matrix_size": 24576,
|
|
"telemetry_interval_sec": 1,
|
|
"warmup_sec": 60,
|
|
"min_steady_samples": 10,
|
|
"max_temp_c": 80,
|
|
"max_temp_delta_c": 5,
|
|
"min_power_watts": 630,
|
|
"max_tflops_jitter_pct": 5,
|
|
"require_tflops_jitter": True,
|
|
},
|
|
"rdma": {
|
|
"min_bandwidth_gbps": 47,
|
|
"min_port_rate_gbps": 400,
|
|
"max_latency_us": 3.5,
|
|
"max_write_latency_us": 2.0,
|
|
"max_read_latency_us": 3.5,
|
|
"ib_iterations": 1000,
|
|
"msg_size": 4194304,
|
|
"latency_msg_size": 8,
|
|
"ib_device": None,
|
|
"ib_port": 1,
|
|
"server_addr": None,
|
|
"ibping_target": None,
|
|
"ibping_count": 5,
|
|
"role": "auto",
|
|
"pfc_ecn_counters": True,
|
|
},
|
|
"nvlink": {
|
|
"expected_links_per_gpu": 18,
|
|
"expected_link_speed_gbps": 25,
|
|
"require_zero_errors": True,
|
|
},
|
|
"dcgm": {
|
|
"diag_level": 3,
|
|
"timeout_sec": 1200,
|
|
"expected_num_gpus": 8,
|
|
"json_output": True,
|
|
"require_subtests": True,
|
|
},
|
|
"training": {
|
|
"model": "synthetic_1.5b",
|
|
"batch_size": 8,
|
|
"seq_length": 2048,
|
|
"num_steps": 50,
|
|
"warmup_steps": 5,
|
|
"dtype": "bf16",
|
|
"mode": "ddp",
|
|
"synthetic_params_b": 1.5,
|
|
"min_tokens_per_sec": 45000,
|
|
"max_step_jitter_pct": 3,
|
|
"max_peak_memory_gb": 70,
|
|
"require_distributed": True,
|
|
},
|
|
"report": {"output_dir": "./reports", "format": "json"},
|
|
"tools": {"install_dir": "/opt/gpu-test-tools"},
|
|
}
|
|
|
|
def _build_banner() -> str:
|
|
gpu_list = " / ".join(g.upper() for g in get_supported_gpus())
|
|
return (
|
|
"[bold cyan]\n"
|
|
"╔══════════════════════════════════════════════════════════╗\n"
|
|
"║ ║\n"
|
|
"║ GPU Training Server Test Suite ║\n"
|
|
"║ Diagnostics & Benchmarking Tool ║\n"
|
|
f"║ Supports: {gpu_list:<40s} ║\n"
|
|
"║ ║\n"
|
|
"╚══════════════════════════════════════════════════════════╝\n"
|
|
"[/bold cyan]"
|
|
)
|
|
|
|
|
|
def load_config() -> dict:
|
|
"""Load config from yaml file, fallback to defaults."""
|
|
config_path = Path(__file__).parent / "configs" / "default.yaml"
|
|
if config_path.exists():
|
|
with open(config_path) as f:
|
|
return yaml.safe_load(f) or DEFAULT_CONFIG
|
|
return DEFAULT_CONFIG.copy()
|
|
|
|
|
|
def check_prerequisites(console: Console) -> bool:
|
|
"""Check if required tools are available."""
|
|
import shutil
|
|
|
|
ok = True
|
|
if not shutil.which("nvidia-smi"):
|
|
console.print("[bold red]ERROR: nvidia-smi not found![/bold red]")
|
|
console.print(" Please install NVIDIA drivers first.")
|
|
ok = False
|
|
return ok
|
|
|
|
|
|
def interactive_menu(config: dict):
|
|
"""Run interactive menu loop."""
|
|
console = Console()
|
|
|
|
console.print(_build_banner())
|
|
|
|
gpu_type = detect_gpu_type()
|
|
gpu_label = get_gpu_label(gpu_type)
|
|
if gpu_type != "unknown":
|
|
console.print(f"[bold green]Detected GPU: {gpu_label} ({gpu_type.upper()})[/bold green]\n")
|
|
else:
|
|
console.print("[yellow]GPU type could not be auto-detected. Using default thresholds.[/yellow]\n")
|
|
|
|
# Driver / CUDA compatibility check
|
|
compat_warnings = validate_driver_compatibility(gpu_type)
|
|
for w in compat_warnings:
|
|
console.print(f"[bold yellow]\u26a0 {w}[/bold yellow]")
|
|
|
|
if not check_prerequisites(console):
|
|
return
|
|
|
|
results_store: dict = {"timestamp": datetime.now().isoformat(), "hostname": socket.gethostname(), "tests": {}}
|
|
|
|
menu_items = [
|
|
("1", "GPU Information", "gpu_info"),
|
|
("2", "Health Check", "health"),
|
|
("3", "Memory Benchmark (nvbandwidth)", "memory_bench"),
|
|
("4", "Compute Benchmark", "compute_bench"),
|
|
("5", "NCCL Multi-GPU Test", "nccl"),
|
|
("6", "GPU Stress Test (PyTorch/gpu-burn)", "stress"),
|
|
("7", "RDMA/IB Test", "rdma"),
|
|
("8", "NVLink/NVSwitch Test", "nvlink"),
|
|
("9", "DCGM Diagnostic", "dcgm"),
|
|
("10", "Training Simulation", "training"),
|
|
("11", "Multi-node NCCL Test", "multinode_nccl"),
|
|
("12", "Full Test Suite (All Tests)", "all"),
|
|
("0", "Generate Report", "report"),
|
|
]
|
|
|
|
while True:
|
|
console.print()
|
|
table = Table(
|
|
title="[bold cyan]Select a Test[/bold cyan]",
|
|
box=box.ROUNDED,
|
|
border_style="cyan",
|
|
show_header=False,
|
|
padding=(0, 2),
|
|
)
|
|
table.add_column("Key", style="bold yellow", width=5)
|
|
table.add_column("Test Name")
|
|
table.add_column("Description", style="dim")
|
|
descriptions = {
|
|
"gpu_info": "Detect GPUs, show specs & NVLink topology",
|
|
"health": "Temperature, power, ECC errors, PCIe, DCGM",
|
|
"memory_bench": "HBM bandwidth via nvbandwidth",
|
|
"compute_bench": "GEMM TFLOPS across FP32/TF32/FP16/BF16/FP8",
|
|
"nccl": "AllReduce, AllToAll, Broadcast via nccl-tests",
|
|
"stress": "Long-running high-power GEMM stress with telemetry",
|
|
"rdma": "InfiniBand bandwidth & latency (ib_write_bw)",
|
|
"nvlink": "NVLink links, speed, and error counters",
|
|
"dcgm": "DCGM diag -r 3 production diagnostic",
|
|
"training": "Simulate LLM training with PyTorch",
|
|
"multinode_nccl": "Cross-node NCCL via mpirun/nccl-tests",
|
|
"all": "Run all tests sequentially",
|
|
"report": "Export results to JSON/HTML",
|
|
}
|
|
for key, name, action in menu_items:
|
|
table.add_row(f"[{key}]", name, descriptions.get(action, ""))
|
|
table.add_row("[q]", Text("Quit", style="bold red"), "Exit the program")
|
|
|
|
console.print(table)
|
|
choice = console.input("\n[bold green]Enter choice > [/bold green]").strip().lower()
|
|
|
|
if choice == "q":
|
|
if results_store.get("tests"):
|
|
_save_results_prompt(results_store, config, console)
|
|
console.print("[dim]Goodbye![/dim]")
|
|
break
|
|
|
|
action_map = {item[0]: item[2] for item in menu_items}
|
|
action = action_map.get(choice)
|
|
if action is None:
|
|
console.print(f"[yellow]Invalid choice: {choice}[/yellow]")
|
|
continue
|
|
|
|
result = _run_test(action, config, console)
|
|
if result:
|
|
if result.get("__report__"):
|
|
if results_store.get("tests"):
|
|
rg = ReportGenerator(config)
|
|
rg.generate(results_store)
|
|
else:
|
|
console.print("[yellow]No test results to export. Run tests first.[/yellow]")
|
|
else:
|
|
results_store["tests"][action] = result
|
|
|
|
return results_store
|
|
|
|
|
|
def _save_results_prompt(results_store: dict, config: dict, console: Console):
|
|
if not results_store.get("tests"):
|
|
return
|
|
save = console.input("[bold green]Save results before quitting? [y/N]: [/bold green]").strip().lower()
|
|
if save == "y":
|
|
rg = ReportGenerator(config)
|
|
rg.generate(results_store)
|
|
|
|
|
|
def _run_test(test_name: str, config: dict, console: Console) -> dict:
|
|
"""Execute a single test by name."""
|
|
try:
|
|
if test_name == "gpu_info":
|
|
m = GPUInfo(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "health":
|
|
m = HealthCheck(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "memory_bench":
|
|
m = Benchmark(config)
|
|
result = m.run_memory_benchmark()
|
|
Benchmark.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "compute_bench":
|
|
m = Benchmark(config)
|
|
result = m.run_compute_benchmark()
|
|
Benchmark.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "nccl":
|
|
m = NCCLTest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "stress":
|
|
m = StressTest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "rdma":
|
|
m = RDMATest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "nvlink":
|
|
m = NVLinkTest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "dcgm":
|
|
m = DCGMTest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "training":
|
|
m = TrainingSim(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "multinode_nccl":
|
|
m = MultiNodeNCCLTest(config)
|
|
result = m.run()
|
|
m.print_results(result)
|
|
return result
|
|
|
|
elif test_name == "all":
|
|
return _run_full_suite(config, console)
|
|
|
|
elif test_name == "report":
|
|
return {"__report__": True}
|
|
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Test interrupted by user.[/yellow]")
|
|
return {"error": "interrupted"}
|
|
except Exception as e:
|
|
console.print(f"[bold red]Test failed: {e}[/bold red]")
|
|
return {"error": str(e)}
|
|
|
|
|
|
def _run_full_suite(config: dict, console: Console) -> dict:
|
|
"""Run all tests sequentially."""
|
|
console.print(Panel("[bold cyan]Running Full Test Suite[/bold cyan]", box=box.DOUBLE))
|
|
all_results: dict = {"timestamp": datetime.now().isoformat(), "hostname": socket.gethostname()}
|
|
tests = [
|
|
("gpu_info", "GPU Information", GPUInfo),
|
|
("health", "Health Check", HealthCheck),
|
|
("memory_bench", "Memory Benchmark", lambda c: Benchmark(c)),
|
|
("compute_bench", "Compute Benchmark", lambda c: Benchmark(c)),
|
|
("nvlink", "NVLink/NVSwitch Test", NVLinkTest),
|
|
("nccl", "NCCL Test", NCCLTest),
|
|
("stress", "GPU Stress Test", StressTest),
|
|
("rdma", "RDMA/IB Test", RDMATest),
|
|
("dcgm", "DCGM Diagnostic", DCGMTest),
|
|
("training", "Training Simulation", TrainingSim),
|
|
]
|
|
if (config.get("multinode_nccl", {}) or {}).get("enabled"):
|
|
tests.append(("multinode_nccl", "Multi-node NCCL Test", MultiNodeNCCLTest))
|
|
|
|
for i, (key, name, mod_cls) in enumerate(tests, 1):
|
|
console.print(f"\n[bold cyan][{i}/{len(tests)}] {name}[/bold cyan]")
|
|
try:
|
|
mod = mod_cls(config)
|
|
if key == "memory_bench":
|
|
result = mod.run_memory_benchmark()
|
|
mod.print_results(result)
|
|
elif key == "compute_bench":
|
|
result = mod.run_compute_benchmark()
|
|
mod.print_results(result)
|
|
else:
|
|
result = mod.run()
|
|
mod.print_results(result)
|
|
all_results[key] = result
|
|
except Exception as e:
|
|
console.print(f"[bold red]{name} FAILED: {e}[/bold red]")
|
|
all_results[key] = {"error": str(e)}
|
|
|
|
# Summary
|
|
console.print("\n" + "=" * 60)
|
|
# Only count test results, exclude metadata like timestamp
|
|
test_results = {k: v for k, v in all_results.items() if k not in ("timestamp", "hostname")}
|
|
passed = sum(1 for v in test_results.values() if _test_result_passed(v))
|
|
total = len(test_results)
|
|
color = "green" if passed == total else ("yellow" if passed > 0 else "red")
|
|
console.print(f"[bold {color}]Suite complete: {passed}/{total} tests passed[/bold {color}]")
|
|
return all_results
|
|
|
|
|
|
def _test_result_passed(result) -> bool:
|
|
"""Strict production verdict helper for full-suite exit status."""
|
|
if not isinstance(result, dict):
|
|
return True
|
|
if result.get("error"):
|
|
return False
|
|
if result.get("skipped") or result.get("status") == "SKIP":
|
|
return False
|
|
if result.get("source") == "torchrun_fallback":
|
|
return False
|
|
if "passed" in result:
|
|
return bool(result.get("passed"))
|
|
if "memory" in result:
|
|
mem = result["memory"]
|
|
if isinstance(mem, dict) and "passed" in mem:
|
|
return bool(mem.get("passed"))
|
|
if mem.get("error") or mem.get("source") == "pytorch":
|
|
return False
|
|
eff = mem.get("d2d_efficiency_pct") or mem.get("efficiency_pct") or 0
|
|
return eff >= 80
|
|
if "compute" in result:
|
|
comp = result["compute"]
|
|
if isinstance(comp, dict) and "passed" in comp:
|
|
return bool(comp.get("passed"))
|
|
thresholds = comp.get("pass_thresholds_tflops", {}) or {}
|
|
per_dtype = comp.get("per_dtype_tflops", {})
|
|
for dt, threshold in thresholds.items():
|
|
val = per_dtype.get(dt)
|
|
if not isinstance(val, (int, float)) or val < threshold:
|
|
return False
|
|
consistency = comp.get("consistency", {})
|
|
return not any(not c.get("passed", False) for c in consistency.values())
|
|
return True
|
|
|
|
|
|
def main():
|
|
gpu_list_str = " / ".join(g.upper() for g in get_supported_gpus())
|
|
parser = argparse.ArgumentParser(
|
|
description=f"GPU Training Server Test Suite ({gpu_list_str})",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python gpu_tester.py # Interactive menu
|
|
python gpu_tester.py --gpu-type a800 # Override GPU type
|
|
python gpu_tester.py --test gpu-info # GPU info
|
|
python gpu_tester.py --test health # Health check
|
|
python gpu_tester.py --test benchmark --type memory
|
|
python gpu_tester.py --test benchmark --type compute --dtype fp16
|
|
python gpu_tester.py --test nccl # NCCL test
|
|
python gpu_tester.py --test multinode-nccl # Cross-node NCCL test
|
|
python gpu_tester.py --test nvlink # NVLink/NVSwitch test
|
|
python gpu_tester.py --test dcgm # DCGM diagnostic
|
|
python gpu_tester.py --test training # Training sim
|
|
python gpu_tester.py --test all # Full suite
|
|
python gpu_tester.py --report --format json --output report.json
|
|
""",
|
|
)
|
|
parser.add_argument("--test", choices=["gpu-info", "health", "benchmark", "nccl", "multinode-nccl", "stress", "rdma", "nvlink", "dcgm", "training", "all"],
|
|
help="Run a specific test")
|
|
parser.add_argument("--type", choices=["memory", "compute"], help="Benchmark type (with --test benchmark)")
|
|
parser.add_argument("--dtype", choices=["fp32", "tf32", "fp16", "bf16", "fp8", "fp64", "int8"],
|
|
help="Compute benchmark dtype (with --test benchmark --type compute)")
|
|
parser.add_argument("--interactive", action="store_true", help="Force interactive mode")
|
|
parser.add_argument("--report", action="store_true", help="Generate report from last results")
|
|
parser.add_argument("--format", choices=["json", "html", "md"], default="json", help="Report format")
|
|
parser.add_argument("--output", default=None, help="Report output file path")
|
|
parser.add_argument("--config", default=None, help="Path to config YAML file")
|
|
parser.add_argument(
|
|
"--gpu-type",
|
|
choices=["auto"] + get_supported_gpus(),
|
|
default="auto",
|
|
help="Override GPU type detection",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
config = load_config()
|
|
|
|
# Override config with CLI args (load before gpu_type so custom configs work)
|
|
if args.config:
|
|
with open(args.config) as f:
|
|
config = yaml.safe_load(f)
|
|
|
|
# Set GPU type after config is finalized
|
|
if args.gpu_type and args.gpu_type != "auto":
|
|
config["gpu_type"] = args.gpu_type
|
|
else:
|
|
config["gpu_type"] = detect_gpu_type()
|
|
|
|
console = Console()
|
|
|
|
# Driver / CUDA compatibility check
|
|
compat_warnings = validate_driver_compatibility(config["gpu_type"])
|
|
for w in compat_warnings:
|
|
console.print(f"[bold yellow]\u26a0 {w}[/bold yellow]")
|
|
|
|
# Handle --report standalone
|
|
if args.report and not args.test:
|
|
console.print("[yellow]Run tests first to generate a report.[/yellow]")
|
|
return
|
|
|
|
# Interactive mode
|
|
if args.interactive or not args.test:
|
|
interactive_menu(config)
|
|
return
|
|
|
|
# CLI mode
|
|
if not check_prerequisites(console):
|
|
sys.exit(1)
|
|
|
|
test_map = {
|
|
"gpu-info": "gpu_info",
|
|
"health": "health",
|
|
"benchmark": None,
|
|
"nccl": "nccl",
|
|
"multinode-nccl": "multinode_nccl",
|
|
"stress": "stress",
|
|
"rdma": "rdma",
|
|
"nvlink": "nvlink",
|
|
"dcgm": "dcgm",
|
|
"training": "training",
|
|
"all": "all",
|
|
}
|
|
|
|
if args.test == "benchmark":
|
|
bench = Benchmark(config)
|
|
if args.type == "memory":
|
|
result = bench.run_memory_benchmark()
|
|
Benchmark.print_results(result)
|
|
elif args.type == "compute":
|
|
result = bench.run_compute_benchmark(dtypes=[args.dtype] if args.dtype else None)
|
|
Benchmark.print_results(result)
|
|
else:
|
|
result = bench.run()
|
|
Benchmark.print_results(result)
|
|
if args.report:
|
|
ReportGenerator(config).generate({
|
|
"benchmark": result,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"hostname": socket.gethostname(),
|
|
},
|
|
fmt=args.format, output=args.output)
|
|
sys.exit(0 if _test_result_passed(result) else 1)
|
|
elif args.test == "all":
|
|
results = _run_full_suite(config, console)
|
|
if args.report:
|
|
ReportGenerator(config).generate(results, fmt=args.format, output=args.output)
|
|
failed = any(not _test_result_passed(v) for k, v in results.items() if k not in ("timestamp", "hostname"))
|
|
sys.exit(1 if failed else 0)
|
|
else:
|
|
result = _run_test(test_map[args.test], config, console)
|
|
if args.report and result:
|
|
report_key = test_map[args.test] or args.test
|
|
ReportGenerator(config).generate({
|
|
report_key: result,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"hostname": socket.gethostname(),
|
|
},
|
|
fmt=args.format, output=args.output)
|
|
sys.exit(0 if _test_result_passed(result) else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|