"""
Spectral SHA-256 — ASIC Virtualized as Sound Block Tests

Tests the complete SHA-256 implementation using only Fabric primitives.
Every ASIC gate (XOR, AND, NOT, ADD, ROTR) expressed as GPU-resident
spectral operations.

Run:
    cd afs-engine
    python3 tests/test_sb_spectral_sha256.py -v
"""

import unittest
import numpy as np
import hashlib
import time
import struct
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from afs.gpu.sb_fabric import SoundBlockFabric, gpu_available
from afs.gpu.sb_spectral_sha256 import (
    SpectralSHA256, _u32_to_f32, _f32_to_u32, _u32_arr_to_f32, _f32_arr_to_u32
)


# ═══════════════════════════════════════════════════════════════════════════
# Primitive Tests — XOR, AND, ROTR, BITNOT on GPU
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestBitwiseGPUPrimitives(unittest.TestCase):
    """Test the ASIC gate equivalents as GPU Fabric ops."""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=10240)

    def test_xor_gpu(self):
        """XOR gate: spectral toggle."""
        a = np.array([0xAAAAAAAA, 0x00FF00FF, 0xFFFFFFFF, 0x00000000], dtype=np.uint32)
        b = np.array([0x55555555, 0xFF00FF00, 0xFFFFFFFF, 0x00000000], dtype=np.uint32)
        expected = a ^ b

        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric.upload('b', _u32_arr_to_f32(b))
        self.fabric._slot_sizes['a'] = 4
        self.fabric._slot_sizes['b'] = 4
        self.fabric.xor_gpu('a', 'b', 'out')
        result = _f32_arr_to_u32(self.fabric.download('out')[:4])
        np.testing.assert_array_equal(result, expected)
        print(f"\n  XOR gate: {result[0]:#010x} ✓")

    def test_and_gpu(self):
        """AND gate: spectral mask."""
        a = np.array([0xAAAAAAAA, 0xFF00FF00], dtype=np.uint32)
        b = np.array([0x55555555, 0x0F0F0F0F], dtype=np.uint32)
        expected = a & b

        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric.upload('b', _u32_arr_to_f32(b))
        self.fabric._slot_sizes['a'] = 2
        self.fabric._slot_sizes['b'] = 2
        self.fabric.and_gpu('a', 'b', 'out')
        result = _f32_arr_to_u32(self.fabric.download('out')[:2])
        np.testing.assert_array_equal(result, expected)
        print(f"\n  AND gate: {result[0]:#010x} ✓")

    def test_or_gpu(self):
        """OR gate: spectral union."""
        a = np.array([0xAA000000], dtype=np.uint32)
        b = np.array([0x00BB0000], dtype=np.uint32)
        expected = a | b

        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric.upload('b', _u32_arr_to_f32(b))
        self.fabric._slot_sizes['a'] = 1
        self.fabric._slot_sizes['b'] = 1
        self.fabric.or_gpu('a', 'b', 'out')
        result = _f32_arr_to_u32(self.fabric.download('out')[:1])
        np.testing.assert_array_equal(result, expected)

    def test_bitnot_gpu(self):
        """NOT gate: phase inversion."""
        a = np.array([0x00000000, 0xFFFFFFFF, 0xAAAAAAAA], dtype=np.uint32)
        expected = ~a

        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric._slot_sizes['a'] = 3
        self.fabric.bitnot_gpu('a', 'out')
        result = _f32_arr_to_u32(self.fabric.download('out')[:3])
        np.testing.assert_array_equal(result, expected)

    def test_rotr_gpu(self):
        """ROTR: frequency shift (circular bit permutation)."""
        val = np.uint32(0x80000001)  # 1 bit at MSB and LSB
        a = np.array([val], dtype=np.uint32)

        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric._slot_sizes['a'] = 1
        self.fabric.rotr_gpu('a', 'out', 1)
        result = _f32_arr_to_u32(self.fabric.download('out')[:1])

        expected = np.uint32((int(val) >> 1) | (int(val) << 31) & 0xFFFFFFFF)
        self.assertEqual(result[0], expected,
                         f"ROTR(1): got {result[0]:#010x}, expected {expected:#010x}")
        print(f"\n  ROTR(1): {val:#010x} → {result[0]:#010x} ✓")

    def test_shr_gpu(self):
        """SHR: spectral decay (non-circular shift)."""
        a = np.array([np.uint32(0xFF000000)], dtype=np.uint32)
        self.fabric.upload('a', _u32_arr_to_f32(a))
        self.fabric._slot_sizes['a'] = 1
        self.fabric.shr_gpu('a', 'out', 8)
        result = _f32_arr_to_u32(self.fabric.download('out')[:1])
        self.assertEqual(result[0], 0x00FF0000)

    def test_u32_f32_roundtrip(self):
        """Bitcast u32↔f32 preserves all bits."""
        test_vals = [0, 1, 0xFFFFFFFF, 0x80000000, 0x12345678, 0xDEADBEEF]
        for v in test_vals:
            f = _u32_to_f32(np.uint32(v))
            back = _f32_to_u32(f)
            self.assertEqual(back, v, f"Roundtrip failed for {v:#010x}")


