## M0p1 — Embeddable Runtime Skeleton (329 tests)
- TidalDb with builder(), health_check(), close(), and Drop-based cleanup
- TidalDbBuilder fluent API: ephemeral(), with_data_dir(), wal_dir(), cache_dir()
- Config, StorageMode, ConfigError types; Config(ConfigError) variant on LumenError
- Paths: single source of truth for directory layout (wal, items, users, creators, cache)
- TempTidalHome: test isolation helper gated behind #[cfg(test)] / test-utils feature
- 8 integration tests: tests/sandboxed_storage.rs
## M0p2 — Tooling & Diagnostics (349 tests)
- Workspace root Cargo.toml (members: ["tidal", "tidalctl"])
- tidal/build.rs: BUILD_HASH from GIT_HASH with option_env!() fallback to "dev"
- MetricsState: always-compiled Arc-shared atomics (uptime, health_ok)
- MetricsHandle (metrics feature): hand-rolled TcpListener HTTP, zero new deps
- GET /healthz → {"status":"ok","uptime_secs":N}
- GET /metrics → Prometheus text (tidaldb_uptime_seconds, health_ok, info)
- TidalDbBuilder.enable_metrics(addr) starts background metrics thread
- tidalctl binary: status + paths commands, manual std::env::args() parsing
- 7 metrics integration tests, 9 tidalctl CLI tests
## m1p4 Signal Ledger (in-progress)
- SignalLedger: DashMap<(EntityId, SignalTypeId), EntitySignalEntry>, WAL-first writes
- HotSignalState: #[repr(C, align(64))], lock-free CAS decay, out-of-order handling
- BucketedCounter: 60 per-minute + 168 per-hour circular buffers, trigger-based rotation
- CheckpointMeta + serialize/restore: 983-byte fixed records, atomic WriteBatch
- Property tests: running score matches analytical to 1e-6, decay monotonic, non-negative
- Proptest regression: signals/warm.txt
## Documentation and planning
- ROADMAP: m0p1 COMPLETE (329), m0p2 COMPLETE (349), product track milestones
- PRODUCT_ROADMAP: P0-P4 product milestone track (personal briefing beachhead)
- Milestone planning docs: milestone-0 (phases 1-3), milestone-p (phases 1-5)
- docs/research/tidaldb_tooling_and_diagnostics.md
- ARCHITECTURE.md, CLAUDE.md, VISION.md updates
## Site
- Blog: every-platform-builds-the-same-6-systems.mdx (new)
- Blog: why-tidaldb.mdx (updated)
- next.config.ts, layout.tsx, blog/page.tsx updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
4.6 KiB
Rust
169 lines
4.6 KiB
Rust
//! Integration tests for the metrics HTTP server.
|
|
//!
|
|
//! These tests require `--features metrics`.
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
use std::io::{Read, Write};
|
|
use std::net::TcpStream;
|
|
use std::time::Duration;
|
|
|
|
use tidaldb::TidalDb;
|
|
|
|
/// Make an HTTP GET request to the given address and path.
|
|
/// Returns (status_code, body).
|
|
fn http_get(addr: std::net::SocketAddr, path: &str) -> (u16, String) {
|
|
let mut stream = TcpStream::connect(addr).expect("connect");
|
|
write!(
|
|
stream,
|
|
"GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.unwrap();
|
|
stream.flush().unwrap();
|
|
let mut response = String::new();
|
|
stream.read_to_string(&mut response).unwrap();
|
|
// Parse status line: "HTTP/1.1 200 OK"
|
|
let status: u16 = response
|
|
.lines()
|
|
.next()
|
|
.and_then(|l| l.split_whitespace().nth(1))
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0);
|
|
let body = response
|
|
.splitn(2, "\r\n\r\n")
|
|
.nth(1)
|
|
.unwrap_or("")
|
|
.to_string();
|
|
(status, body)
|
|
}
|
|
|
|
fn open_with_metrics() -> (std::net::SocketAddr, TidalDb) {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.enable_metrics("127.0.0.1:0")
|
|
.open()
|
|
.expect("open should succeed");
|
|
let addr = db
|
|
.metrics_addr()
|
|
.expect("metrics_addr should be Some when metrics enabled");
|
|
(addr, db)
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_server_starts_on_port_zero() {
|
|
let (addr, db) = open_with_metrics();
|
|
assert_ne!(addr.port(), 0, "OS should have assigned a real port");
|
|
db.close().expect("close should succeed");
|
|
}
|
|
|
|
#[test]
|
|
fn healthz_returns_200_json() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (status, body) = http_get(addr, "/healthz");
|
|
assert_eq!(status, 200);
|
|
assert!(body.contains("\"status\":\"ok\""), "body: {body}");
|
|
assert!(
|
|
body.contains("\"uptime_seconds\":"),
|
|
"body should have uptime: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("\"version\":"),
|
|
"body should have version: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("\"build_hash\":"),
|
|
"body should have build_hash: {body}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_returns_200_prometheus() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (status, body) = http_get(addr, "/metrics");
|
|
assert_eq!(status, 200);
|
|
assert!(
|
|
body.contains("tidaldb_uptime_seconds"),
|
|
"missing uptime metric in body: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("tidaldb_health_ok"),
|
|
"missing health_ok metric in body: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("tidaldb_info"),
|
|
"missing info metric in body: {body}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn uptime_increases_over_time() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (_, body1) = http_get(addr, "/metrics");
|
|
std::thread::sleep(Duration::from_millis(150));
|
|
let (_, body2) = http_get(addr, "/metrics");
|
|
|
|
let uptime1 = extract_uptime(&body1);
|
|
let uptime2 = extract_uptime(&body2);
|
|
|
|
assert!(
|
|
uptime2 > uptime1,
|
|
"uptime should increase: {uptime1} -> {uptime2}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn health_ok_value_is_one() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (_, body) = http_get(addr, "/metrics");
|
|
let health_line = body
|
|
.lines()
|
|
.find(|l| l.starts_with("tidaldb_health_ok{"))
|
|
.expect("should have health_ok metric");
|
|
assert!(
|
|
health_line.ends_with(" 1"),
|
|
"health should be 1: {health_line}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn not_found_returns_404() {
|
|
let (addr, _db) = open_with_metrics();
|
|
let (status, _) = http_get(addr, "/nonexistent");
|
|
assert_eq!(status, 404);
|
|
}
|
|
|
|
#[test]
|
|
fn close_stops_server() {
|
|
let (addr, db) = open_with_metrics();
|
|
// Server should respond before close
|
|
let (status, _) = http_get(addr, "/healthz");
|
|
assert_eq!(status, 200);
|
|
|
|
db.close().expect("close should succeed");
|
|
|
|
// Give the thread a moment to shut down
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
// After close, connection should be refused
|
|
let result = TcpStream::connect_timeout(&addr.into(), Duration::from_millis(200));
|
|
assert!(result.is_err(), "connection should be refused after close");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
fn extract_uptime(prometheus_body: &str) -> f64 {
|
|
for line in prometheus_body.lines() {
|
|
if line.starts_with("tidaldb_uptime_seconds{") {
|
|
let val = line.split_whitespace().last().unwrap_or("0");
|
|
return val.parse().unwrap_or(0.0);
|
|
}
|
|
}
|
|
0.0
|
|
}
|