mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 15:19:58 -07:00
914 lines
33 KiB
Rust
914 lines
33 KiB
Rust
//! Tests that verify pattern analysis using the real catalog.
|
|
|
|
use std::collections::HashSet;
|
|
use std::fmt::Write;
|
|
|
|
use brk_bindgen::ClientMetadata;
|
|
use brk_types::TreeNode;
|
|
|
|
/// Load the catalog from the JSON file.
|
|
fn load_catalog() -> TreeNode {
|
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/catalog.json");
|
|
let catalog_json = std::fs::read_to_string(path).expect("Failed to read catalog.json");
|
|
serde_json::from_str(&catalog_json).expect("Failed to parse catalog.json")
|
|
}
|
|
|
|
/// Load OpenAPI spec from openapi.json.
|
|
fn load_openapi_json() -> String {
|
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json");
|
|
std::fs::read_to_string(path).expect("Failed to read openapi.json")
|
|
}
|
|
|
|
/// Load metadata from the catalog.
|
|
#[allow(unused)]
|
|
fn load_metadata() -> ClientMetadata {
|
|
ClientMetadata::from_catalog(load_catalog())
|
|
}
|
|
|
|
/// Collect all leaf metric names from a tree.
|
|
fn collect_leaf_names(node: &TreeNode, names: &mut HashSet<String>) {
|
|
match node {
|
|
TreeNode::Leaf(leaf) => {
|
|
names.insert(leaf.name().to_string());
|
|
}
|
|
TreeNode::Branch(children) => {
|
|
for child in children.values() {
|
|
collect_leaf_names(child, names);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_catalog_loads() {
|
|
let catalog = load_catalog();
|
|
|
|
// Should be a branch with top-level categories
|
|
let TreeNode::Branch(categories) = &catalog else {
|
|
panic!("Expected catalog to be a branch");
|
|
};
|
|
|
|
// Check some expected top-level categories exist
|
|
assert!(
|
|
categories.contains_key("addresses"),
|
|
"Missing addresses category"
|
|
);
|
|
assert!(categories.contains_key("blocks"), "Missing blocks category");
|
|
assert!(categories.contains_key("market"), "Missing market category");
|
|
assert!(categories.contains_key("supply"), "Missing supply category");
|
|
|
|
println!("Catalog has {} top-level categories", categories.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_leaves_have_names() {
|
|
let catalog = load_catalog();
|
|
let mut names = HashSet::new();
|
|
collect_leaf_names(&catalog, &mut names);
|
|
|
|
println!("Catalog has {} unique metric names", names.len());
|
|
assert!(!names.is_empty(), "Should have at least some metrics");
|
|
|
|
// All names should be non-empty
|
|
for name in &names {
|
|
assert!(!name.is_empty(), "Found empty metric name");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pattern_detection() {
|
|
let catalog = load_catalog();
|
|
|
|
let (patterns, concrete_to_pattern, concrete_to_type_param, _node_bases) =
|
|
brk_bindgen::detect_structural_patterns(&catalog);
|
|
|
|
println!("Detected {} structural patterns", patterns.len());
|
|
println!(
|
|
"Concrete to pattern mappings: {}",
|
|
concrete_to_pattern.len()
|
|
);
|
|
println!("Type parameter mappings: {}", concrete_to_type_param.len());
|
|
|
|
// Print pattern details
|
|
for pattern in &patterns {
|
|
let mode_str = match &pattern.mode {
|
|
Some(brk_bindgen::PatternMode::Suffix { relatives }) => {
|
|
format!("Suffix({})", relatives.len())
|
|
}
|
|
Some(brk_bindgen::PatternMode::Prefix { prefixes }) => {
|
|
format!("Prefix({})", prefixes.len())
|
|
}
|
|
None => "None".to_string(),
|
|
};
|
|
println!(
|
|
" {} (fields: {}, generic: {}, mode: {})",
|
|
pattern.name,
|
|
pattern.fields.len(),
|
|
pattern.is_generic,
|
|
mode_str
|
|
);
|
|
}
|
|
|
|
// Should have detected some patterns
|
|
assert!(!patterns.is_empty(), "Should detect at least some patterns");
|
|
|
|
// Check that parameterizable patterns have valid modes
|
|
for pattern in &patterns {
|
|
if pattern.is_parameterizable() {
|
|
let mode = pattern.mode.as_ref().unwrap();
|
|
match mode {
|
|
brk_bindgen::PatternMode::Suffix { relatives } => {
|
|
assert_eq!(
|
|
relatives.len(),
|
|
pattern.fields.len(),
|
|
"Pattern {} should have relative for each field",
|
|
pattern.name
|
|
);
|
|
}
|
|
brk_bindgen::PatternMode::Prefix { prefixes } => {
|
|
assert_eq!(
|
|
prefixes.len(),
|
|
pattern.fields.len(),
|
|
"Pattern {} should have prefix for each field",
|
|
pattern.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_cost_basis_pattern() {
|
|
let catalog = load_catalog();
|
|
|
|
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
|
|
|
// Find CostBasisPattern2 and inspect it
|
|
let cost_basis = patterns
|
|
.iter()
|
|
.find(|p| p.name == "CostBasisPattern2")
|
|
.expect("CostBasisPattern2 should exist");
|
|
|
|
println!("CostBasisPattern2:");
|
|
println!(
|
|
" Fields: {:?}",
|
|
cost_basis
|
|
.fields
|
|
.iter()
|
|
.map(|f| &f.name)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
println!(" Mode: {:?}", cost_basis.mode);
|
|
println!(" Is generic: {}", cost_basis.is_generic);
|
|
|
|
// With suffix naming convention (cost_basis_max, cost_basis_min, cost_basis):
|
|
//
|
|
// At root level: common prefix is "cost_basis_" -> suffix mode
|
|
// max -> "max"
|
|
// min -> "min"
|
|
// percentiles -> "" (identity)
|
|
//
|
|
// At lth_ level: common prefix is "lth_cost_basis_" -> suffix mode
|
|
// max -> "max"
|
|
// min -> "min"
|
|
// percentiles -> "" (identity)
|
|
//
|
|
// Both use suffix mode with same relatives, so pattern IS parameterizable!
|
|
assert!(
|
|
cost_basis.is_parameterizable(),
|
|
"CostBasisPattern2 should be parameterizable with consistent suffix mode"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_realized_pattern3_fields() {
|
|
let catalog = load_catalog();
|
|
let metadata = ClientMetadata::from_catalog(catalog);
|
|
|
|
let pattern = metadata
|
|
.find_pattern("RealizedPattern3")
|
|
.expect("RealizedPattern3 should exist");
|
|
|
|
println!("RealizedPattern3 fields:");
|
|
for field in &pattern.fields {
|
|
let is_branch = field.is_branch();
|
|
let is_pattern = metadata.find_pattern(&field.rust_type).is_some();
|
|
let is_param = metadata.is_parameterizable(&field.rust_type);
|
|
println!(
|
|
" {} -> {} (branch={}, pattern={}, param={})",
|
|
field.name, field.rust_type, is_branch, is_pattern, is_param
|
|
);
|
|
}
|
|
|
|
// Check if RealizedPattern3 is considered parameterizable
|
|
println!(
|
|
"\nRealizedPattern3 is_parameterizable (metadata): {}",
|
|
metadata.is_parameterizable("RealizedPattern3")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parameterizable_patterns_have_mode() {
|
|
let catalog = load_catalog();
|
|
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
|
|
|
// All patterns that appear 2+ times should either:
|
|
// 1. Be parameterizable (have a mode)
|
|
// 2. Or have inconsistent instances (mode = None)
|
|
//
|
|
// Patterns with mode = None should be inlined, not generate factories
|
|
|
|
let parameterizable: Vec<_> = patterns.iter().filter(|p| p.is_parameterizable()).collect();
|
|
let non_parameterizable: Vec<_> = patterns
|
|
.iter()
|
|
.filter(|p| !p.is_parameterizable())
|
|
.collect();
|
|
|
|
println!("\nParameterizable patterns ({}):", parameterizable.len());
|
|
for p in ¶meterizable {
|
|
let mode = p.mode.as_ref().unwrap();
|
|
let mode_type = match mode {
|
|
brk_bindgen::PatternMode::Suffix { .. } => "Suffix",
|
|
brk_bindgen::PatternMode::Prefix { .. } => "Prefix",
|
|
};
|
|
println!(" {} ({} fields, {})", p.name, p.fields.len(), mode_type);
|
|
}
|
|
|
|
println!(
|
|
"\nNon-parameterizable patterns ({}):",
|
|
non_parameterizable.len()
|
|
);
|
|
for p in &non_parameterizable {
|
|
println!(" {} ({} fields)", p.name, p.fields.len());
|
|
}
|
|
|
|
// Verify all parameterizable patterns have valid modes with all fields
|
|
for pattern in ¶meterizable {
|
|
let mode = pattern.mode.as_ref().unwrap();
|
|
let field_names: HashSet<_> = pattern.fields.iter().map(|f| f.name.clone()).collect();
|
|
|
|
match mode {
|
|
brk_bindgen::PatternMode::Suffix { relatives } => {
|
|
let mode_fields: HashSet<_> = relatives.keys().cloned().collect();
|
|
assert_eq!(
|
|
field_names, mode_fields,
|
|
"Pattern {} suffix mode should have all fields",
|
|
pattern.name
|
|
);
|
|
}
|
|
brk_bindgen::PatternMode::Prefix { prefixes } => {
|
|
let mode_fields: HashSet<_> = prefixes.keys().cloned().collect();
|
|
assert_eq!(
|
|
field_names, mode_fields,
|
|
"Pattern {} prefix mode should have all fields",
|
|
pattern.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fee_rate_pattern_relatives() {
|
|
let catalog = load_catalog();
|
|
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
|
|
|
let fee_rate_pattern = patterns
|
|
.iter()
|
|
.find(|p| p.name == "FeeRatePattern")
|
|
.expect("FeeRatePattern should exist");
|
|
|
|
println!("FeeRatePattern mode:");
|
|
if let Some(mode) = &fee_rate_pattern.mode {
|
|
match mode {
|
|
brk_bindgen::PatternMode::Suffix { relatives } => {
|
|
println!(" Suffix mode:");
|
|
for (field, relative) in relatives {
|
|
println!(" {} -> '{}'", field, relative);
|
|
}
|
|
}
|
|
brk_bindgen::PatternMode::Prefix { prefixes } => {
|
|
println!(" Prefix mode:");
|
|
for (field, prefix) in prefixes {
|
|
println!(" {} -> '{}'", field, prefix);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
println!(" No mode (not parameterizable)");
|
|
}
|
|
|
|
// Check that relatives are correct - should be "average", "max", etc.
|
|
// NOT "tx_weight_average", "tx_weight_max", etc.
|
|
if let Some(brk_bindgen::PatternMode::Suffix { relatives }) = &fee_rate_pattern.mode {
|
|
assert_eq!(
|
|
relatives.get("average"),
|
|
Some(&"average".to_string()),
|
|
"average relative should be 'average', not 'tx_weight_average'"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_index_patterns() {
|
|
let catalog = load_catalog();
|
|
|
|
let index_patterns = brk_bindgen::detect_index_patterns(&catalog);
|
|
|
|
// println!("Used indexes: {:?}", used_indexes);
|
|
println!("Index set patterns: {}", index_patterns.len());
|
|
|
|
for pattern in &index_patterns {
|
|
println!(" {} -> {:?}", pattern.name, pattern.indexes);
|
|
}
|
|
|
|
// Should have detected some index patterns
|
|
assert!(!index_patterns.is_empty(), "Should detect index patterns");
|
|
}
|
|
|
|
#[test]
|
|
fn test_generated_rust_output() {
|
|
let catalog = load_catalog();
|
|
let metadata = ClientMetadata::from_catalog(catalog.clone());
|
|
|
|
// Collect all metric names from the catalog
|
|
let mut all_metrics = HashSet::new();
|
|
collect_leaf_names(&catalog, &mut all_metrics);
|
|
|
|
// Generate Rust client output
|
|
let mut rust_output = String::new();
|
|
brk_bindgen::rust::client::generate_imports(&mut rust_output);
|
|
brk_bindgen::rust::client::generate_base_client(&mut rust_output);
|
|
brk_bindgen::rust::client::generate_metric_pattern_trait(&mut rust_output);
|
|
brk_bindgen::rust::client::generate_endpoint(&mut rust_output);
|
|
brk_bindgen::rust::client::generate_index_accessors(
|
|
&mut rust_output,
|
|
&metadata.index_set_patterns,
|
|
);
|
|
brk_bindgen::rust::client::generate_pattern_structs(
|
|
&mut rust_output,
|
|
&metadata.structural_patterns,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::rust::tree::generate_tree(&mut rust_output, &metadata.catalog, &metadata);
|
|
brk_bindgen::rust::api::generate_main_client(&mut rust_output, &[]);
|
|
|
|
// Count metrics that appear as direct string literals
|
|
let mut direct_metrics = 0;
|
|
for metric in &all_metrics {
|
|
if rust_output.contains(&format!("\"{}\"", metric)) {
|
|
direct_metrics += 1;
|
|
}
|
|
}
|
|
|
|
println!("\nGenerated Rust output stats:");
|
|
println!(" Total metrics in catalog: {}", all_metrics.len());
|
|
println!(" Direct string literals: {}", direct_metrics);
|
|
println!(
|
|
" Via pattern factories: {}",
|
|
all_metrics.len() - direct_metrics
|
|
);
|
|
println!(" Output size: {} bytes", rust_output.len());
|
|
|
|
// Write output to test directory (not actual client)
|
|
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
|
|
std::fs::create_dir_all(output_dir).ok();
|
|
let output_path = format!("{}/rust_client.rs", output_dir);
|
|
std::fs::write(&output_path, &rust_output).expect("Failed to write client output");
|
|
println!(" Wrote output to: {}", output_path);
|
|
|
|
// Verify the output contains the key components
|
|
assert!(rust_output.contains("fn _m("), "Should define _m helper");
|
|
assert!(
|
|
rust_output.contains("pub struct MetricsTree"),
|
|
"Should have MetricsTree"
|
|
);
|
|
assert!(
|
|
rust_output.contains("impl MetricsTree"),
|
|
"Should have MetricsTree impl"
|
|
);
|
|
|
|
// Count parameterizable patterns (these use _m for dynamic metric names)
|
|
// Use metadata.is_parameterizable() for full recursive check
|
|
let parameterizable_count = metadata
|
|
.structural_patterns
|
|
.iter()
|
|
.filter(|p| metadata.is_parameterizable(&p.name))
|
|
.count();
|
|
println!(" Parameterizable patterns: {}", parameterizable_count);
|
|
|
|
// Verify all pattern structs are generated (parameterizable and non)
|
|
for pattern in &metadata.structural_patterns {
|
|
assert!(
|
|
rust_output.contains(&format!("pub struct {}", pattern.name)),
|
|
"Missing pattern struct: {}",
|
|
pattern.name
|
|
);
|
|
}
|
|
|
|
println!("\nGenerated Rust client is complete!");
|
|
}
|
|
|
|
#[test]
|
|
fn test_generated_javascript_output() {
|
|
let catalog = load_catalog();
|
|
let metadata = ClientMetadata::from_catalog(catalog.clone());
|
|
|
|
// Collect all metric names from the catalog
|
|
let mut all_metrics = HashSet::new();
|
|
collect_leaf_names(&catalog, &mut all_metrics);
|
|
|
|
// Load schemas from OpenAPI spec only (catalog schemas require runtime data)
|
|
let openapi_json = load_openapi_json();
|
|
let schemas = brk_bindgen::extract_schemas(&openapi_json);
|
|
|
|
// Generate JavaScript client output
|
|
let mut js_output = String::new();
|
|
writeln!(js_output, "// Auto-generated BRK JavaScript client").unwrap();
|
|
writeln!(js_output, "// Do not edit manually\n").unwrap();
|
|
brk_bindgen::javascript::types::generate_type_definitions(&mut js_output, &schemas);
|
|
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
|
|
brk_bindgen::javascript::client::generate_index_accessors(
|
|
&mut js_output,
|
|
&metadata.index_set_patterns,
|
|
);
|
|
brk_bindgen::javascript::client::generate_structural_patterns(
|
|
&mut js_output,
|
|
&metadata.structural_patterns,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::javascript::tree::generate_tree_typedefs(
|
|
&mut js_output,
|
|
&metadata.catalog,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::javascript::tree::generate_main_client(
|
|
&mut js_output,
|
|
&metadata.catalog,
|
|
&metadata,
|
|
&[],
|
|
);
|
|
|
|
// Count metrics that appear as direct string literals
|
|
let mut direct_metrics = 0;
|
|
for metric in &all_metrics {
|
|
if js_output.contains(&format!("'{}'", metric))
|
|
|| js_output.contains(&format!("\"{}\"", metric))
|
|
{
|
|
direct_metrics += 1;
|
|
}
|
|
}
|
|
|
|
println!("\nGenerated JavaScript output stats:");
|
|
println!(" Total metrics in catalog: {}", all_metrics.len());
|
|
println!(" Direct string literals: {}", direct_metrics);
|
|
println!(
|
|
" Via pattern factories: {}",
|
|
all_metrics.len() - direct_metrics
|
|
);
|
|
println!(" Output size: {} bytes", js_output.len());
|
|
println!(" Output lines: {}", js_output.lines().count());
|
|
|
|
// Write output to test directory (not actual client)
|
|
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
|
|
std::fs::create_dir_all(output_dir).ok();
|
|
let output_path = format!("{}/js_client.js", output_dir);
|
|
std::fs::write(&output_path, &js_output).expect("Failed to write JS client output");
|
|
println!(" Wrote output to: {}", output_path);
|
|
|
|
// Verify the output contains key components
|
|
assert!(js_output.contains("const _m ="), "Should define _m helper");
|
|
assert!(js_output.contains("const _p ="), "Should define _p helper");
|
|
assert!(
|
|
js_output.contains("@typedef {Object} MetricsTree"),
|
|
"Should have MetricsTree typedef"
|
|
);
|
|
assert!(
|
|
js_output.contains("class BrkClient"),
|
|
"Should have BrkClient class"
|
|
);
|
|
|
|
// Verify all pattern factories are generated
|
|
for pattern in &metadata.structural_patterns {
|
|
assert!(
|
|
js_output.contains(&format!("function create{}(", pattern.name)),
|
|
"Missing pattern factory: {}",
|
|
pattern.name
|
|
);
|
|
}
|
|
|
|
println!("\nGenerated JavaScript client is complete!");
|
|
}
|
|
|
|
#[test]
|
|
fn test_generated_python_output() {
|
|
let catalog = load_catalog();
|
|
let metadata = ClientMetadata::from_catalog(catalog.clone());
|
|
|
|
// Collect all metric names from the catalog
|
|
let mut all_metrics = HashSet::new();
|
|
collect_leaf_names(&catalog, &mut all_metrics);
|
|
|
|
// Load schemas from OpenAPI spec only (catalog schemas require runtime data)
|
|
let openapi_json = load_openapi_json();
|
|
let schemas = brk_bindgen::extract_schemas(&openapi_json);
|
|
|
|
// Generate Python client output
|
|
let mut py_output = String::new();
|
|
writeln!(py_output, "# Auto-generated BRK Python client").unwrap();
|
|
writeln!(py_output, "# Do not edit manually\n").unwrap();
|
|
writeln!(py_output, "from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload").unwrap();
|
|
writeln!(
|
|
py_output,
|
|
"from http.client import HTTPSConnection, HTTPConnection"
|
|
)
|
|
.unwrap();
|
|
writeln!(py_output, "from urllib.parse import urlparse").unwrap();
|
|
writeln!(py_output, "import json\n").unwrap();
|
|
writeln!(py_output, "T = TypeVar('T')\n").unwrap();
|
|
|
|
brk_bindgen::python::types::generate_type_definitions(&mut py_output, &schemas);
|
|
brk_bindgen::python::client::generate_base_client(&mut py_output);
|
|
brk_bindgen::python::client::generate_endpoint_class(&mut py_output);
|
|
brk_bindgen::python::client::generate_index_accessors(
|
|
&mut py_output,
|
|
&metadata.index_set_patterns,
|
|
);
|
|
brk_bindgen::python::client::generate_structural_patterns(
|
|
&mut py_output,
|
|
&metadata.structural_patterns,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::python::tree::generate_tree_classes(&mut py_output, &metadata.catalog, &metadata);
|
|
brk_bindgen::python::api::generate_main_client(&mut py_output, &[]);
|
|
|
|
// Count metrics that appear as direct string literals
|
|
let mut direct_metrics = 0;
|
|
for metric in &all_metrics {
|
|
if py_output.contains(&format!("'{}'", metric))
|
|
|| py_output.contains(&format!("\"{}\"", metric))
|
|
{
|
|
direct_metrics += 1;
|
|
}
|
|
}
|
|
|
|
println!("\nGenerated Python output stats:");
|
|
println!(" Total metrics in catalog: {}", all_metrics.len());
|
|
println!(" Direct string literals: {}", direct_metrics);
|
|
println!(
|
|
" Via pattern factories: {}",
|
|
all_metrics.len() - direct_metrics
|
|
);
|
|
println!(" Output size: {} bytes", py_output.len());
|
|
println!(" Output lines: {}", py_output.lines().count());
|
|
|
|
// Write output to test directory (not actual client)
|
|
let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output");
|
|
std::fs::create_dir_all(output_dir).ok();
|
|
let output_path = format!("{}/python_client.py", output_dir);
|
|
std::fs::write(&output_path, &py_output).expect("Failed to write Python client output");
|
|
println!(" Wrote output to: {}", output_path);
|
|
|
|
// Verify the output contains key components
|
|
assert!(py_output.contains("def _m("), "Should define _m helper");
|
|
assert!(py_output.contains("def _p("), "Should define _p helper");
|
|
assert!(
|
|
py_output.contains("class MetricsTree:"),
|
|
"Should have MetricsTree class"
|
|
);
|
|
assert!(
|
|
py_output.contains("class BrkClient"),
|
|
"Should have BrkClient class"
|
|
);
|
|
|
|
// Verify all pattern classes have constructors
|
|
for pattern in &metadata.structural_patterns {
|
|
assert!(
|
|
py_output.contains(&format!("class {}:", pattern.name))
|
|
|| py_output.contains(&format!("class {}(", pattern.name)),
|
|
"Missing pattern class: {}",
|
|
pattern.name
|
|
);
|
|
}
|
|
|
|
println!("\nGenerated Python client is complete!");
|
|
}
|
|
|
|
#[test]
|
|
fn test_cost_basis_relatives() {
|
|
let catalog = load_catalog();
|
|
|
|
// Find cost_basis branches that have 3 direct children (max, min, percentiles)
|
|
fn find_cost_basis_with_percentiles(
|
|
node: &TreeNode,
|
|
path: &str,
|
|
) -> Vec<(String, Vec<(String, String)>)> {
|
|
let mut results = Vec::new();
|
|
if let TreeNode::Branch(children) = node {
|
|
for (name, child) in children {
|
|
let child_path = if path.is_empty() {
|
|
name.clone()
|
|
} else {
|
|
format!("{}.{}", path, name)
|
|
};
|
|
|
|
if name == "cost_basis"
|
|
&& let TreeNode::Branch(cb_children) = child
|
|
&& cb_children.contains_key("percentiles")
|
|
{
|
|
// Found a cost_basis with percentiles
|
|
let mut metrics = Vec::new();
|
|
for (field_name, field_node) in cb_children {
|
|
match field_node {
|
|
TreeNode::Leaf(leaf) => {
|
|
metrics.push((field_name.clone(), leaf.name().to_string()));
|
|
}
|
|
TreeNode::Branch(pct_children) => {
|
|
// Get first percentile as example
|
|
if let Some((_, TreeNode::Leaf(first))) = pct_children.iter().next()
|
|
{
|
|
metrics.push((
|
|
format!("{}.first", field_name),
|
|
first.name().to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
results.push((child_path.clone(), metrics));
|
|
}
|
|
results.extend(find_cost_basis_with_percentiles(child, &child_path));
|
|
}
|
|
}
|
|
results
|
|
}
|
|
|
|
let instances = find_cost_basis_with_percentiles(&catalog, "");
|
|
|
|
println!("\nCostBasisPattern2 instances (with percentiles):");
|
|
for (path, metrics) in instances.iter().take(10) {
|
|
println!(" {}:", path);
|
|
for (field, metric) in metrics {
|
|
println!(" {} -> {}", field, metric);
|
|
}
|
|
}
|
|
|
|
// Now compute what relatives the pattern detection would see
|
|
// The key is: percentiles returns its BASE (common prefix of pct05, pct10, etc.)
|
|
// not the individual percentile metrics
|
|
use brk_bindgen::find_common_prefix;
|
|
|
|
println!("\nComputing relatives (simulating branch base returns):");
|
|
for (path, metrics) in instances.iter().take(5) {
|
|
println!(" Instance: {}", path);
|
|
|
|
// For leaves (max, min), the base is the metric name
|
|
// For branches (percentiles), the base is the common prefix of its children
|
|
let mut child_bases: std::collections::HashMap<String, String> =
|
|
std::collections::HashMap::new();
|
|
for (field, metric) in metrics {
|
|
if field.starts_with("percentiles.") {
|
|
// This is a percentile metric - compute what the percentiles branch would return
|
|
// The base is the metric name with the pct suffix stripped
|
|
let base = metric
|
|
.strip_suffix("_pct05")
|
|
.or_else(|| metric.strip_suffix("_pct10"))
|
|
.unwrap_or(metric)
|
|
.to_string();
|
|
child_bases.insert("percentiles".to_string(), base);
|
|
} else {
|
|
child_bases.insert(field.clone(), metric.clone());
|
|
}
|
|
}
|
|
|
|
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
|
|
println!(" Child bases:");
|
|
for (field, base) in &child_bases {
|
|
println!(" {} -> {}", field, base);
|
|
}
|
|
|
|
if let Some(prefix) = find_common_prefix(&bases) {
|
|
println!(" Common prefix: '{}'", prefix);
|
|
for (field, base) in &child_bases {
|
|
let relative = base.strip_prefix(&prefix).unwrap_or(base);
|
|
println!(" {} -> relative '{}'", field, relative);
|
|
}
|
|
} else {
|
|
println!(" No common prefix found!");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_cost_basis_pattern2_mode() {
|
|
// Debug why CostBasisPattern2 has mode=None
|
|
let catalog = load_catalog();
|
|
let metadata = brk_bindgen::ClientMetadata::from_catalog(catalog.clone());
|
|
let pattern_lookup = metadata.pattern_lookup();
|
|
|
|
let pattern = metadata
|
|
.find_pattern("CostBasisPattern2")
|
|
.expect("CostBasisPattern2 should exist");
|
|
|
|
println!("\nCostBasisPattern2 fields:");
|
|
for field in &pattern.fields {
|
|
println!(" {} (type: {})", field.name, field.rust_type);
|
|
}
|
|
println!("Mode: {:?}", pattern.mode);
|
|
|
|
// Now debug the instance collection
|
|
#[derive(Debug, Clone)]
|
|
struct DebugInstanceAnalysis {
|
|
base: String,
|
|
field_parts: std::collections::HashMap<String, String>,
|
|
is_suffix_mode: bool,
|
|
}
|
|
|
|
fn collect_debug(
|
|
node: &TreeNode,
|
|
pattern_lookup: &std::collections::HashMap<Vec<brk_bindgen::PatternField>, String>,
|
|
all_analyses: &mut std::collections::HashMap<String, Vec<DebugInstanceAnalysis>>,
|
|
) -> Option<String> {
|
|
match node {
|
|
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
|
|
TreeNode::Branch(children) => {
|
|
let mut child_bases: std::collections::HashMap<String, String> =
|
|
std::collections::HashMap::new();
|
|
for (field_name, child_node) in children {
|
|
if let Some(base) = collect_debug(child_node, pattern_lookup, all_analyses) {
|
|
child_bases.insert(field_name.clone(), base);
|
|
}
|
|
}
|
|
|
|
if child_bases.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Analyze this instance
|
|
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
|
|
let (base, field_parts, is_suffix_mode) =
|
|
if let Some(common_prefix) = brk_bindgen::find_common_prefix(&bases) {
|
|
let base = common_prefix.trim_end_matches('_').to_string();
|
|
let mut parts = std::collections::HashMap::new();
|
|
for (field_name, child_base) in &child_bases {
|
|
let relative = if *child_base == base {
|
|
String::new()
|
|
} else {
|
|
child_base
|
|
.strip_prefix(&common_prefix)
|
|
.unwrap_or(child_base)
|
|
.to_string()
|
|
};
|
|
parts.insert(field_name.clone(), relative);
|
|
}
|
|
(base, parts, true)
|
|
} else {
|
|
let base = child_bases.values().next().cloned().unwrap_or_default();
|
|
let parts = child_bases
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
(base, parts, true)
|
|
};
|
|
|
|
let analysis = DebugInstanceAnalysis {
|
|
base: base.clone(),
|
|
field_parts,
|
|
is_suffix_mode,
|
|
};
|
|
|
|
// Get the pattern name for this node
|
|
let fields = brk_bindgen::get_node_fields(children, pattern_lookup);
|
|
if let Some(pattern_name) = pattern_lookup.get(&fields) {
|
|
all_analyses
|
|
.entry(pattern_name.clone())
|
|
.or_default()
|
|
.push(analysis);
|
|
}
|
|
|
|
Some(base)
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut all_analyses: std::collections::HashMap<String, Vec<DebugInstanceAnalysis>> =
|
|
std::collections::HashMap::new();
|
|
collect_debug(&catalog, &pattern_lookup, &mut all_analyses);
|
|
|
|
if let Some(analyses) = all_analyses.get("CostBasisPattern2") {
|
|
println!(
|
|
"\nCollected {} instances of CostBasisPattern2:",
|
|
analyses.len()
|
|
);
|
|
for (i, a) in analyses.iter().enumerate() {
|
|
println!(" Instance {}:", i);
|
|
println!(" base: {}", a.base);
|
|
println!(" is_suffix: {}", a.is_suffix_mode);
|
|
println!(" field_parts:");
|
|
for (f, p) in &a.field_parts {
|
|
println!(" {} -> '{}'", f, p);
|
|
}
|
|
}
|
|
|
|
// Check consistency
|
|
if analyses.len() >= 2 {
|
|
let first = &analyses[0];
|
|
for (i, a) in analyses.iter().enumerate().skip(1) {
|
|
if a.is_suffix_mode != first.is_suffix_mode {
|
|
println!(" INCONSISTENT: Instance {} has different mode", i);
|
|
}
|
|
for (field, part) in &a.field_parts {
|
|
if first.field_parts.get(field) != Some(part) {
|
|
println!(
|
|
" INCONSISTENT: Instance {} field '{}' has part '{}' vs '{}'",
|
|
i,
|
|
field,
|
|
part,
|
|
first
|
|
.field_parts
|
|
.get(field)
|
|
.unwrap_or(&"<missing>".to_string())
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
println!("\nNo instances collected for CostBasisPattern2!");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_cost_basis_prefix() {
|
|
use brk_bindgen::find_common_prefix;
|
|
|
|
// Root-level cost_basis has:
|
|
// max -> "max_cost_basis"
|
|
// min -> "min_cost_basis"
|
|
// percentiles -> "cost_basis" (base of pct05, pct10, etc.)
|
|
|
|
let bases = vec!["max_cost_basis", "min_cost_basis", "cost_basis"];
|
|
let prefix = find_common_prefix(&bases);
|
|
println!("Root cost_basis prefix: {:?}", prefix);
|
|
|
|
// Compare with nested cost_basis
|
|
let nested_bases = vec![
|
|
"utxos_at_least_15y_old_max_cost_basis",
|
|
"utxos_at_least_15y_old_min_cost_basis",
|
|
"utxos_at_least_15y_old_cost_basis",
|
|
];
|
|
let nested_prefix = find_common_prefix(&nested_bases);
|
|
println!("Nested cost_basis prefix: {:?}", nested_prefix);
|
|
}
|
|
|
|
#[test]
|
|
fn test_utxo_cohorts_all_activity_base() {
|
|
// Test that distribution.utxo_cohorts.all.activity uses empty base
|
|
// because its children (coinblocks_destroyed, coindays_destroyed, etc.)
|
|
// have no common prefix or suffix.
|
|
let catalog = load_catalog();
|
|
let metadata = ClientMetadata::from_catalog(catalog.clone());
|
|
|
|
// Generate JavaScript output
|
|
let mut js_output = String::new();
|
|
writeln!(js_output, "// Test output").unwrap();
|
|
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
|
|
brk_bindgen::javascript::client::generate_index_accessors(
|
|
&mut js_output,
|
|
&metadata.index_set_patterns,
|
|
);
|
|
brk_bindgen::javascript::client::generate_structural_patterns(
|
|
&mut js_output,
|
|
&metadata.structural_patterns,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::javascript::tree::generate_tree_typedefs(
|
|
&mut js_output,
|
|
&metadata.catalog,
|
|
&metadata,
|
|
);
|
|
brk_bindgen::javascript::tree::generate_main_client(
|
|
&mut js_output,
|
|
&metadata.catalog,
|
|
&metadata,
|
|
&[],
|
|
);
|
|
|
|
// The all.activity should use empty base, so metrics don't get duplicated
|
|
// Look for: activity: createActivityPattern2(this, '')
|
|
// NOT: activity: createActivityPattern2(this, 'coinblocks_destroyed')
|
|
assert!(
|
|
!js_output.contains("createActivityPattern2(this, 'coinblocks_destroyed')"),
|
|
"all.activity should NOT use 'coinblocks_destroyed' as base (causes duplication)"
|
|
);
|
|
|
|
// Check that it uses empty string as base
|
|
assert!(
|
|
js_output.contains("activity: createActivityPattern2(this, '')"),
|
|
"all.activity should use empty base"
|
|
);
|
|
|
|
println!("utxo_cohorts.all.activity base test passed!");
|
|
}
|