# ═══════════════════════════════════════════════════════════════════════════
# SHA-256 Correctness Tests
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestSpectralSHA256(unittest.TestCase):
    """SHA-256 using only Fabric primitives must match hashlib."""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=10240)
        self.hasher = SpectralSHA256(self.fabric)

    def test_empty_string(self):
        """SHA-256 of empty string."""
        expected = hashlib.sha256(b"").hexdigest()
        result = self.hasher.hash_hex(b"")
        self.assertEqual(result, expected)
        print(f"\n  SHA-256(''): {result[:16]}... ✓")

    def test_hello_world(self):
        """SHA-256 of 'hello world'."""
        msg = b"hello world"
        expected = hashlib.sha256(msg).hexdigest()
        result = self.hasher.hash_hex(msg)
        self.assertEqual(result, expected)
        print(f"\n  SHA-256('hello world'): {result[:16]}... ✓")

    def test_abc(self):
        """SHA-256 of 'abc' — NIST test vector."""
        expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        result = self.hasher.hash_hex(b"abc")
        self.assertEqual(result, expected)

    def test_single_byte(self):
        """SHA-256 of single byte."""
        expected = hashlib.sha256(b"\x00").hexdigest()
        result = self.hasher.hash_hex(b"\x00")
        self.assertEqual(result, expected)

    def test_55_bytes(self):
        """55 bytes — fits in one block with padding."""
        msg = b"A" * 55
        expected = hashlib.sha256(msg).hexdigest()
        result = self.hasher.hash_hex(msg)
        self.assertEqual(result, expected)

    def test_56_bytes(self):
        """56 bytes — requires two blocks (boundary case)."""
        msg = b"B" * 56
        expected = hashlib.sha256(msg).hexdigest()
        result = self.hasher.hash_hex(msg)
        self.assertEqual(result, expected)

    def test_64_bytes(self):
        """64 bytes — exactly one full block before padding."""
        msg = b"C" * 64
        expected = hashlib.sha256(msg).hexdigest()
        result = self.hasher.hash_hex(msg)
        self.assertEqual(result, expected)

    def test_long_message(self):
        """256 bytes — multiple blocks."""
        msg = b"D" * 256
        expected = hashlib.sha256(msg).hexdigest()
        result = self.hasher.hash_hex(msg)
        self.assertEqual(result, expected)

    def test_double_sha256(self):
        """Bitcoin double-SHA-256."""
        msg = b"bitcoin block header"
        expected = hashlib.sha256(hashlib.sha256(msg).digest()).hexdigest()
        result = self.hasher.double_hash(msg).hex()
        self.assertEqual(result, expected)
        print(f"\n  Double-SHA-256: {result[:16]}... ✓")

    def test_gpu_matches_cpu(self):
        """GPU-accelerated hash must match CPU hash."""
        msg = b"gpu vs cpu test"
        cpu_result = self.hasher.hash_hex(msg)
        gpu_result = self.hasher.hash_gpu(msg).hex()
        self.assertEqual(cpu_result, gpu_result)
        print(f"\n  GPU matches CPU: {gpu_result[:16]}... ✓")


