mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 23:04:46 -07:00
global: fixes
This commit is contained in:
@@ -2,14 +2,51 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_fee_rates(brk, mempool, period):
|
||||
"""Block fee-rate percentiles must have the same element structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
PERCENTILES = ["avgFee_0", "avgFee_10", "avgFee_25", "avgFee_50", "avgFee_75", "avgFee_90", "avgFee_100"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_blocks_fee_rates_structure(brk, mempool, period):
|
||||
"""Block fee-rate percentiles envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/blocks/fee-rates/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_block_fee_rates(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_blocks_fee_rates_invariants(brk):
|
||||
"""Series ordering, percentile monotonicity, non-negative rates (period=1m)."""
|
||||
period = "1m"
|
||||
b = brk.get_block_fee_rates(period)
|
||||
show("GET", f"/api/v1/mining/blocks/fee-rates/{period}", summary(b), "-")
|
||||
assert len(b) > 0, "expected non-empty fee-rates series for 1m"
|
||||
heights = [entry["avgHeight"] for entry in b]
|
||||
timestamps = [entry["timestamp"] for entry in b]
|
||||
assert heights == sorted(heights), "avgHeight not ascending"
|
||||
assert timestamps == sorted(timestamps), "timestamps not ascending"
|
||||
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
|
||||
for entry in b:
|
||||
values = [entry[k] for k in PERCENTILES]
|
||||
assert values == sorted(values), (
|
||||
f"percentiles not monotonically non-decreasing at height {entry['avgHeight']}: {values}"
|
||||
)
|
||||
for k in PERCENTILES:
|
||||
assert entry[k] >= 0, f"negative fee rate {k}={entry[k]} at {entry['avgHeight']}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_blocks_fee_rates_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/blocks/fee-rates/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,46 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_fees(brk, mempool, period):
|
||||
"""Average block fees must have the same element structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_blocks_fees_structure(brk, mempool, period):
|
||||
"""Average block fees envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/blocks/fees/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_block_fees(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_blocks_fees_invariants(brk):
|
||||
"""Series ascending by height and timestamp, fees and USD non-negative (period=1m)."""
|
||||
period = "1m"
|
||||
b = brk.get_block_fees(period)
|
||||
show("GET", f"/api/v1/mining/blocks/fees/{period}", summary(b), "-")
|
||||
assert len(b) > 0, "expected non-empty fees series for 1m"
|
||||
heights = [entry["avgHeight"] for entry in b]
|
||||
timestamps = [entry["timestamp"] for entry in b]
|
||||
assert heights == sorted(heights), "avgHeight not ascending"
|
||||
assert timestamps == sorted(timestamps), "timestamps not ascending"
|
||||
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
|
||||
for entry in b:
|
||||
assert entry["avgFees"] >= 0, f"negative avgFees: {entry}"
|
||||
assert entry["USD"] >= 0, f"negative USD: {entry}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_blocks_fees_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/blocks/fees/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,46 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_rewards(brk, mempool, period):
|
||||
"""Average block rewards must have the same element structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_blocks_rewards_structure(brk, mempool, period):
|
||||
"""Average block rewards envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/blocks/rewards/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_block_rewards(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_blocks_rewards_invariants(brk):
|
||||
"""Series ascending by height and timestamp, rewards positive, USD non-negative (period=1m)."""
|
||||
period = "1m"
|
||||
b = brk.get_block_rewards(period)
|
||||
show("GET", f"/api/v1/mining/blocks/rewards/{period}", summary(b), "-")
|
||||
assert len(b) > 0, "expected non-empty rewards series for 1m"
|
||||
heights = [entry["avgHeight"] for entry in b]
|
||||
timestamps = [entry["timestamp"] for entry in b]
|
||||
assert heights == sorted(heights), "avgHeight not ascending"
|
||||
assert timestamps == sorted(timestamps), "timestamps not ascending"
|
||||
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
|
||||
for entry in b:
|
||||
assert entry["avgRewards"] > 0, f"non-positive avgRewards: {entry}"
|
||||
assert entry["USD"] >= 0, f"negative USD: {entry}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_blocks_rewards_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/blocks/rewards/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,60 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
|
||||
def test_mining_blocks_sizes_weights(brk, mempool, period):
|
||||
"""Block sizes and weights must have the same structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
MAX_BLOCK_WEIGHT = 4_000_000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_blocks_sizes_weights_structure(brk, mempool, period):
|
||||
"""Combined sizes/weights envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/blocks/sizes-weights/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_block_sizes_weights(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, dict) and isinstance(m, dict)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_blocks_sizes_weights_invariants(brk):
|
||||
"""Parallel arrays, ascending order, positive size, weight in (0, 4M] (period=1m)."""
|
||||
period = "1m"
|
||||
b = brk.get_block_sizes_weights(period)
|
||||
sizes = b["sizes"]
|
||||
weights = b["weights"]
|
||||
show("GET", f"/api/v1/mining/blocks/sizes-weights/{period}", summary(b), "-")
|
||||
assert len(sizes) > 0, "expected non-empty sizes series for 1m"
|
||||
assert len(sizes) == len(weights), (
|
||||
f"sizes/weights array lengths diverge: {len(sizes)} vs {len(weights)}"
|
||||
)
|
||||
size_heights = [e["avgHeight"] for e in sizes]
|
||||
size_ts = [e["timestamp"] for e in sizes]
|
||||
assert size_heights == sorted(size_heights), "size avgHeights not ascending"
|
||||
assert size_ts == sorted(size_ts), "size timestamps not ascending"
|
||||
assert len(set(size_heights)) == len(size_heights), "duplicate avgHeight in sizes"
|
||||
for s, w in zip(sizes, weights):
|
||||
assert s["avgHeight"] == w["avgHeight"], (
|
||||
f"size/weight height misalignment: {s['avgHeight']} vs {w['avgHeight']}"
|
||||
)
|
||||
assert s["timestamp"] == w["timestamp"], (
|
||||
f"size/weight timestamp misalignment at height {s['avgHeight']}"
|
||||
)
|
||||
assert s["avgSize"] > 0, f"non-positive avgSize at {s['avgHeight']}: {s['avgSize']}"
|
||||
assert 0 < w["avgWeight"] <= MAX_BLOCK_WEIGHT, (
|
||||
f"avgWeight out of range at {w['avgHeight']}: {w['avgWeight']}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_blocks_sizes_weights_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/blocks/sizes-weights/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,15 +1,52 @@
|
||||
"""GET /api/v1/mining/blocks/timestamp/{timestamp}"""
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, assert_same_values, show
|
||||
|
||||
|
||||
def test_mining_blocks_timestamp(brk, mempool, live):
|
||||
"""Block lookup by timestamp must have the same structure for various eras."""
|
||||
GENESIS_TIMESTAMP = 1231006505
|
||||
|
||||
|
||||
def test_mining_blocks_timestamp_structure_and_parity(brk, mempool, live):
|
||||
"""For each live era, brk and mempool must resolve the same block."""
|
||||
for block in live.blocks:
|
||||
info = brk.get_json(f"/api/block/{block.hash}")
|
||||
ts = info["timestamp"]
|
||||
path = f"/api/v1/mining/blocks/timestamp/{ts}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_block_by_timestamp(ts)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
def test_mining_blocks_timestamp_round_trip(brk, live):
|
||||
"""Looking up a block's own timestamp must return that block (or an earlier one with same ts)."""
|
||||
for block in live.blocks:
|
||||
info = brk.get_json(f"/api/block/{block.hash}")
|
||||
ts = info["timestamp"]
|
||||
b = brk.get_block_by_timestamp(ts)
|
||||
show("GET", f"/api/v1/mining/blocks/timestamp/{ts}", b, "-")
|
||||
assert b["height"] <= block.height, (
|
||||
f"resolved height {b['height']} > requested block height {block.height}"
|
||||
)
|
||||
|
||||
|
||||
def test_mining_blocks_timestamp_genesis(brk):
|
||||
"""Genesis Unix timestamp must resolve to genesis (height 0)."""
|
||||
b = brk.get_block_by_timestamp(GENESIS_TIMESTAMP)
|
||||
show("GET", f"/api/v1/mining/blocks/timestamp/{GENESIS_TIMESTAMP}", b, "-")
|
||||
assert b["height"] == 0, f"genesis ts must resolve to height 0, got {b['height']}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["abc", "-1"])
|
||||
def test_mining_blocks_timestamp_malformed(brk, bad):
|
||||
"""Non-numeric or negative timestamp must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/blocks/timestamp/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,53 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
|
||||
def test_mining_difficulty_adjustments(brk, mempool, period):
|
||||
"""Historical difficulty adjustments must have the same structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
RETARGET_INTERVAL = 2016
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_difficulty_adjustments_structure(brk, mempool, period):
|
||||
"""Historical difficulty adjustments envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/difficulty-adjustments/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_difficulty_adjustments_by_period(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_difficulty_adjustments_invariants(brk):
|
||||
"""Tip-first ordering, retarget-aligned heights, genesis sentinel (period=all)."""
|
||||
period = "all"
|
||||
b = brk.get_difficulty_adjustments_by_period(period)
|
||||
show("GET", f"/api/v1/mining/difficulty-adjustments/{period}", summary(b), "-")
|
||||
assert len(b) > 0, "expected non-empty difficulty adjustments for period=all"
|
||||
heights = [entry[1] for entry in b]
|
||||
assert heights == sorted(heights, reverse=True), "entries not descending by height"
|
||||
assert len(set(heights)) == len(heights), "duplicate heights in series"
|
||||
assert heights[-1] == 0, f"last entry must be genesis (height 0), got {heights[-1]}"
|
||||
assert heights.count(0) == 1, "expected exactly one genesis entry"
|
||||
for entry in b[:-1]:
|
||||
timestamp, height, difficulty, change_ratio = entry
|
||||
assert height % RETARGET_INTERVAL == 0, (
|
||||
f"non-genesis height {height} not on retarget boundary"
|
||||
)
|
||||
assert difficulty > 0, f"non-positive difficulty: {difficulty} at height {height}"
|
||||
assert change_ratio > 0, f"non-positive change ratio: {change_ratio} at height {height}"
|
||||
genesis = b[-1]
|
||||
assert genesis[2] == 1.0, f"genesis difficulty must be 1.0, got {genesis[2]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_difficulty_adjustments_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/difficulty-adjustments/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,52 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
|
||||
def test_mining_hashrate(brk, mempool, period):
|
||||
"""Network hashrate + difficulty must have the same structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_hashrate_structure(brk, mempool, period):
|
||||
"""Network hashrate envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/hashrate/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_hashrate_by_period(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_hashrate_invariants(brk):
|
||||
"""Series ascending, values positive, current* fields populated (period=1m)."""
|
||||
period = "1m"
|
||||
b = brk.get_hashrate_by_period(period)
|
||||
show("GET", f"/api/v1/mining/hashrate/{period}", summary(b), "-")
|
||||
assert isinstance(b["currentHashrate"], int) and b["currentHashrate"] > 0
|
||||
assert isinstance(b["currentDifficulty"], (int, float)) and b["currentDifficulty"] > 0
|
||||
hashrates = b["hashrates"]
|
||||
assert len(hashrates) > 0, "expected non-empty hashrates list for 1m"
|
||||
timestamps = [h["timestamp"] for h in hashrates]
|
||||
assert timestamps == sorted(timestamps), "hashrate timestamps not ascending"
|
||||
assert len(set(timestamps)) == len(timestamps), "duplicate hashrate timestamps"
|
||||
for h in hashrates:
|
||||
assert isinstance(h["avgHashrate"], int) and h["avgHashrate"] > 0
|
||||
difficulty = b["difficulty"]
|
||||
times = [d["time"] for d in difficulty]
|
||||
heights = [d["height"] for d in difficulty]
|
||||
assert times == sorted(times), "difficulty entries not ascending by time"
|
||||
assert heights == sorted(heights), "difficulty entries not ascending by height"
|
||||
for d in difficulty:
|
||||
assert d["difficulty"] > 0, f"non-positive difficulty: {d}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_hashrate_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/hashrate/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,48 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "1y"])
|
||||
def test_mining_hashrate_pools(brk, mempool, period):
|
||||
"""Per-pool hashrate must have the same structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_hashrate_pools_structure(brk, mempool, period):
|
||||
"""Per-pool hashrate snapshot envelope must match across all periods."""
|
||||
path = f"/api/v1/mining/hashrate/pools/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pools_hashrate_by_period(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_hashrate_pools_invariants(brk):
|
||||
"""Snapshot has single timestamp, valid shares summing to <=1, unique pool names (period=1w)."""
|
||||
period = "1w"
|
||||
b = brk.get_pools_hashrate_by_period(period)
|
||||
show("GET", f"/api/v1/mining/hashrate/pools/{period}", summary(b), "-")
|
||||
assert len(b) > 0, "expected non-empty per-pool hashrate snapshot for 1w"
|
||||
timestamps = {entry["timestamp"] for entry in b}
|
||||
assert len(timestamps) == 1, f"expected single snapshot timestamp, got {timestamps}"
|
||||
pool_names = [entry["poolName"] for entry in b]
|
||||
assert len(set(pool_names)) == len(pool_names), "duplicate poolName in snapshot"
|
||||
for entry in b:
|
||||
assert entry["poolName"], "empty poolName"
|
||||
assert isinstance(entry["avgHashrate"], int) and entry["avgHashrate"] >= 0
|
||||
assert isinstance(entry["share"], (int, float)) and 0.0 <= entry["share"] <= 1.0
|
||||
total_share = sum(entry["share"] for entry in b)
|
||||
assert total_share <= 1.0001, f"share sum > 1: {total_share}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_hashrate_pools_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/hashrate/pools/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,73 @@
|
||||
"""GET /api/v1/mining/pool/{slug}"""
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, assert_same_values, show, summary
|
||||
|
||||
|
||||
def test_mining_pool_detail(brk, mempool, pool_slugs):
|
||||
"""Pool detail must have the same structure for top pools."""
|
||||
# Tip-race / mempool-only / int-vs-str fields excluded from value equality.
|
||||
VOLATILE = {
|
||||
"blockCount", "blockShare", "estimatedHashrate", "reportedHashrate",
|
||||
"totalReward", "avgBlockHealth", "avgMatchRate", "avgFeeDelta",
|
||||
}
|
||||
|
||||
# Digit/punctuation slugs that previously diverged between brk and mempool.
|
||||
# Pinning them here lets the slug rename fixes regress loudly if reverted.
|
||||
SLUG_RENAME_REGRESSION_GUARD = ["1thash", "175btc", "21inc", "1hash", "58coin", "7pool"]
|
||||
|
||||
|
||||
def test_mining_pool_detail_structure(brk, mempool, pool_slugs):
|
||||
"""Pool detail envelope must match mempool for the top active pools."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pool(slug)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_detail_static_fields(brk, mempool, pool_slug):
|
||||
"""The pool registry fields (id, name, link, slug, unique_id) must value-match."""
|
||||
path = f"/api/v1/mining/pool/{pool_slug}"
|
||||
b = brk.get_pool(pool_slug)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b["pool"], m["pool"])
|
||||
assert_same_values(b["pool"], m["pool"], path=f"{path}.pool")
|
||||
|
||||
|
||||
def test_mining_pool_detail_invariants(brk, pool_slug):
|
||||
"""blockCount monotonic by window; blockShare in [0,1]; pool.slug round-trips."""
|
||||
b = brk.get_pool(pool_slug)
|
||||
show("GET", f"/api/v1/mining/pool/{pool_slug}", summary(b), "-")
|
||||
assert b["pool"]["slug"] == pool_slug, (
|
||||
f"response.pool.slug={b['pool']['slug']!r} vs URL slug={pool_slug!r}"
|
||||
)
|
||||
bc = b["blockCount"]
|
||||
assert bc["all"] >= bc["1w"] >= bc["24h"] >= 0, f"blockCount not monotonic: {bc}"
|
||||
bs = b["blockShare"]
|
||||
for window, value in bs.items():
|
||||
assert 0.0 <= value <= 1.0, f"blockShare[{window}]={value} out of [0,1]"
|
||||
assert isinstance(b["estimatedHashrate"], int) and b["estimatedHashrate"] >= 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("slug", SLUG_RENAME_REGRESSION_GUARD)
|
||||
def test_mining_pool_detail_slug_renames(brk, mempool, slug):
|
||||
"""Pools whose slugs were renamed to match mempool must remain reachable."""
|
||||
path = f"/api/v1/mining/pool/{slug}"
|
||||
b = brk.get_pool(slug)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b["pool"], m["pool"])
|
||||
assert b["pool"]["slug"] == slug
|
||||
assert_same_values(b["pool"], m["pool"], path=f"{path}.pool")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA", ""])
|
||||
def test_mining_pool_detail_malformed(brk, bad):
|
||||
"""Unknown slug must produce BrkError(status=400 or 404)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pool/{bad}")
|
||||
assert exc_info.value.status in (400, 404), (
|
||||
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
"""GET /api/v1/mining/pool/{slug}/blocks"""
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
def test_mining_pool_blocks(brk, mempool, pool_slugs):
|
||||
"""Recent blocks by pool must have the same element structure."""
|
||||
PAGE_SIZE = 100
|
||||
|
||||
|
||||
def test_mining_pool_blocks_structure(brk, mempool, pool_slugs):
|
||||
"""Per-pool block list element schema must match for top active pools."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}/blocks"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pool_blocks(slug)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_blocks_invariants(brk, pool_slug):
|
||||
"""Page is descending, capped at 100, all blocks attributed to the requested pool."""
|
||||
b = brk.get_pool_blocks(pool_slug)
|
||||
show("GET", f"/api/v1/mining/pool/{pool_slug}/blocks", f"({len(b)} blocks)", "-")
|
||||
assert 0 < len(b) <= PAGE_SIZE, f"unexpected length: {len(b)}"
|
||||
heights = [blk["height"] for blk in b]
|
||||
assert heights == sorted(heights, reverse=True), f"not tip-first: {heights[:5]}..."
|
||||
assert len(set(heights)) == len(heights), "duplicate heights in page"
|
||||
for blk in b:
|
||||
assert blk["stale"] is False, f"stale block in page: {blk['id']}"
|
||||
assert blk["extras"]["pool"]["slug"] == pool_slug, (
|
||||
f"block {blk['id']} attributed to {blk['extras']['pool']['slug']}, "
|
||||
f"expected {pool_slug}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA"])
|
||||
def test_mining_pool_blocks_malformed(brk, bad):
|
||||
"""Unknown slug must produce BrkError(status=400 or 404)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pool/{bad}/blocks")
|
||||
assert exc_info.value.status in (400, 404), (
|
||||
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,15 +1,61 @@
|
||||
"""GET /api/v1/mining/pool/{slug}/blocks/{height}"""
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
def test_mining_pool_blocks_at_height(brk, mempool, pool_slug, live):
|
||||
"""Pool blocks before various heights must have the same element structure."""
|
||||
for block in live.blocks[::2]: # every other block, to keep run-time bounded
|
||||
path = f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}"
|
||||
b = brk.get_json(path)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
if b and m:
|
||||
assert_same_structure(b[0], m[0])
|
||||
PAGE_SIZE = 100
|
||||
|
||||
|
||||
def test_mining_pool_blocks_from_height_structure(brk, mempool, pool_slug, block):
|
||||
"""Per-pool block list before a height must match mempool's element schema."""
|
||||
path = f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}"
|
||||
b = brk.get_pool_blocks_from(pool_slug, block.height)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_blocks_from_height_invariants(brk, pool_slug, block):
|
||||
"""Page is descending, capped at 100, height-bounded, attributed to the pool."""
|
||||
b = brk.get_pool_blocks_from(pool_slug, block.height)
|
||||
show("GET", f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}", f"({len(b)} blocks)", "-")
|
||||
assert 0 <= len(b) <= PAGE_SIZE, f"unexpected length: {len(b)}"
|
||||
if not b:
|
||||
return
|
||||
heights = [blk["height"] for blk in b]
|
||||
assert heights == sorted(heights, reverse=True), f"not descending: {heights[:5]}..."
|
||||
assert max(heights) <= block.height, (
|
||||
f"page contains height > requested {block.height}: max={max(heights)}"
|
||||
)
|
||||
assert len(set(heights)) == len(heights), "duplicate heights in page"
|
||||
for blk in b:
|
||||
assert blk["stale"] is False, f"stale block in page: {blk['id']}"
|
||||
assert blk["extras"]["pool"]["slug"] == pool_slug, (
|
||||
f"block {blk['id']} attributed to {blk['extras']['pool']['slug']}, "
|
||||
f"expected {pool_slug}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_slug", ["notapool", "FoundryUSA"])
|
||||
def test_mining_pool_blocks_from_height_malformed_slug(brk, bad_slug):
|
||||
"""Unknown slug must produce BrkError(status=400 or 404)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pool/{bad_slug}/blocks/100000")
|
||||
assert exc_info.value.status in (400, 404), (
|
||||
f"expected 400 or 404 for slug {bad_slug!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_height", ["-1", "abc"])
|
||||
def test_mining_pool_blocks_from_height_malformed_height(brk, pool_slug, bad_height):
|
||||
"""Negative or non-numeric height must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pool/{pool_slug}/blocks/{bad_height}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected 400 for height {bad_height!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
"""GET /api/v1/mining/pool/{slug}/hashrate"""
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
def test_mining_pool_hashrate(brk, mempool, pool_slugs):
|
||||
"""Pool hashrate history must have the same structure for top pools."""
|
||||
def test_mining_pool_hashrate_structure(brk, mempool, pool_slugs):
|
||||
"""Pool hashrate history element schema must match for top active pools."""
|
||||
for slug in pool_slugs:
|
||||
path = f"/api/v1/mining/pool/{slug}/hashrate"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pool_hashrate(slug)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert isinstance(b, list) and isinstance(m, list)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pool_hashrate_invariants(brk, pool_slug):
|
||||
"""Series must be non-empty, ascending in time, with valid hashrate/share/poolName."""
|
||||
b = brk.get_pool_hashrate(pool_slug)
|
||||
show("GET", f"/api/v1/mining/pool/{pool_slug}/hashrate", summary(b), "-")
|
||||
assert len(b) > 0, f"empty hashrate history for {pool_slug}"
|
||||
timestamps = [entry["timestamp"] for entry in b]
|
||||
assert timestamps == sorted(timestamps), "timestamps not ascending"
|
||||
assert len(set(timestamps)) == len(timestamps), "duplicate timestamps"
|
||||
pool_names = {entry["poolName"] for entry in b}
|
||||
assert len(pool_names) == 1, f"poolName not consistent across series: {pool_names}"
|
||||
for entry in b:
|
||||
assert isinstance(entry["avgHashrate"], int) and entry["avgHashrate"] >= 0
|
||||
assert isinstance(entry["share"], (int, float)) and 0.0 <= entry["share"] <= 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA"])
|
||||
def test_mining_pool_hashrate_malformed(brk, bad):
|
||||
"""Unknown slug must produce BrkError(status=400 or 404)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pool/{bad}/hashrate")
|
||||
assert exc_info.value.status in (400, 404), (
|
||||
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -3,43 +3,78 @@
|
||||
from _lib import assert_same_structure, show
|
||||
|
||||
|
||||
# Slugs present in brk's vendored pools-v2.json but reported under a different
|
||||
# slug (or missing) by mempool.space. Currently the duplicate-pool collision
|
||||
# case where brk preserves both `bitcoinindia` (variant 80) and
|
||||
# `bitcoinindiapool` (variant 134), while mempool emits both as `bitcoinindia`.
|
||||
KNOWN_BRK_ONLY_SLUGS = {"bitcoinindiapool"}
|
||||
|
||||
# Pools added upstream after brk's vendored pools-v2.json snapshot. Refresh
|
||||
# the vendored file (and update this set) when bumping the snapshot.
|
||||
KNOWN_MEMPOOL_ONLY_SLUGS = {
|
||||
"drdetroit", "emzy", "knorrium", "mononaut", "nymkappa", "rijndael",
|
||||
}
|
||||
|
||||
EXPECTED_MIN_POOLS = 165
|
||||
|
||||
|
||||
def test_mining_pools_list_structure(brk, mempool):
|
||||
"""Pool list must have the same element structure."""
|
||||
"""Pool list element schema must match (flat list, {name, slug, unique_id})."""
|
||||
path = "/api/v1/mining/pools"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pools()
|
||||
m = mempool.get_json(path)
|
||||
show(
|
||||
"GET", path,
|
||||
b[:3] if isinstance(b, list) else b,
|
||||
m[:3] if isinstance(m, list) else m,
|
||||
)
|
||||
show("GET", path, f"({len(b)} pools)", f"({len(m)} pools)", max_lines=4)
|
||||
assert isinstance(b, list) and isinstance(m, list), "both must be flat lists"
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def _pools(data):
|
||||
"""`pools` may live at the root or inside an envelope across versions."""
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return data.get("pools", []) if isinstance(data, dict) else []
|
||||
|
||||
|
||||
def test_mining_pools_list_fields(brk):
|
||||
"""Each pool entry must carry slug and name (period-less endpoint omits stats)."""
|
||||
b = _pools(brk.get_json("/api/v1/mining/pools"))
|
||||
show("GET", "/api/v1/mining/pools", f"({len(b)} pools)", "—")
|
||||
assert b, "no pools in brk's response"
|
||||
required = {"slug", "name"}
|
||||
for p in b[:5]:
|
||||
missing = required - set(p.keys())
|
||||
assert not missing, f"pool {p.get('slug', '?')} missing fields: {missing}"
|
||||
assert isinstance(p["name"], str) and p["name"]
|
||||
"""Every pool entry must carry a non-empty slug + name + non-negative unique_id."""
|
||||
b = brk.get_pools()
|
||||
show("GET", "/api/v1/mining/pools", f"({len(b)} pools)", "-")
|
||||
assert len(b) >= EXPECTED_MIN_POOLS, f"expected >= {EXPECTED_MIN_POOLS} pools, got {len(b)}"
|
||||
for p in b:
|
||||
assert isinstance(p["slug"], str) and p["slug"], f"bad slug: {p!r}"
|
||||
assert isinstance(p["name"], str) and p["name"], f"bad name: {p!r}"
|
||||
assert isinstance(p["unique_id"], int) and p["unique_id"] >= 0, (
|
||||
f"bad unique_id: {p!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_mining_pools_slugs_unique(brk):
|
||||
"""Pool slugs must be unique across the response."""
|
||||
b = _pools(brk.get_json("/api/v1/mining/pools"))
|
||||
b = brk.get_pools()
|
||||
slugs = [p["slug"] for p in b]
|
||||
show("GET", "/api/v1/mining/pools", f"({len(slugs)} slugs)", "—")
|
||||
show("GET", "/api/v1/mining/pools", f"({len(slugs)} slugs)", "-")
|
||||
assert len(slugs) == len(set(slugs)), (
|
||||
f"duplicate slugs: {len(slugs) - len(set(slugs))}"
|
||||
)
|
||||
|
||||
|
||||
def test_mining_pools_unique_ids_unique(brk):
|
||||
"""Pool unique_ids must be unique across the response."""
|
||||
b = brk.get_pools()
|
||||
ids = [p["unique_id"] for p in b]
|
||||
show("GET", "/api/v1/mining/pools", f"({len(ids)} unique_ids)", "-")
|
||||
assert len(ids) == len(set(ids)), (
|
||||
f"duplicate unique_ids: {len(ids) - len(set(ids))}"
|
||||
)
|
||||
|
||||
|
||||
def test_mining_pools_slugs_match_mempool(brk, mempool):
|
||||
"""brk's slug set must equal mempool's, modulo documented exceptions."""
|
||||
b_slugs = {p["slug"] for p in brk.get_pools()}
|
||||
m_slugs = {p["slug"] for p in mempool.get_json("/api/v1/mining/pools")}
|
||||
show(
|
||||
"GET", "/api/v1/mining/pools",
|
||||
f"brk-only={sorted(b_slugs - m_slugs)}",
|
||||
f"mempool-only={sorted(m_slugs - b_slugs)}",
|
||||
)
|
||||
unexpected_brk_only = (b_slugs - m_slugs) - KNOWN_BRK_ONLY_SLUGS
|
||||
unexpected_mempool_only = (m_slugs - b_slugs) - KNOWN_MEMPOOL_ONLY_SLUGS
|
||||
assert not unexpected_brk_only, (
|
||||
f"undocumented brk-only slugs (likely format divergence): {unexpected_brk_only}"
|
||||
)
|
||||
assert not unexpected_mempool_only, (
|
||||
f"undocumented mempool-only slugs (refresh pools-v2.json?): {unexpected_mempool_only}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,53 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, show, summary
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"])
|
||||
def test_mining_pools_by_period(brk, mempool, period):
|
||||
"""Pool stats for a time period must have the same structure."""
|
||||
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period", PERIODS)
|
||||
def test_mining_pools_by_period_structure(brk, mempool, period):
|
||||
"""Pool stats envelope must structurally match mempool across all periods."""
|
||||
path = f"/api/v1/mining/pools/{period}"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_pool_stats(period)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, summary(b), summary(m))
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
def test_mining_pools_by_period_invariants(brk):
|
||||
"""A single deep-period sanity pass on `1w`."""
|
||||
period = "1w"
|
||||
b = brk.get_pool_stats(period)
|
||||
show("GET", f"/api/v1/mining/pools/{period}", summary(b), "-")
|
||||
assert isinstance(b["blockCount"], int) and b["blockCount"] > 0
|
||||
assert isinstance(b["lastEstimatedHashrate"], int) and b["lastEstimatedHashrate"] > 0
|
||||
pools = b["pools"]
|
||||
assert pools, "expected non-empty pools list for 1w"
|
||||
slugs = [p["slug"] for p in pools]
|
||||
assert len(slugs) == len(set(slugs)), "duplicate slugs in pools list"
|
||||
ranks = [p["rank"] for p in pools]
|
||||
assert ranks == list(range(1, len(pools) + 1)), f"ranks not 1..N: {ranks}"
|
||||
block_total = 0
|
||||
for p in pools:
|
||||
assert isinstance(p["blockCount"], int) and p["blockCount"] >= 0
|
||||
assert isinstance(p["emptyBlocks"], int) and p["emptyBlocks"] >= 0
|
||||
assert 0.0 <= p["share"] <= 1.0, f"share out of range for {p['slug']}: {p['share']}"
|
||||
block_total += p["blockCount"]
|
||||
assert block_total <= b["blockCount"], (
|
||||
f"sum(pool.blockCount)={block_total} exceeds envelope.blockCount={b['blockCount']}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
|
||||
def test_mining_pools_by_period_malformed(brk, bad):
|
||||
"""Unknown time period must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/pools/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
@@ -2,14 +2,62 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
from brk_client import BrkError
|
||||
|
||||
from _lib import assert_same_structure, assert_same_values, show
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block_count", [10, 100, 500])
|
||||
def test_mining_reward_stats(brk, mempool, block_count):
|
||||
"""Reward stats must have the same structure."""
|
||||
path = f"/api/v1/mining/reward-stats/{block_count}"
|
||||
b = brk.get_json(path)
|
||||
COUNTS = [1, 10, 100, 500, 1000]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", COUNTS)
|
||||
def test_mining_reward_stats_structure(brk, mempool, count):
|
||||
"""Reward stats envelope must match across counts."""
|
||||
path = f"/api/v1/mining/reward-stats/{count}"
|
||||
b = brk.get_reward_stats(count)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [100, 1000])
|
||||
def test_mining_reward_stats_values_match(brk, mempool, count):
|
||||
"""brk and mempool must agree exactly on aggregated stats."""
|
||||
path = f"/api/v1/mining/reward-stats/{count}"
|
||||
b = brk.get_reward_stats(count)
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_values(b, m)
|
||||
|
||||
|
||||
def test_mining_reward_stats_invariants(brk):
|
||||
"""Range alignment, reward >= fee, totalTx >= block count (count=1000)."""
|
||||
count = 1000
|
||||
b = brk.get_reward_stats(count)
|
||||
show("GET", f"/api/v1/mining/reward-stats/{count}", b, "-")
|
||||
start = int(b["startBlock"])
|
||||
end = int(b["endBlock"])
|
||||
total_reward = int(b["totalReward"])
|
||||
total_fee = int(b["totalFee"])
|
||||
total_tx = int(b["totalTx"])
|
||||
assert start <= end, f"startBlock {start} > endBlock {end}"
|
||||
assert end - start + 1 == count, (
|
||||
f"range mismatch: {end} - {start} + 1 = {end - start + 1}, expected {count}"
|
||||
)
|
||||
assert total_fee >= 0, f"negative totalFee: {total_fee}"
|
||||
assert total_reward >= total_fee, (
|
||||
f"totalReward {total_reward} < totalFee {total_fee} (subsidy must be non-negative)"
|
||||
)
|
||||
assert total_tx >= count, (
|
||||
f"totalTx {total_tx} < block_count {count} (each block has >=1 coinbase tx)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["abc", "-1"])
|
||||
def test_mining_reward_stats_malformed(brk, bad):
|
||||
"""Non-numeric or negative block_count must produce BrkError(status=400)."""
|
||||
with pytest.raises(BrkError) as exc_info:
|
||||
brk.get_text(f"/api/v1/mining/reward-stats/{bad}")
|
||||
assert exc_info.value.status == 400, (
|
||||
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user