Skip to content
Open
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
87 changes: 87 additions & 0 deletions diagnostic/build-23f043a7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"generated_at": "2026-06-22T13:26:00.673811+00:00",
"commit": "23f043a7",
"diagnostic_logd": "diagnostic/build-23f043a7.logd",
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "888a19d8830218c43784",
"decrypt_command": "encryptly unpack diagnostic/build-23f043a7.logd <outdir> --password 888a19d8830218c43784",
"total_modules": 10,
"passed": 2,
"failed": 8,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0.074,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'cargo'"
},
{
"name": "frontend",
"status": "PASS",
"elapsed_seconds": 45.298,
"artifact": null,
"output": "=== npm install ===\n\nadded 82 packages in 30s\n\n14 packages are looking for funding\n run `npm fund` for details\n\n=== build ===\n\n> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.63 kB \u2502 gzip: 0.35 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,045.57 kB\n\u2713 built in 3.92s\n"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0.112,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'go'"
},
{
"name": "frailbox",
"status": "PASS",
"elapsed_seconds": 1.947,
"artifact": null,
"output": "=== build ===\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nsrc/logger.c: In function \u2018log_message\u2019:\nsrc/logger.c:315:5: warning: \u2018__builtin___strncpy_chk\u2019 output may be truncated copying 4095 bytes from a string of length 4095 [-Wstringop-truncation]\n 315 | strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1);\n | ^\n"
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0.116,
"artifact": null,
"output": "=== build ===\nCMake Error: The current CMakeCache.txt directory /mnt/e/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build/CMakeCache.txt is different than the directory e:/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt\nError: could not create CMAKE_GENERATOR \"Visual Studio 18 2026\"\n"
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0.08,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'javac'"
},
{
"name": "v2-market-stream",
"status": "FAIL",
"elapsed_seconds": 0.081,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ruby'"
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0.069,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0.077,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ghc'"
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0.067,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-23f043a7.logd."
}
Binary file added diagnostic/build-23f043a7.logd
Binary file not shown.
104 changes: 104 additions & 0 deletions tests/test_data_generator_seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Tests for data_generator deterministic seed support (bounty #4).

Verifies that seeding produces reproducible output across runs, that
different seeds produce different data, and that the helper functions
(phone/email/datetime/gaussian) honour an injected rng.
"""

import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "tools"))

import random
from data_generator import (
DataGenerator,
random_phone,
random_email,
random_datetime,
gaussian_random,
)


class TestDeterministicSeed(unittest.TestCase):
def test_same_seed_identical_users(self):
g1 = DataGenerator(seed=42)
g2 = DataGenerator(seed=42)
u1 = g1.generate_users(50)
u2 = g2.generate_users(50)
self.assertEqual(u1, u2)

def test_different_seed_different_users(self):
g1 = DataGenerator(seed=42)
g2 = DataGenerator(seed=999)
u1 = g1.generate_users(50)
u2 = g2.generate_users(50)
self.assertNotEqual(u1, u2)

def test_same_seed_identical_orders(self):
g1 = DataGenerator(seed=7)
g2 = DataGenerator(seed=7)
g1.generate_users(20)
g2.generate_users(20)
o1 = g1.generate_orders(50)
o2 = g2.generate_orders(50)
self.assertEqual(o1, o2)

def test_same_seed_identical_trades(self):
g1 = DataGenerator(seed=123)
g2 = DataGenerator(seed=123)
g1.generate_users(20)
g2.generate_users(20)
g1.generate_orders(30)
g2.generate_orders(30)
t1 = g1.generate_trades(40)
t2 = g2.generate_trades(40)
self.assertEqual(t1, t2)

def test_reproducible_across_runs(self):
results = []
for _ in range(3):
g = DataGenerator(seed=42)
g.generate_users(10)
results.append(g.users[0]["email"])
self.assertEqual(len(set(results)), 1)

def test_default_seed_is_set(self):
g1 = DataGenerator()
g2 = DataGenerator()
self.assertEqual(g1.generate_users(5), g2.generate_users(5))


class TestHelpersHonourRng(unittest.TestCase):
def test_random_phone_deterministic(self):
rng1 = random.Random(1)
rng2 = random.Random(1)
self.assertEqual(random_phone(rng1), random_phone(rng2))

def test_random_email_deterministic(self):
rng1 = random.Random(2)
rng2 = random.Random(2)
self.assertEqual(random_email("john", "doe", rng1), random_email("john", "doe", rng2))

def test_random_datetime_deterministic(self):
rng1 = random.Random(3)
rng2 = random.Random(3)
self.assertEqual(random_datetime(rng=rng1), random_datetime(rng=rng2))

def test_gaussian_random_deterministic(self):
rng1 = random.Random(4)
rng2 = random.Random(4)
self.assertEqual(gaussian_random(0, 1, rng1), gaussian_random(0, 1, rng2))

def test_helpers_default_to_global_when_no_rng(self):
# without rng, helpers still work (fall back to global random)
phone = random_phone()
self.assertTrue(phone.startswith("+1-"))
email = random_email("jane", "roe")
self.assertIn("@", email)


if __name__ == "__main__":
unittest.main()
44 changes: 27 additions & 17 deletions tools/data_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,37 +76,47 @@
"fictitious.co", "imaginary.app", "pretend.tech", "dummy.biz",
"simulated.com", "testmail.com", "inbox.test"]

def gaussian_random(mean: float, stddev: float) -> float:
return random.gauss(mean, stddev)
def gaussian_random(mean: float, stddev: float, rng: Optional[random.Random] = None) -> float:
return (rng or random).gauss(mean, stddev)

def clamp(value: float, min_val: float, max_val: float) -> float:
return max(min_val, min(max_val, value))

def round_to_tick(value: float, tick_size: float) -> float:
return round(value / tick_size) * tick_size

def random_phone() -> str:
return f"+1-{random.randint(200, 999)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}"
def random_phone(rng: Optional[random.Random] = None) -> str:
r = rng or random
return f"+1-{r.randint(200, 999)}-{r.randint(100, 999)}-{r.randint(1000, 9999)}"

def random_email(first: str, last: str) -> str:
domain = random.choice(DOMAINS)
pattern = random.choice([
def random_email(first: str, last: str, rng: Optional[random.Random] = None) -> str:
r = rng or random
domain = r.choice(DOMAINS)
pattern = r.choice([
f"{first.lower()}.{last.lower()}",
f"{first.lower()}{last.lower()}",
f"{first[0].lower()}{last.lower()}",
f"{last.lower()}.{first.lower()}",
f"{first.lower()}{random.randint(1, 999)}",
f"{first.lower()}{r.randint(1, 999)}",
])
return f"{pattern}@{domain}"

def random_datetime(start_year: int = 2023, end_year: int = 2024) -> datetime:
def random_datetime(start_year: int = 2023, end_year: int = 2024, rng: Optional[random.Random] = None) -> datetime:
r = rng or random
start = datetime(start_year, 1, 1, tzinfo=timezone.utc)
end = datetime(end_year, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
delta = end - start
return start + timedelta(seconds=random.randint(0, int(delta.total_seconds())))
return start + timedelta(seconds=r.randint(0, int(delta.total_seconds())))


class DataGenerator:
"""Generates deterministic test data when seeded.

All randomness flows through a single ``random.Random`` instance seeded
in ``__init__``, so the same seed reproduces identical data across
runs (users, orders, trades, ticks, candles, emails, phones, dates).
"""

def __init__(self, seed: int = 42):
self.random = random.Random(seed)
self.instruments = INSTRUMENTS
Expand All @@ -126,16 +136,16 @@ def generate_users(self, count: int = 50) -> List[Dict[str, Any]]:
last = self.random.choice(LAST_NAMES)
user = {
"id": f"user_{self.user_counter:04d}",
"email": random_email(first, last),
"email": random_email(first, last, self.random),
"name": f"{first} {last}",
"role": self.random.choice(["trader", "trader", "trader", "admin",
"analyst", "viewer"]),
"status": self.random.choice(["active", "active", "active", "active", "inactive"]),
"mfa_enabled": self.random.random() < 0.3,
"email_verified": self.random.random() < 0.95,
"created_at": random_datetime().isoformat(),
"last_login": random_datetime(2024, 2024).isoformat(),
"phone": random_phone(),
"created_at": random_datetime(rng=self.random).isoformat(),
"last_login": random_datetime(2024, 2024, self.random).isoformat(),
"phone": random_phone(self.random),
"preferences": {
"theme": self.random.choice(["dark", "light"]),
"language": "en",
Expand Down Expand Up @@ -180,8 +190,8 @@ def generate_orders(self, count: int = 200) -> List[Dict[str, Any]]:
"status": self.random.choice(ORDER_STATUSES),
"filled_quantity": 0,
"avg_fill_price": None,
"created_at": random_datetime().isoformat(),
"updated_at": random_datetime(2024, 2024).isoformat(),
"created_at": random_datetime(rng=self.random).isoformat(),
"updated_at": random_datetime(2024, 2024, self.random).isoformat(),
}
self.orders.append(order)

Expand Down Expand Up @@ -210,7 +220,7 @@ def generate_trades(self, count: int = 500) -> List[Dict[str, Any]]:
"quantity": quantity,
"total": round(price * quantity, 2),
"side": side,
"timestamp": random_datetime(2024, 2024).isoformat(),
"timestamp": random_datetime(2024, 2024, self.random).isoformat(),
"buyer": self.random.choice(self.users)["id"],
"seller": self.random.choice(self.users)["id"],
"buyer_fee": round(price * quantity * 0.001, 2),
Expand Down