# ═══════════════════════════════════════════════════════════════════════════
# Benchmark
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestSpectralSHA256Benchmark(unittest.TestCase):
    """Performance comparison: Spectral vs hashlib vs existing SHA-256 shader."""

    def test_benchmark(self):
        """Compare spectral SHA-256 throughput."""
        fabric = SoundBlockFabric(max_size=10240)
        hasher = SpectralSHA256(fabric)

        msg = b"benchmark test message for sha256"

        # hashlib (C implementation)
        t0 = time.perf_counter()
        for _ in range(1000):
            hashlib.sha256(msg).digest()
        hashlib_ms = (time.perf_counter() - t0) * 1000

        # Spectral SHA-256
        # Warmup
        hasher.hash(msg)
        t0 = time.perf_counter()
        n_spectral = 10
        for _ in range(n_spectral):
            hasher.hash(msg)
        spectral_ms = (time.perf_counter() - t0) * 1000

        # Spectral GPU
        hasher.hash_gpu(msg)
        t0 = time.perf_counter()
        for _ in range(n_spectral):
            hasher.hash_gpu(msg)
        gpu_ms = (time.perf_counter() - t0) * 1000

        print(f"\n  SHA-256 Benchmark:")
        print(f"    hashlib (C):      {hashlib_ms/1000:.3f}ms/hash (1000 hashes)")
        print(f"    Spectral (CPU):   {spectral_ms/n_spectral:.1f}ms/hash")
        print(f"    Spectral (GPU):   {gpu_ms/n_spectral:.1f}ms/hash")
        print(f"    ASIC (S21 XP):    ~0.000002ms/hash (270 TH/s)")
        print(f"    Existing shader:  ~0.009ms/hash (109.7 MH/s)")
        print(f"\n  This proves ASIC pipeline virtualization, not performance parity.")
        print(f"  The value: every ASIC gate IS a Sound Block operation.")


# ═══════════════════════════════════════════════════════════════════════════
# Stage 1: Optimized — zero per-round GPU transfer
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestOptimizedSHA256(unittest.TestCase):
    """Stage 1: hash_optimized with zero per-round transfers."""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=10240)
        self.hasher = SpectralSHA256(self.fabric)

    def test_optimized_empty(self):
        """Optimized matches hashlib on empty string."""
        expected = hashlib.sha256(b"").hexdigest()
        result = self.hasher.hash_optimized(b"").hex()
        self.assertEqual(result, expected)

    def test_optimized_abc(self):
        """NIST test vector."""
        expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        result = self.hasher.hash_optimized(b"abc").hex()
        self.assertEqual(result, expected)

    def test_optimized_matches_original(self):
        """Optimized must be bit-exact vs original for all test cases."""
        test_msgs = [b"", b"abc", b"hello world", b"\x00", b"A" * 55,
                     b"B" * 56, b"C" * 64, b"D" * 256]
        for msg in test_msgs:
            original = self.hasher.hash(msg)
            optimized = self.hasher.hash_optimized(msg)
            self.assertEqual(original, optimized,
                f"Mismatch for message len={len(msg)}")

    def test_optimized_double_sha256(self):
        """Bitcoin double-hash, optimized."""
        msg = b"bitcoin block header test"
        expected = hashlib.sha256(hashlib.sha256(msg).digest()).hexdigest()
        result = self.hasher.double_hash_optimized(msg).hex()
        self.assertEqual(result, expected)

    def test_optimized_benchmark(self):
        """Stage 1 benchmark: optimized vs original vs hashlib."""
        msg = b"benchmark test message for sha256"

        # hashlib
        t0 = time.perf_counter()
        for _ in range(10000):
            hashlib.sha256(msg).digest()
        hashlib_us = (time.perf_counter() - t0) * 1e6 / 10000

        # Original spectral (with GPU transfers)
        self.hasher.hash(msg)  # warmup
        t0 = time.perf_counter()
        n = 5
        for _ in range(n):
            self.hasher.hash(msg)
        original_ms = (time.perf_counter() - t0) * 1000 / n

        # Optimized (zero transfers)
        self.hasher.hash_optimized(msg)  # warmup
        t0 = time.perf_counter()
        n_opt = 100
        for _ in range(n_opt):
            self.hasher.hash_optimized(msg)
        optimized_ms = (time.perf_counter() - t0) * 1000 / n_opt

        speedup = original_ms / max(optimized_ms, 0.001)
        rate = 1000 / optimized_ms

        print(f"\n  Stage 1 Benchmark:")
        print(f"    hashlib (C):       {hashlib_us:.1f}us/hash ({1e6/hashlib_us:.0f} H/s)")
        print(f"    Original spectral: {original_ms:.1f}ms/hash ({1000/original_ms:.0f} H/s)")
        print(f"    Optimized (no xfer): {optimized_ms:.2f}ms/hash ({rate:.0f} H/s)")
        print(f"    Speedup: {speedup:.1f}x")
        print(f"    Existing shader:   ~0.009ms/hash (109.7 MH/s)")

    def test_batch_hashing(self):
        """Batch hash multiple messages."""
        msgs = [f"message {i}".encode() for i in range(10)]
        results = self.hasher.hash_batch(msgs)
        for i, msg in enumerate(msgs):
            expected = hashlib.sha256(msg).digest()
            self.assertEqual(results[i], expected)


# ═══════════════════════════════════════════════════════════════════════════
# Stage 2: Validate Spectral Width Scaling
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestSpectralWidthValidation(unittest.TestCase):
    """Stage 2: Does throughput scale linearly with N parallel streams?"""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=1_048_576)

    def test_width_scaling(self):
        """Measure GPU op throughput at increasing spectral width."""
        print(f"\n  Spectral Width Scaling (XOR across N×8 elements):")
        print(f"    {'N':>8}  {'Elements':>10}  {'Time':>10}  {'Throughput':>15}  {'Per-hash-eq':>12}")

        results = []
        for N in [1, 10, 100, 1000, 10000, 100000]:
            n_elem = N * 8  # 8 working variables per hash
            if n_elem > self.fabric._max_size:
                break

            a = np.random.randn(n_elem).astype(np.float32)
            b = np.random.randn(n_elem).astype(np.float32)
            self.fabric.upload('a', a)
            self.fabric.upload('b', b)
            self.fabric._slot_sizes['a'] = n_elem
            self.fabric._slot_sizes['b'] = n_elem

            # Warmup
            self.fabric.xor_gpu('a', 'b', 'out')

            # Measure
            times = []
            for _ in range(20):
                t0 = time.perf_counter()
                self.fabric.xor_gpu('a', 'b', 'out')
                times.append((time.perf_counter() - t0) * 1000)

            avg_ms = np.mean(times)
            throughput = n_elem / avg_ms * 1000
            per_hash_us = avg_ms * 1000 / N  # us per "hash equivalent"
            results.append((N, n_elem, avg_ms, throughput, per_hash_us))

            print(f"    {N:>8}  {n_elem:>10}  {avg_ms:>9.3f}ms  "
                  f"{throughput:>13,.0f}/sec  {per_hash_us:>10.1f}us")

        # Verify sub-linear or linear scaling
        if len(results) >= 3:
            # 100x more elements should cost <10x more time (sub-linear)
            ratio_elem = results[-1][1] / results[0][1]
            ratio_time = results[-1][2] / max(results[0][2], 0.001)
            scaling = ratio_time / ratio_elem
            print(f"\n    Scaling efficiency: {ratio_elem:.0f}x elements, "
                  f"{ratio_time:.1f}x time = {scaling:.3f} (1.0=linear, <1.0=sub-linear)")

    def test_width_memory(self):
        """Memory consumption at different widths."""
        print(f"\n  Spectral Width Memory (8 u32 per hash × N):")
        for N in [1, 100, 10000, 100000]:
            bytes_needed = N * 8 * 4  # N hashes × 8 vars × 4 bytes
            print(f"    N={N:>7}: {bytes_needed/1024:.1f} KB "
                  f"({bytes_needed/1e6:.2f} MB)")
        print(f"    Available: {self.fabric._max_size * 4 / 1e6:.0f} MB per slot")


# ═══════════════════════════════════════════════════════════════════════════
# Stage 3: Fused GPU SHA-256 — N parallel hashes in one dispatch
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestFusedSHA256(unittest.TestCase):
    """Stage 3: All 64 rounds + N parallel hashes in one GPU dispatch."""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=1_048_576)
        self.hasher = SpectralSHA256(self.fabric)

    def test_fused_single(self):
        """Single hash via fused shader matches hashlib."""
        msg = b"hello world"
        expected = hashlib.sha256(msg).digest()
        results = self.hasher.hash_fused([msg])
        self.assertEqual(results[0], expected)
        print(f"\n  Fused single: {results[0].hex()[:16]}... ✓")

    def test_fused_abc(self):
        """NIST test vector via fused shader."""
        expected = hashlib.sha256(b"abc").digest()
        results = self.hasher.hash_fused([b"abc"])
        self.assertEqual(results[0], expected)

    def test_fused_empty(self):
        """Empty string via fused shader."""
        expected = hashlib.sha256(b"").digest()
        results = self.hasher.hash_fused([b""])
        self.assertEqual(results[0], expected)

    def test_fused_batch_10(self):
        """10 different messages, all correct."""
        msgs = [f"message {i}".encode() for i in range(10)]
        results = self.hasher.hash_fused(msgs)
        for i, msg in enumerate(msgs):
            expected = hashlib.sha256(msg).digest()
            self.assertEqual(results[i], expected,
                f"Mismatch at message {i}: {msg}")

    def test_fused_batch_100(self):
        """100 messages in one dispatch."""
        msgs = [f"batch100_{i:04d}".encode() for i in range(100)]
        results = self.hasher.hash_fused(msgs)
        for i, msg in enumerate(msgs):
            expected = hashlib.sha256(msg).digest()
            self.assertEqual(results[i], expected,
                f"Mismatch at message {i}")
        print(f"\n  Fused batch 100: all correct ✓")

    def test_fused_benchmark(self):
        """Stage 3 benchmark: fused vs optimized vs hashlib."""
        # Prepare batch
        msgs = [f"bench_{i:06d}".encode() for i in range(1000)]

        # hashlib batch
        t0 = time.perf_counter()
        for msg in msgs:
            hashlib.sha256(msg).digest()
        hashlib_ms = (time.perf_counter() - t0) * 1000

        # Optimized batch (Python loop)
        t0 = time.perf_counter()
        self.hasher.hash_batch(msgs[:100])
        opt_ms = (time.perf_counter() - t0) * 1000

        # Fused GPU (one dispatch)
        self.hasher.hash_fused(msgs[:10])  # warmup
        t0 = time.perf_counter()
        self.hasher.hash_fused(msgs)
        fused_ms = (time.perf_counter() - t0) * 1000

        fused_rate = 1000 / (fused_ms / 1000)
        opt_rate = 100 / (opt_ms / 1000)

        print(f"\n  Stage 3 Fused Benchmark (N=1000):")
        print(f"    hashlib (C):    {hashlib_ms:.1f}ms for 1000 ({1000/(hashlib_ms/1000):.0f} H/s)")
        print(f"    Optimized:      {opt_ms:.1f}ms for 100 ({opt_rate:.0f} H/s)")
        print(f"    Fused GPU:      {fused_ms:.1f}ms for 1000 ({fused_rate:.0f} H/s)")
        print(f"    Speedup vs opt: {(opt_ms/100)/(fused_ms/1000):.1f}x")

    def test_fused_scaling(self):
        """Measure fused throughput at different batch sizes."""
        print(f"\n  Fused Scaling:")
        print(f"    {'N':>8}  {'Time':>10}  {'H/s':>12}  {'Per-hash':>10}")

        for N in [1, 10, 100, 1000, 5000]:
            msgs = [f"scale_{i:06d}".encode() for i in range(N)]
            # warmup
            if N <= 100:
                self.hasher.hash_fused(msgs)
            t0 = time.perf_counter()
            self.hasher.hash_fused(msgs)
            ms = (time.perf_counter() - t0) * 1000
            rate = N / (ms / 1000)
            per_hash = ms / N
            print(f"    {N:>8}  {ms:>9.1f}ms  {rate:>10,.0f}  {per_hash:>9.3f}ms")


# ═══════════════════════════════════════════════════════════════════════════
# Mining: Nonce Loop Sound Block — Closes the 440x Gap
# ═══════════════════════════════════════════════════════════════════════════

@unittest.skipUnless(gpu_available(), "No GPU available")
class TestSpectralMining(unittest.TestCase):
    """Mining with inner nonce loop — the 440x roadblock Sound Blocked."""

    def setUp(self):
        self.fabric = SoundBlockFabric(max_size=1_048_576)
        self.hasher = SpectralSHA256(self.fabric)

    def test_mine_basic(self):
        """Mine with low difficulty — should find nonce quickly."""
        header = b"test block header for spectral mining benchmark!!" + b"\x00" * 14
        result = self.hasher.mine_spectral(
            header, difficulty=4, threads=1024, nonces_per_thread=32)

        self.assertGreater(result['hashes_computed'], 0)
        print(f"\n  Spectral Mining (difficulty=4):")
        print(f"    Hashes: {result['hashes_computed']:,}")
        print(f"    Time: {result['total_ms']:.1f}ms")
        print(f"    Rate: {result['mh_per_sec']:.2f} MH/s")
        print(f"    Found: {result['found']}")
        if result['found']:
            print(f"    Nonce: {result['nonce']}")
            print(f"    Hash: {result['hash_hex'][:16]}...")

    def test_mine_benchmark(self):
        """Benchmark: spectral mining at full scale vs existing shader."""
        header = b"Sound Block spectral mining benchmark header!!!!!" + b"\x00" * 14

        # Scale up: 262K threads × 32 nonces = 8.39M hashes
        result = self.hasher.mine_spectral(
            header, difficulty=32,  # impossible difficulty — just benchmarking speed
            threads=262144, nonces_per_thread=32)

        print(f"\n  ═══ SPECTRAL MINING BENCHMARK ═══")
        print(f"    Threads: {262144:,}")
        print(f"    Nonces/thread: 32")
        print(f"    Total hashes: {result['hashes_computed']:,}")
        print(f"    Time: {result['total_ms']:.1f}ms")
        print(f"    Hash rate: {result['mh_per_sec']:.1f} MH/s")
        print(f"    Existing shader: 109.7 MH/s")
        print(f"    Gap: {109.7 / max(result['mh_per_sec'], 0.01):.1f}x")
        print(f"    (was 440x — Sound Blocked)")

    def test_mine_scaling(self):
        """Test mining at different nonces_per_thread depths."""
        header = b"scaling test header for spectral SHA-256 mining!" + b"\x00" * 14

        print(f"\n  Mining Batch Depth Scaling:")
        print(f"    {'N/thread':>10}  {'Threads':>10}  {'Total':>12}  {'Time':>8}  {'MH/s':>8}")

        for npt in [1, 4, 16, 32, 64]:
            threads = min(262144, 1048576 // npt)  # cap total work
            result = self.hasher.mine_spectral(
                header, difficulty=32, threads=threads, nonces_per_thread=npt)
            total = result['hashes_computed']
            print(f"    {npt:>10}  {threads:>10,}  {total:>12,}  "
                  f"{result['total_ms']:>7.1f}ms  {result['mh_per_sec']:>7.1f}")


if __name__ == "__main__":
    unittest.main(verbosity=2)
