use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::{Query, State};
use axum::Form;
use maud::{html, Markup};
use serde::{Deserialize, Serialize};
use graph_engine::{
AStarConfig, BiconnectedConfig, CentralityConfig, CommunityConfig, Direction, KCoreConfig,
MstConfig, PageRankConfig, SccConfig, SimilarityConfig, SimilarityMetric, TriangleConfig,
};
use crate::web::templates::layout;
use crate::web::templates::layout::{format_number, m_breadcrumb, m_empty, m_header};
use crate::web::AdminContext;
use crate::web::NavItem;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AlgorithmCategory {
Centrality,
Community,
Pathfinding,
Structure,
Similarity,
}
impl AlgorithmCategory {
const fn label(self) -> &'static str {
match self {
Self::Centrality => "CENTRALITY",
Self::Community => "COMMUNITY",
Self::Pathfinding => "PATHFINDING",
Self::Structure => "STRUCTURE",
Self::Similarity => "SIMILARITY",
}
}
}
struct AlgorithmDef {
id: &'static str,
name: &'static str,
category: AlgorithmCategory,
description: &'static str,
params: &'static [ParamDef],
}
struct ParamDef {
name: &'static str,
label: &'static str,
param_type: ParamType,
default: &'static str,
description: &'static str,
}
enum ParamType {
Float,
Int,
NodeId,
Direction,
SimilarityMetric,
}
const ALGORITHMS: &[AlgorithmDef] = &[
AlgorithmDef {
id: "pagerank",
name: "PageRank",
category: AlgorithmCategory::Centrality,
description: "Compute node importance based on link structure (iterative random walk)",
params: &[
ParamDef {
name: "damping",
label: "Damping Factor",
param_type: ParamType::Float,
default: "0.85",
description: "Probability of following links vs random jump (0.0-1.0)",
},
ParamDef {
name: "tolerance",
label: "Tolerance",
param_type: ParamType::Float,
default: "0.000001",
description: "Convergence threshold",
},
ParamDef {
name: "max_iterations",
label: "Max Iterations",
param_type: ParamType::Int,
default: "100",
description: "Maximum iteration count",
},
ParamDef {
name: "top_k",
label: "Top K",
param_type: ParamType::Int,
default: "20",
description: "Number of top results to show",
},
],
},
AlgorithmDef {
id: "betweenness",
name: "Betweenness Centrality",
category: AlgorithmCategory::Centrality,
description: "Measure how often a node lies on shortest paths between other nodes",
params: &[
ParamDef {
name: "top_k",
label: "Top K",
param_type: ParamType::Int,
default: "20",
description: "Number of top results to show",
},
ParamDef {
name: "direction",
label: "Direction",
param_type: ParamType::Direction,
default: "both",
description: "Edge direction to follow",
},
],
},
AlgorithmDef {
id: "closeness",
name: "Closeness Centrality",
category: AlgorithmCategory::Centrality,
description: "Measure average shortest path distance to all other nodes",
params: &[
ParamDef {
name: "top_k",
label: "Top K",
param_type: ParamType::Int,
default: "20",
description: "Number of top results to show",
},
ParamDef {
name: "direction",
label: "Direction",
param_type: ParamType::Direction,
default: "both",
description: "Edge direction to follow",
},
],
},
AlgorithmDef {
id: "eigenvector",
name: "Eigenvector Centrality",
category: AlgorithmCategory::Centrality,
description: "Measure influence based on connections to other influential nodes",
params: &[
ParamDef {
name: "top_k",
label: "Top K",
param_type: ParamType::Int,
default: "20",
description: "Number of top results to show",
},
ParamDef {
name: "max_iterations",
label: "Max Iterations",
param_type: ParamType::Int,
default: "100",
description: "Maximum iteration count",
},
ParamDef {
name: "tolerance",
label: "Tolerance",
param_type: ParamType::Float,
default: "0.000001",
description: "Convergence threshold",
},
],
},
AlgorithmDef {
id: "louvain",
name: "Louvain Communities",
category: AlgorithmCategory::Community,
description: "Detect communities by modularity optimization (hierarchical)",
params: &[
ParamDef {
name: "resolution",
label: "Resolution",
param_type: ParamType::Float,
default: "1.0",
description: "Controls community size (higher = smaller communities)",
},
ParamDef {
name: "max_passes",
label: "Max Passes",
param_type: ParamType::Int,
default: "10",
description: "Maximum number of Louvain passes",
},
],
},
AlgorithmDef {
id: "label_propagation",
name: "Label Propagation",
category: AlgorithmCategory::Community,
description: "Fast community detection via label spreading",
params: &[ParamDef {
name: "max_iterations",
label: "Max Iterations",
param_type: ParamType::Int,
default: "100",
description: "Maximum iteration count",
}],
},
AlgorithmDef {
id: "connected_components",
name: "Connected Components",
category: AlgorithmCategory::Community,
description: "Find groups of interconnected nodes (Union-Find)",
params: &[ParamDef {
name: "direction",
label: "Direction",
param_type: ParamType::Direction,
default: "both",
description: "Edge direction (both = undirected)",
}],
},
AlgorithmDef {
id: "astar",
name: "A* Pathfinding",
category: AlgorithmCategory::Pathfinding,
description: "Find optimal path using heuristic-guided search",
params: &[
ParamDef {
name: "from",
label: "Source Node",
param_type: ParamType::NodeId,
default: "",
description: "Starting node ID",
},
ParamDef {
name: "to",
label: "Target Node",
param_type: ParamType::NodeId,
default: "",
description: "Destination node ID",
},
ParamDef {
name: "max_depth",
label: "Max Depth",
param_type: ParamType::Int,
default: "100",
description: "Maximum search depth",
},
],
},
AlgorithmDef {
id: "dijkstra",
name: "Dijkstra Shortest Path",
category: AlgorithmCategory::Pathfinding,
description: "Find shortest weighted path between nodes",
params: &[
ParamDef {
name: "from",
label: "Source Node",
param_type: ParamType::NodeId,
default: "",
description: "Starting node ID",
},
ParamDef {
name: "to",
label: "Target Node",
param_type: ParamType::NodeId,
default: "",
description: "Destination node ID",
},
ParamDef {
name: "weight_property",
label: "Weight Property",
param_type: ParamType::Int,
default: "",
description: "Edge property name for weights (empty = uniform)",
},
],
},
AlgorithmDef {
id: "variable_paths",
name: "Variable-Length Paths",
category: AlgorithmCategory::Pathfinding,
description: "Find all paths within hop range between two nodes",
params: &[
ParamDef {
name: "from",
label: "Source Node",
param_type: ParamType::NodeId,
default: "",
description: "Starting node ID",
},
ParamDef {
name: "to",
label: "Target Node",
param_type: ParamType::NodeId,
default: "",
description: "Destination node ID",
},
ParamDef {
name: "min_hops",
label: "Min Hops",
param_type: ParamType::Int,
default: "1",
description: "Minimum path length",
},
ParamDef {
name: "max_hops",
label: "Max Hops",
param_type: ParamType::Int,
default: "3",
description: "Maximum path length",
},
ParamDef {
name: "max_paths",
label: "Max Paths",
param_type: ParamType::Int,
default: "100",
description: "Maximum paths to return",
},
],
},
AlgorithmDef {
id: "kcore",
name: "K-Core Decomposition",
category: AlgorithmCategory::Structure,
description: "Find densely connected subgraphs by core number",
params: &[ParamDef {
name: "min_k",
label: "Minimum K",
param_type: ParamType::Int,
default: "1",
description: "Minimum core number to report",
}],
},
AlgorithmDef {
id: "scc",
name: "Strongly Connected Components",
category: AlgorithmCategory::Structure,
description: "Find maximal strongly connected subgraphs (Tarjan)",
params: &[],
},
AlgorithmDef {
id: "mst",
name: "Minimum Spanning Tree",
category: AlgorithmCategory::Structure,
description: "Find minimum weight tree connecting all nodes (Kruskal)",
params: &[ParamDef {
name: "weight_property",
label: "Weight Property",
param_type: ParamType::Int,
default: "",
description: "Edge property for weights (empty = uniform)",
}],
},
AlgorithmDef {
id: "biconnected",
name: "Biconnected Components",
category: AlgorithmCategory::Structure,
description: "Find articulation points and bridges",
params: &[],
},
AlgorithmDef {
id: "triangles",
name: "Triangle Counting",
category: AlgorithmCategory::Structure,
description: "Count triangles and compute clustering coefficients",
params: &[ParamDef {
name: "top_k",
label: "Top K",
param_type: ParamType::Int,
default: "20",
description: "Number of top nodes by triangle count",
}],
},
AlgorithmDef {
id: "similarity",
name: "Node Similarity",
category: AlgorithmCategory::Similarity,
description: "Compare neighborhoods using various metrics",
params: &[
ParamDef {
name: "node_a",
label: "Node A",
param_type: ParamType::NodeId,
default: "",
description: "First node ID",
},
ParamDef {
name: "node_b",
label: "Node B",
param_type: ParamType::NodeId,
default: "",
description: "Second node ID",
},
ParamDef {
name: "metric",
label: "Similarity Metric",
param_type: ParamType::SimilarityMetric,
default: "jaccard",
description: "Metric to compute",
},
],
},
];
#[derive(Debug, Deserialize)]
pub struct DashboardParams {
#[serde(default)]
pub category: Option<String>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default, Deserialize)]
pub struct ExecuteParams {
pub algorithm: String,
#[serde(default)]
pub top_k: Option<usize>,
#[serde(default)]
pub max_iterations: Option<usize>,
#[serde(default)]
pub tolerance: Option<f64>,
#[serde(default)]
pub direction: Option<String>,
#[serde(default)]
pub damping: Option<f64>,
#[serde(default)]
pub resolution: Option<f64>,
#[serde(default)]
pub max_passes: Option<usize>,
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub to: Option<String>,
#[serde(default)]
pub max_depth: Option<usize>,
#[serde(default)]
pub weight_property: Option<String>,
#[serde(default)]
pub min_hops: Option<usize>,
#[serde(default)]
pub max_hops: Option<usize>,
#[serde(default)]
pub max_paths: Option<usize>,
#[serde(default)]
pub min_k: Option<usize>,
#[serde(default)]
pub node_a: Option<String>,
#[serde(default)]
pub node_b: Option<String>,
#[serde(default)]
pub metric: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct AlgorithmResult {
pub algorithm: String,
pub status: ResultStatus,
pub elapsed_ms: u64,
pub data: ResultData,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultStatus {
Success,
Error,
NoData,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum ResultData {
Scores(Vec<(u64, f64)>),
Communities(CommunityData),
Path(PathData),
Structure(StructureData),
Similarity(f64),
Error(String),
Empty,
}
#[derive(Debug, Serialize)]
pub struct CommunityData {
pub count: usize,
pub modularity: Option<f64>,
pub communities: Vec<(u64, usize)>,
}
#[derive(Debug, Serialize)]
pub struct PathData {
pub nodes: Vec<u64>,
pub weight: Option<f64>,
pub found: bool,
}
#[derive(Debug, Serialize)]
pub struct StructureData {
pub summary: HashMap<String, String>,
pub items: Vec<(String, String)>,
}
pub async fn dashboard(
State(ctx): State<Arc<AdminContext>>,
Query(params): Query<DashboardParams>,
) -> Markup {
let node_count = ctx.graph.node_count();
let edge_count = ctx.graph.edge_count();
let selected_category = params.category.as_deref();
let content = html! {
(m_breadcrumb(&[("/graph", "GRAPH"), ("", "ALGORITHMS")]))
(m_header("ALGORITHM DASHBOARD", Some("Execute graph analysis algorithms")))
div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6" {
div class="m-card" {
div class="m-card-content text-center" {
div class="text-3xl font-mono text-white" { (format_number(node_count)) }
div class="text-xs text-neutral-400" { "NODES" }
}
}
div class="m-card" {
div class="m-card-content text-center" {
div class="text-3xl font-mono text-white" { (format_number(edge_count)) }
div class="text-xs text-neutral-400" { "EDGES" }
}
}
div class="m-card" {
div class="m-card-content text-center" {
div class="text-3xl font-mono text-neutral-300" { (ALGORITHMS.len()) }
div class="text-xs text-neutral-400" { "ALGORITHMS" }
}
}
}
div class="m-card mb-6" {
div class="m-card-header" { "ALGORITHM CATEGORIES" }
div class="m-card-content" {
div class="flex flex-wrap gap-2" {
a href="/graph/algorithms/dashboard"
class=(if selected_category.is_none() { "m-btn m-btn-active" } else { "m-btn" }) {
"[ ALL ]"
}
@for cat in [AlgorithmCategory::Centrality, AlgorithmCategory::Community, AlgorithmCategory::Pathfinding, AlgorithmCategory::Structure, AlgorithmCategory::Similarity] {
a href=(format!("/graph/algorithms/dashboard?category={}", cat.label().to_lowercase()))
class=(if selected_category.is_some_and(|c| c.eq_ignore_ascii_case(cat.label())) { "m-btn m-btn-active" } else { "m-btn" }) {
"[ " (cat.label()) " ]"
}
}
}
}
}
@if node_count == 0 {
(m_empty("NO GRAPH DATA", "Create nodes and edges to run algorithms"))
} @else {
div class="grid grid-cols-1 lg:grid-cols-2 gap-4" {
@for algo in ALGORITHMS {
@if selected_category.is_none() || selected_category.is_some_and(|c| c.eq_ignore_ascii_case(algo.category.label())) {
(render_algorithm_card(algo))
}
}
}
}
};
layout::layout("Algorithm Dashboard", NavItem::Graph, content)
}
#[allow(clippy::option_if_let_else)]
pub async fn execute_form(
State(_ctx): State<Arc<AdminContext>>,
Query(params): Query<ExecuteParams>,
) -> Markup {
let algo = ALGORITHMS.iter().find(|a| a.id == params.algorithm);
let content = if let Some(algo) = algo {
html! {
(m_breadcrumb(&[("/graph", "GRAPH"), ("/graph/algorithms/dashboard", "ALGORITHMS"), ("", algo.name)]))
(m_header(algo.name, Some(algo.description)))
div class="m-card mb-6" {
div class="m-card-header" { "PARAMETERS" }
div class="m-card-content" {
form method="post" action="/graph/algorithms/execute" class="space-y-4" {
input type="hidden" name="algorithm" value=(algo.id);
div class="grid grid-cols-1 md:grid-cols-2 gap-4" {
@for param in algo.params {
div {
label for=(param.name) class="block text-sm text-neutral-400 mb-2" {
(param.label)
}
@match param.param_type {
ParamType::Float | ParamType::Int | ParamType::NodeId => {
input
type="text"
id=(param.name)
name=(param.name)
value=(param.default)
placeholder=(param.description)
class="m-input w-full";
}
ParamType::Direction => {
select id=(param.name) name=(param.name) class="m-input w-full" {
option value="both" selected[param.default == "both"] { "Both (Undirected)" }
option value="outgoing" selected[param.default == "outgoing"] { "Outgoing" }
option value="incoming" selected[param.default == "incoming"] { "Incoming" }
}
}
ParamType::SimilarityMetric => {
select id=(param.name) name=(param.name) class="m-input w-full" {
option value="jaccard" selected[param.default == "jaccard"] { "Jaccard" }
option value="cosine" { "Cosine" }
option value="adamic_adar" { "Adamic-Adar" }
option value="resource_allocation" { "Resource Allocation" }
option value="preferential_attachment" { "Preferential Attachment" }
option value="common_neighbors" { "Common Neighbors" }
}
}
}
p class="text-xs text-neutral-500 mt-1" { (param.description) }
}
}
}
button type="submit" class="m-btn" { "[ EXECUTE ALGORITHM ]" }
}
}
}
div class="m-card" {
div class="m-card-header" { "ALGORITHM INFO" }
div class="m-card-content text-sm" {
div class="flex gap-2 mb-2" {
span class="text-neutral-400" { "CATEGORY:" }
span class="text-neutral-300" { (algo.category.label()) }
}
p class="text-neutral-400" { (algo.description) }
}
}
}
} else {
html! {
(m_breadcrumb(&[("/graph", "GRAPH"), ("/graph/algorithms/dashboard", "ALGORITHMS"), ("", "NOT FOUND")]))
(m_empty("ALGORITHM NOT FOUND", "The requested algorithm does not exist"))
}
};
layout::layout("Execute Algorithm", NavItem::Graph, content)
}
pub async fn execute_submit(
State(ctx): State<Arc<AdminContext>>,
Form(params): Form<ExecuteParams>,
) -> Markup {
let start = std::time::Instant::now();
let result = execute_algorithm(&ctx, ¶ms);
#[allow(clippy::cast_possible_truncation)] let elapsed_ms = start.elapsed().as_millis() as u64;
let algo = ALGORITHMS.iter().find(|a| a.id == params.algorithm);
let algo_name = algo.map_or("Unknown", |a| a.name);
let status_text = match result.status {
ResultStatus::Success => ("text-white", "SUCCESS"),
ResultStatus::Error => ("text-neutral-400", "ERROR"),
ResultStatus::NoData => ("text-neutral-300", "NO DATA"),
};
let content = html! {
(m_breadcrumb(&[("/graph", "GRAPH"), ("/graph/algorithms/dashboard", "ALGORITHMS"), ("", algo_name)]))
(m_header(&format!("{} RESULTS", algo_name.to_uppercase()), None))
div class="m-card mb-6" {
div class="m-card-header" { "EXECUTION STATS" }
div class="m-card-content" {
div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm" {
div {
span class="text-neutral-400" { "ALGORITHM: " }
span class="text-white" { (algo_name) }
}
div {
span class="text-neutral-400" { "STATUS: " }
span class=(status_text.0) { (status_text.1) }
}
div {
span class="text-neutral-400" { "TIME: " }
span class="text-neutral-300 font-mono" { (elapsed_ms) "ms" }
}
div {
a href=(format!("/graph/algorithms/execute?algorithm={}", params.algorithm)) class="m-btn text-xs" {
"[ RUN AGAIN ]"
}
}
}
}
}
(render_result(&result))
div class="mt-6" {
a href="/graph/algorithms/dashboard" class="m-btn" { "[ BACK TO DASHBOARD ]" }
}
};
layout::layout("Algorithm Results", NavItem::Graph, content)
}
fn parse_direction(s: &str) -> Direction {
match s.to_lowercase().as_str() {
"outgoing" | "out" => Direction::Outgoing,
"incoming" | "in" => Direction::Incoming,
_ => Direction::Both,
}
}
fn parse_similarity_metric(s: &str) -> SimilarityMetric {
match s.to_lowercase().as_str() {
"cosine" => SimilarityMetric::Cosine,
"adamic_adar" | "adamicadar" => SimilarityMetric::AdamicAdar,
"resource_allocation" | "resourceallocation" => SimilarityMetric::ResourceAllocation,
"preferential_attachment" | "preferentialattachment" => {
SimilarityMetric::PreferentialAttachment
},
"common_neighbors" | "commonneighbors" => SimilarityMetric::CommonNeighbors,
_ => SimilarityMetric::Jaccard,
}
}
#[allow(clippy::too_many_lines)]
fn execute_algorithm(ctx: &AdminContext, params: &ExecuteParams) -> AlgorithmResult {
let algorithm = params.algorithm.clone();
match params.algorithm.as_str() {
"pagerank" => {
let config = PageRankConfig::default()
.damping(params.damping.unwrap_or(0.85))
.tolerance(params.tolerance.unwrap_or(1e-6))
.max_iterations(params.max_iterations.unwrap_or(100));
match ctx.graph.pagerank(Some(config)) {
Ok(pr) => {
let top_k = params.top_k.unwrap_or(20);
let scores = pr.top_k(top_k);
AlgorithmResult {
algorithm,
status: if scores.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Scores(scores),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"betweenness" => {
let direction = params
.direction
.as_deref()
.map_or(Direction::Both, parse_direction);
let config = CentralityConfig::default().direction(direction);
match ctx.graph.betweenness_centrality(Some(config)) {
Ok(result) => {
let top_k = params.top_k.unwrap_or(20);
let mut scores: Vec<_> = result.scores.into_iter().collect();
scores
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores.truncate(top_k);
AlgorithmResult {
algorithm,
status: if scores.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Scores(scores),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"closeness" => {
let direction = params
.direction
.as_deref()
.map_or(Direction::Both, parse_direction);
let config = CentralityConfig::default().direction(direction);
match ctx.graph.closeness_centrality(Some(config)) {
Ok(result) => {
let top_k = params.top_k.unwrap_or(20);
let mut scores: Vec<_> = result.scores.into_iter().collect();
scores
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores.truncate(top_k);
AlgorithmResult {
algorithm,
status: if scores.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Scores(scores),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"eigenvector" => {
let config = CentralityConfig::default()
.max_iterations(params.max_iterations.unwrap_or(100))
.tolerance(params.tolerance.unwrap_or(1e-6));
match ctx.graph.eigenvector_centrality(Some(config)) {
Ok(result) => {
let top_k = params.top_k.unwrap_or(20);
let mut scores: Vec<_> = result.scores.into_iter().collect();
scores
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores.truncate(top_k);
AlgorithmResult {
algorithm,
status: if scores.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Scores(scores),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"louvain" => {
let config = CommunityConfig::default()
.resolution(params.resolution.unwrap_or(1.0))
.max_passes(params.max_passes.unwrap_or(10));
match ctx.graph.louvain_communities(Some(config)) {
Ok(result) => {
let communities: Vec<_> = result.communities_by_size();
AlgorithmResult {
algorithm,
status: if communities.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Communities(CommunityData {
count: result.community_count,
modularity: result.modularity,
communities,
}),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"label_propagation" => {
let config =
CommunityConfig::default().max_iterations(params.max_iterations.unwrap_or(100));
match ctx.graph.label_propagation(Some(config)) {
Ok(result) => {
let communities: Vec<_> = result.communities_by_size();
AlgorithmResult {
algorithm,
status: if communities.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Communities(CommunityData {
count: result.community_count,
modularity: None,
communities,
}),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"connected_components" => {
let direction = params
.direction
.as_deref()
.map_or(Direction::Both, parse_direction);
let config = CommunityConfig::default().direction(direction);
match ctx.graph.connected_components(Some(config)) {
Ok(result) => {
let communities: Vec<_> = result.communities_by_size();
AlgorithmResult {
algorithm,
status: if communities.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Communities(CommunityData {
count: result.community_count,
modularity: None,
communities,
}),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"astar" => {
let from: Option<u64> = params.from.as_ref().and_then(|s| s.parse().ok());
let to: Option<u64> = params.to.as_ref().and_then(|s| s.parse().ok());
match (from, to) {
(Some(from_id), Some(to_id)) => {
let config = AStarConfig::new();
match ctx.graph.astar_path(from_id, to_id, &config) {
Ok(result) => {
if let Some(weighted_path) = result.path {
AlgorithmResult {
algorithm,
status: ResultStatus::Success,
elapsed_ms: 0,
data: ResultData::Path(PathData {
nodes: weighted_path.nodes,
weight: Some(weighted_path.total_weight),
found: true,
}),
}
} else {
AlgorithmResult {
algorithm,
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Path(PathData {
nodes: vec![],
weight: None,
found: false,
}),
}
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
_ => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Invalid source or target node ID".to_string()),
},
}
},
"dijkstra" => {
let from: Option<u64> = params.from.as_ref().and_then(|s| s.parse().ok());
let to: Option<u64> = params.to.as_ref().and_then(|s| s.parse().ok());
let weight_prop = params
.weight_property
.as_ref()
.filter(|s| !s.is_empty())
.map_or("weight", String::as_str);
match (from, to) {
(Some(from_id), Some(to_id)) => {
match ctx.graph.find_weighted_path(from_id, to_id, weight_prop) {
Ok(path) => AlgorithmResult {
algorithm,
status: ResultStatus::Success,
elapsed_ms: 0,
data: ResultData::Path(PathData {
nodes: path.nodes,
weight: Some(path.total_weight),
found: true,
}),
},
Err(e) => {
if matches!(e, graph_engine::GraphError::PathNotFound) {
AlgorithmResult {
algorithm,
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Path(PathData {
nodes: vec![],
weight: None,
found: false,
}),
}
} else {
AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
}
}
},
}
},
_ => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Invalid source or target node ID".to_string()),
},
}
},
"variable_paths" => {
let from: Option<u64> = params.from.as_ref().and_then(|s| s.parse().ok());
let to: Option<u64> = params.to.as_ref().and_then(|s| s.parse().ok());
match (from, to) {
(Some(from_id), Some(to_id)) => {
let config = graph_engine::VariableLengthConfig::with_hops(
params.min_hops.unwrap_or(1),
params.max_hops.unwrap_or(3),
)
.max_paths(params.max_paths.unwrap_or(100));
match ctx.graph.find_variable_paths(from_id, to_id, config) {
Ok(result) => {
let paths = &result.paths;
let summary = HashMap::from([
("paths_found".to_string(), paths.len().to_string()),
(
"min_length".to_string(),
paths
.iter()
.map(|p| p.nodes.len())
.min()
.unwrap_or(0)
.to_string(),
),
(
"max_length".to_string(),
paths
.iter()
.map(|p| p.nodes.len())
.max()
.unwrap_or(0)
.to_string(),
),
]);
let items: Vec<_> = paths
.iter()
.take(20)
.map(|p| {
let path_str = p
.nodes
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" -> ");
(format!("{} nodes", p.nodes.len()), path_str)
})
.collect();
AlgorithmResult {
algorithm,
status: if paths.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Structure(StructureData { summary, items }),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
_ => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Invalid source or target node ID".to_string()),
},
}
},
"kcore" => {
let config = KCoreConfig::default();
match ctx.graph.kcore_decomposition(&config) {
Ok(result) => {
let min_k = params.min_k.unwrap_or(1);
let mut summary = HashMap::new();
summary.insert("degeneracy".to_string(), result.degeneracy.to_string());
summary.insert("num_cores".to_string(), result.cores.len().to_string());
let items: Vec<_> = result
.core_numbers
.iter()
.filter(|(_, &k)| k >= min_k)
.take(50)
.map(|(node_id, k)| (format!("Node {node_id}"), format!("k={k}")))
.collect();
AlgorithmResult {
algorithm,
status: if items.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Structure(StructureData { summary, items }),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"scc" => {
let config = SccConfig::default();
match ctx.graph.strongly_connected_components(&config) {
Ok(result) => {
let mut summary = HashMap::new();
summary.insert(
"component_count".to_string(),
result.component_count.to_string(),
);
let items: Vec<_> = result
.members
.iter()
.enumerate()
.take(20)
.map(|(i, comp): (usize, &Vec<u64>)| {
let preview: String = comp
.iter()
.take(5)
.map(|n: &u64| n.to_string())
.collect::<Vec<_>>()
.join(", ");
let suffix = if comp.len() > 5 {
format!("... ({} total)", comp.len())
} else {
String::new()
};
(format!("SCC {}", i + 1), format!("[{preview}{suffix}]"))
})
.collect();
AlgorithmResult {
algorithm,
status: if items.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Structure(StructureData { summary, items }),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"mst" => {
let weight_prop = params
.weight_property
.as_ref()
.filter(|s| !s.is_empty())
.map_or("weight", String::as_str);
let config = MstConfig::new(weight_prop);
match ctx.graph.minimum_spanning_tree(&config) {
Ok(result) => {
let mut summary = HashMap::new();
summary.insert("edge_count".to_string(), result.edges.len().to_string());
summary.insert(
"total_weight".to_string(),
format!("{:.4}", result.total_weight),
);
let items: Vec<_> = result
.edges
.iter()
.take(30)
.map(|e| {
(
format!("{} -> {}", e.from, e.to),
format!("weight: {:.4}", e.weight),
)
})
.collect();
AlgorithmResult {
algorithm,
status: if items.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Structure(StructureData { summary, items }),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"biconnected" => {
let config = BiconnectedConfig::default();
match ctx.graph.biconnected_components(&config) {
Ok(result) => {
let mut summary = HashMap::new();
summary.insert(
"articulation_points".to_string(),
result.articulation_points.len().to_string(),
);
summary.insert("bridges".to_string(), result.bridges.len().to_string());
summary.insert("components".to_string(), result.component_count.to_string());
let mut items = Vec::new();
for ap in result.articulation_points.iter().take(10) {
items.push((
format!("Articulation Point {ap}"),
"critical node".to_string(),
));
}
for (from, to) in result.bridges.iter().take(10) {
items.push((format!("Bridge {from} - {to}"), "critical edge".to_string()));
}
AlgorithmResult {
algorithm,
status: ResultStatus::Success,
elapsed_ms: 0,
data: ResultData::Structure(StructureData { summary, items }),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"triangles" => {
let config = TriangleConfig::default();
match ctx.graph.count_triangles(&config) {
Ok(result) => {
let top_k = params.top_k.unwrap_or(20);
#[allow(clippy::cast_precision_loss)]
let mut scores: Vec<_> = result
.node_triangles
.iter()
.map(|(k, v)| (*k, *v as f64))
.collect();
scores
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores.truncate(top_k);
AlgorithmResult {
algorithm,
status: if scores.is_empty() {
ResultStatus::NoData
} else {
ResultStatus::Success
},
elapsed_ms: 0,
data: ResultData::Scores(scores),
}
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
"similarity" => {
let node_a: Option<u64> = params.node_a.as_ref().and_then(|s| s.parse().ok());
let node_b: Option<u64> = params.node_b.as_ref().and_then(|s| s.parse().ok());
let metric = params
.metric
.as_deref()
.map_or(SimilarityMetric::Jaccard, parse_similarity_metric);
match (node_a, node_b) {
(Some(a), Some(b)) => {
let config = SimilarityConfig::new();
match ctx.graph.node_similarity(a, b, metric, &config) {
Ok(result) => AlgorithmResult {
algorithm,
status: ResultStatus::Success,
elapsed_ms: 0,
data: ResultData::Similarity(result.score),
},
Err(e) => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error(e.to_string()),
},
}
},
_ => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Invalid node IDs".to_string()),
},
}
},
_ => AlgorithmResult {
algorithm,
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Unknown algorithm".to_string()),
},
}
}
fn render_algorithm_card(algo: &AlgorithmDef) -> Markup {
let category_color = match algo.category {
AlgorithmCategory::Centrality => "text-white",
AlgorithmCategory::Community | AlgorithmCategory::Similarity => "text-neutral-300",
AlgorithmCategory::Pathfinding | AlgorithmCategory::Structure => "text-neutral-400",
};
html! {
div class="m-card hover:border-neutral-600 transition-colors" {
div class="m-card-header flex justify-between items-center" {
span { (algo.name) }
span class=(format!("text-xs {category_color}")) { (algo.category.label()) }
}
div class="m-card-content" {
p class="text-sm text-neutral-400 mb-4" { (algo.description) }
div class="flex justify-between items-center" {
span class="text-xs text-neutral-500" {
(algo.params.len()) " parameters"
}
a href=(format!("/graph/algorithms/execute?algorithm={}", algo.id))
class="m-btn text-xs" {
"[ CONFIGURE ]"
}
}
}
}
}
}
#[allow(clippy::too_many_lines)]
fn render_result(result: &AlgorithmResult) -> Markup {
match &result.data {
ResultData::Scores(scores) => html! {
div class="m-card" {
div class="m-card-header" { "NODE SCORES" }
div class="m-card-content p-0" {
@if scores.is_empty() {
div class="p-4 text-neutral-400 italic" { "< NO RESULTS >" }
} @else {
table class="m-table" {
thead {
tr {
th class="w-16" { "#" }
th { "NODE ID" }
th class="text-right w-32" { "SCORE" }
}
}
tbody {
@for (idx, (node_id, score)) in scores.iter().enumerate() {
tr {
td class="text-neutral-400 font-mono" { (idx + 1) }
td class="text-white font-mono" { (node_id) }
td class="text-right text-neutral-300 font-mono" { (format!("{score:.6}")) }
}
}
}
}
}
}
}
},
ResultData::Communities(data) => html! {
div class="m-card" {
div class="m-card-header" { "COMMUNITY DETECTION" }
div class="m-card-content" {
div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4 text-sm" {
div {
span class="text-neutral-400" { "COMMUNITIES: " }
span class="text-neutral-300 font-mono" { (data.count) }
}
@if let Some(mod_val) = data.modularity {
div {
span class="text-neutral-400" { "MODULARITY: " }
span class="text-neutral-300 font-mono" { (format!("{mod_val:.4}")) }
}
}
}
@if data.communities.is_empty() {
div class="text-neutral-400 italic" { "< NO COMMUNITIES FOUND >" }
} @else {
table class="m-table" {
thead {
tr {
th { "COMMUNITY ID" }
th class="text-right" { "SIZE" }
}
}
tbody {
@for (comm_id, size) in data.communities.iter().take(20) {
tr {
td class="text-white font-mono" { (comm_id) }
td class="text-right text-neutral-300 font-mono" { (size) }
}
}
}
}
@if data.communities.len() > 20 {
div class="mt-2 text-xs text-neutral-500" {
"Showing 20 of " (data.communities.len()) " communities"
}
}
}
}
}
},
ResultData::Path(data) => html! {
div class="m-card" {
div class="m-card-header" { "PATH RESULT" }
div class="m-card-content" {
@if data.found {
div class="mb-4 text-sm" {
span class="text-neutral-400" { "PATH LENGTH: " }
span class="text-white" { (data.nodes.len()) " nodes" }
@if let Some(weight) = data.weight {
span class="ml-4 text-neutral-400" { "TOTAL WEIGHT: " }
span class="text-neutral-300 font-mono" { (format!("{weight:.4}")) }
}
}
div class="overflow-x-auto mb-4" {
div class="flex items-center gap-2 min-w-max" {
@for (idx, node_id) in data.nodes.iter().enumerate() {
div class="flex flex-col items-center" {
div class="w-12 h-12 border-2 border-neutral-600 bg-neutral-900 flex items-center justify-center" {
span class="text-sm font-mono text-white" { (node_id) }
}
}
@if idx < data.nodes.len() - 1 {
div class="flex items-center text-neutral-400" { "--->" }
}
}
}
}
div class="text-sm text-neutral-400" {
"SEQUENCE: "
@for (idx, node_id) in data.nodes.iter().enumerate() {
span class="text-white" { (node_id) }
@if idx < data.nodes.len() - 1 { " -> " }
}
}
} @else {
(m_empty("NO PATH FOUND", "The nodes are not connected"))
}
}
}
},
ResultData::Structure(data) => html! {
div class="m-card" {
div class="m-card-header" { "STRUCTURE ANALYSIS" }
div class="m-card-content" {
@if !data.summary.is_empty() {
div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4 text-sm" {
@for (key, value) in &data.summary {
div {
span class="text-neutral-400" { (key.to_uppercase()) ": " }
span class="text-neutral-300 font-mono" { (value) }
}
}
}
}
@if data.items.is_empty() {
div class="text-neutral-400 italic" { "< NO ITEMS >" }
} @else {
div class="space-y-1 max-h-96 overflow-y-auto" {
@for (label, value) in &data.items {
div class="flex justify-between text-sm border-b border-neutral-800 pb-1" {
span class="text-white" { (label) }
span class="text-neutral-400" { (value) }
}
}
}
}
}
}
},
ResultData::Similarity(score) => html! {
div class="m-card" {
div class="m-card-header" { "SIMILARITY SCORE" }
div class="m-card-content text-center py-8" {
div class="text-5xl font-mono text-neutral-300 mb-4" { (format!("{score:.6}")) }
div class="text-neutral-400" { "SIMILARITY COEFFICIENT" }
}
}
},
ResultData::Error(msg) => html! {
div class="m-card" {
div class="m-card-header" { "ERROR" }
div class="m-card-content text-neutral-300" { (msg) }
}
},
ResultData::Empty => html! {
(m_empty("NO DATA", "No results to display"))
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_algorithm_category_label() {
assert_eq!(AlgorithmCategory::Centrality.label(), "CENTRALITY");
assert_eq!(AlgorithmCategory::Community.label(), "COMMUNITY");
assert_eq!(AlgorithmCategory::Pathfinding.label(), "PATHFINDING");
assert_eq!(AlgorithmCategory::Structure.label(), "STRUCTURE");
assert_eq!(AlgorithmCategory::Similarity.label(), "SIMILARITY");
}
#[test]
fn test_parse_direction() {
assert!(matches!(parse_direction("outgoing"), Direction::Outgoing));
assert!(matches!(parse_direction("incoming"), Direction::Incoming));
assert!(matches!(parse_direction("both"), Direction::Both));
assert!(matches!(parse_direction("invalid"), Direction::Both));
}
#[test]
fn test_parse_similarity_metric() {
assert!(matches!(
parse_similarity_metric("jaccard"),
SimilarityMetric::Jaccard
));
assert!(matches!(
parse_similarity_metric("cosine"),
SimilarityMetric::Cosine
));
assert!(matches!(
parse_similarity_metric("adamic_adar"),
SimilarityMetric::AdamicAdar
));
}
#[test]
fn test_algorithms_defined() {
assert_eq!(ALGORITHMS.len(), 16);
let centrality: Vec<_> = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Centrality)
.collect();
assert_eq!(centrality.len(), 4);
let community: Vec<_> = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Community)
.collect();
assert_eq!(community.len(), 3);
}
#[test]
fn test_algorithm_card_rendering() {
let algo = &ALGORITHMS[0];
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains(algo.name));
assert!(html.contains("CONFIGURE"));
}
#[test]
fn test_result_rendering_scores() {
let result = AlgorithmResult {
algorithm: "pagerank".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Scores(vec![(1, 0.5), (2, 0.3)]),
};
let html = render_result(&result).0;
assert!(html.contains("NODE SCORES"));
assert!(html.contains("0.500000"));
}
#[test]
fn test_result_rendering_error() {
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("Test error".to_string()),
};
let html = render_result(&result).0;
assert!(html.contains("ERROR"));
assert!(html.contains("Test error"));
}
#[test]
fn test_result_rendering_similarity() {
let result = AlgorithmResult {
algorithm: "similarity".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Similarity(0.75),
};
let html = render_result(&result).0;
assert!(html.contains("SIMILARITY SCORE"));
assert!(html.contains("0.750000"));
}
#[test]
fn test_result_rendering_communities() {
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::Success,
elapsed_ms: 200,
data: ResultData::Communities(CommunityData {
count: 3,
modularity: Some(0.45),
communities: vec![(1, 10), (2, 5)],
}),
};
let html = render_result(&result).0;
assert!(html.contains("COMMUNITY DETECTION"));
assert!(html.contains("COMMUNITIES:"));
assert!(html.contains("MODULARITY:"));
}
#[test]
fn test_result_rendering_path() {
let result = AlgorithmResult {
algorithm: "astar".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Path(PathData {
nodes: vec![1, 2, 3],
weight: Some(2.5),
found: true,
}),
};
let html = render_result(&result).0;
assert!(html.contains("PATH RESULT"));
assert!(html.contains("3 nodes"));
assert!(html.contains("TOTAL WEIGHT"));
}
#[test]
fn test_result_rendering_structure() {
let result = AlgorithmResult {
algorithm: "kcore".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Structure(StructureData {
summary: HashMap::from([("degeneracy".to_string(), "5".to_string())]),
items: vec![("Node 1".to_string(), "k=5".to_string())],
}),
};
let html = render_result(&result).0;
assert!(html.contains("STRUCTURE ANALYSIS"));
assert!(html.contains("DEGENERACY"));
}
#[test]
fn test_dashboard_params_defaults() {
let params: DashboardParams = serde_json::from_str("{}").unwrap();
assert!(params.category.is_none());
}
#[test]
fn test_dashboard_params_with_category() {
let params: DashboardParams =
serde_json::from_str(r#"{"category": "centrality"}"#).unwrap();
assert_eq!(params.category.as_deref(), Some("centrality"));
}
#[test]
fn test_execute_params_defaults() {
let params: ExecuteParams = serde_json::from_str(r#"{"algorithm": "pagerank"}"#).unwrap();
assert_eq!(params.algorithm, "pagerank");
assert!(params.top_k.is_none());
assert!(params.max_iterations.is_none());
assert!(params.tolerance.is_none());
assert!(params.direction.is_none());
assert!(params.damping.is_none());
}
#[test]
fn test_execute_params_full() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "pagerank",
"top_k": 10,
"max_iterations": 50,
"tolerance": 0.001,
"damping": 0.9
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "pagerank");
assert_eq!(params.top_k, Some(10));
assert_eq!(params.max_iterations, Some(50));
assert_eq!(params.tolerance, Some(0.001));
assert_eq!(params.damping, Some(0.9));
}
#[test]
fn test_execute_params_pathfinding() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "astar",
"from": "123",
"to": "456",
"max_depth": 10
}"#,
)
.unwrap();
assert_eq!(params.from.as_deref(), Some("123"));
assert_eq!(params.to.as_deref(), Some("456"));
assert_eq!(params.max_depth, Some(10));
}
#[test]
fn test_execute_params_variable_paths() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "variable_paths",
"from": "1",
"to": "2",
"min_hops": 2,
"max_hops": 5,
"max_paths": 50
}"#,
)
.unwrap();
assert_eq!(params.min_hops, Some(2));
assert_eq!(params.max_hops, Some(5));
assert_eq!(params.max_paths, Some(50));
}
#[test]
fn test_execute_params_kcore() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "kcore",
"min_k": 3
}"#,
)
.unwrap();
assert_eq!(params.min_k, Some(3));
}
#[test]
fn test_execute_params_similarity() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "similarity",
"node_a": "10",
"node_b": "20",
"metric": "cosine"
}"#,
)
.unwrap();
assert_eq!(params.node_a.as_deref(), Some("10"));
assert_eq!(params.node_b.as_deref(), Some("20"));
assert_eq!(params.metric.as_deref(), Some("cosine"));
}
#[test]
fn test_execute_params_louvain() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "louvain",
"resolution": 1.5,
"max_passes": 20
}"#,
)
.unwrap();
assert_eq!(params.resolution, Some(1.5));
assert_eq!(params.max_passes, Some(20));
}
#[test]
fn test_result_status_serialization() {
let success = serde_json::to_string(&ResultStatus::Success).unwrap();
assert_eq!(success, "\"success\"");
let error = serde_json::to_string(&ResultStatus::Error).unwrap();
assert_eq!(error, "\"error\"");
let no_data = serde_json::to_string(&ResultStatus::NoData).unwrap();
assert_eq!(no_data, "\"no_data\"");
}
#[test]
fn test_community_data_serialization() {
let data = CommunityData {
count: 5,
modularity: Some(0.65),
communities: vec![(1, 100), (2, 50)],
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"count\":5"));
assert!(json.contains("\"modularity\":0.65"));
assert!(json.contains("\"communities\""));
}
#[test]
fn test_path_data_serialization() {
let data = PathData {
nodes: vec![1, 2, 3, 4],
weight: Some(10.5),
found: true,
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"nodes\":[1,2,3,4]"));
assert!(json.contains("\"weight\":10.5"));
assert!(json.contains("\"found\":true"));
}
#[test]
fn test_path_data_not_found() {
let data = PathData {
nodes: vec![],
weight: None,
found: false,
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"nodes\":[]"));
assert!(json.contains("\"weight\":null"));
assert!(json.contains("\"found\":false"));
}
#[test]
fn test_structure_data_serialization() {
let data = StructureData {
summary: HashMap::from([("key1".to_string(), "value1".to_string())]),
items: vec![("item1".to_string(), "desc1".to_string())],
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"summary\""));
assert!(json.contains("\"items\""));
}
#[test]
fn test_algorithm_result_serialization() {
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Success,
elapsed_ms: 42,
data: ResultData::Empty,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"algorithm\":\"test\""));
assert!(json.contains("\"status\":\"success\""));
assert!(json.contains("\"elapsed_ms\":42"));
}
#[test]
fn test_parse_direction_variations() {
assert!(matches!(parse_direction("out"), Direction::Outgoing));
assert!(matches!(parse_direction("in"), Direction::Incoming));
assert!(matches!(parse_direction("OUTGOING"), Direction::Outgoing));
assert!(matches!(parse_direction("INCOMING"), Direction::Incoming));
assert!(matches!(parse_direction("BOTH"), Direction::Both));
assert!(matches!(parse_direction("unknown"), Direction::Both));
}
#[test]
fn test_parse_similarity_metric_variations() {
assert!(matches!(
parse_similarity_metric("adamicadar"),
SimilarityMetric::AdamicAdar
));
assert!(matches!(
parse_similarity_metric("resourceallocation"),
SimilarityMetric::ResourceAllocation
));
assert!(matches!(
parse_similarity_metric("resource_allocation"),
SimilarityMetric::ResourceAllocation
));
assert!(matches!(
parse_similarity_metric("preferentialattachment"),
SimilarityMetric::PreferentialAttachment
));
assert!(matches!(
parse_similarity_metric("preferential_attachment"),
SimilarityMetric::PreferentialAttachment
));
assert!(matches!(
parse_similarity_metric("commonneighbors"),
SimilarityMetric::CommonNeighbors
));
assert!(matches!(
parse_similarity_metric("common_neighbors"),
SimilarityMetric::CommonNeighbors
));
assert!(matches!(
parse_similarity_metric("unknown"),
SimilarityMetric::Jaccard
));
}
#[test]
fn test_algorithm_category_serialization() {
let centrality = serde_json::to_string(&AlgorithmCategory::Centrality).unwrap();
assert_eq!(centrality, "\"centrality\"");
let community = serde_json::to_string(&AlgorithmCategory::Community).unwrap();
assert_eq!(community, "\"community\"");
let pathfinding = serde_json::to_string(&AlgorithmCategory::Pathfinding).unwrap();
assert_eq!(pathfinding, "\"pathfinding\"");
let structure = serde_json::to_string(&AlgorithmCategory::Structure).unwrap();
assert_eq!(structure, "\"structure\"");
let similarity = serde_json::to_string(&AlgorithmCategory::Similarity).unwrap();
assert_eq!(similarity, "\"similarity\"");
}
#[test]
fn test_result_rendering_empty() {
let result = AlgorithmResult {
algorithm: "empty".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Empty,
};
let html = render_result(&result).0;
assert!(html.contains("NO DATA"));
}
#[test]
fn test_result_rendering_path_not_found() {
let result = AlgorithmResult {
algorithm: "astar".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 10,
data: ResultData::Path(PathData {
nodes: vec![],
weight: None,
found: false,
}),
};
let html = render_result(&result).0;
assert!(html.contains("NO PATH FOUND"));
}
#[test]
fn test_result_rendering_scores_empty() {
let result = AlgorithmResult {
algorithm: "pagerank".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Scores(vec![]),
};
let html = render_result(&result).0;
assert!(html.contains("NO RESULTS"));
}
#[test]
fn test_result_rendering_communities_empty() {
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Communities(CommunityData {
count: 0,
modularity: None,
communities: vec![],
}),
};
let html = render_result(&result).0;
assert!(html.contains("NO COMMUNITIES FOUND"));
}
#[test]
fn test_result_rendering_structure_empty() {
let result = AlgorithmResult {
algorithm: "kcore".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Structure(StructureData {
summary: HashMap::new(),
items: vec![],
}),
};
let html = render_result(&result).0;
assert!(html.contains("NO ITEMS"));
}
#[test]
fn test_algorithm_card_community_category() {
let algo = ALGORITHMS
.iter()
.find(|a| a.category == AlgorithmCategory::Community)
.unwrap();
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains("text-neutral-300"));
assert!(html.contains("COMMUNITY"));
}
#[test]
fn test_algorithm_card_pathfinding_category() {
let algo = ALGORITHMS
.iter()
.find(|a| a.category == AlgorithmCategory::Pathfinding)
.unwrap();
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains("text-neutral-400"));
assert!(html.contains("PATHFINDING"));
}
#[test]
fn test_algorithm_card_structure_category() {
let algo = ALGORITHMS
.iter()
.find(|a| a.category == AlgorithmCategory::Structure)
.unwrap();
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains("text-neutral-400"));
assert!(html.contains("STRUCTURE"));
}
#[test]
fn test_algorithm_card_similarity_category() {
let algo = ALGORITHMS
.iter()
.find(|a| a.category == AlgorithmCategory::Similarity)
.unwrap();
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains("text-neutral-300"));
assert!(html.contains("SIMILARITY"));
}
#[test]
fn test_algorithms_have_unique_ids() {
let mut ids: Vec<_> = ALGORITHMS.iter().map(|a| a.id).collect();
ids.sort();
let original_len = ids.len();
ids.dedup();
assert_eq!(ids.len(), original_len, "Algorithm IDs must be unique");
}
#[test]
fn test_all_algorithms_have_descriptions() {
for algo in ALGORITHMS {
assert!(
!algo.description.is_empty(),
"Algorithm {} has empty description",
algo.id
);
assert!(
!algo.name.is_empty(),
"Algorithm {} has empty name",
algo.id
);
}
}
#[test]
fn test_result_rendering_communities_with_many() {
let communities: Vec<_> = (0..25).map(|i| (i as u64, i * 10)).collect();
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Communities(CommunityData {
count: 25,
modularity: Some(0.5),
communities,
}),
};
let html = render_result(&result).0;
assert!(html.contains("Showing 20 of 25 communities"));
}
#[test]
fn test_execute_params_deserialization_all_fields() {
let json = r#"{
"algorithm": "variable_paths",
"from": "1",
"to": "10",
"min_hops": 1,
"max_hops": 5,
"max_paths": 100,
"direction": "both",
"top_k": 25,
"max_iterations": 200,
"tolerance": 0.0001,
"damping": 0.9,
"resolution": 2.0,
"max_passes": 15,
"min_k": 3,
"node_a": "100",
"node_b": "200",
"metric": "cosine",
"weight_property": "distance"
}"#;
let params: ExecuteParams = serde_json::from_str(json).unwrap();
assert_eq!(params.algorithm, "variable_paths");
assert_eq!(params.from, Some("1".to_string()));
assert_eq!(params.to, Some("10".to_string()));
assert_eq!(params.min_hops, Some(1));
assert_eq!(params.max_hops, Some(5));
assert_eq!(params.max_paths, Some(100));
assert_eq!(params.direction, Some("both".to_string()));
assert_eq!(params.top_k, Some(25));
assert_eq!(params.max_iterations, Some(200));
assert_eq!(params.tolerance, Some(0.0001));
assert_eq!(params.damping, Some(0.9));
assert_eq!(params.resolution, Some(2.0));
assert_eq!(params.max_passes, Some(15));
assert_eq!(params.min_k, Some(3));
assert_eq!(params.node_a, Some("100".to_string()));
assert_eq!(params.node_b, Some("200".to_string()));
assert_eq!(params.metric, Some("cosine".to_string()));
assert_eq!(params.weight_property, Some("distance".to_string()));
}
#[test]
fn test_result_data_serialization_all_variants() {
let error_data = ResultData::Error("test error".to_string());
let json = serde_json::to_string(&error_data).unwrap();
assert!(json.contains("test error"));
let empty_data = ResultData::Empty;
let json = serde_json::to_string(&empty_data).unwrap();
assert!(json.contains("Empty") || json.contains("null"));
let scores_data = ResultData::Scores(vec![(1, 0.5), (2, 0.3)]);
let json = serde_json::to_string(&scores_data).unwrap();
assert!(json.contains("0.5"));
let similarity_data = ResultData::Similarity(0.85);
let json = serde_json::to_string(&similarity_data).unwrap();
assert!(json.contains("0.85"));
}
#[test]
fn test_result_rendering_scores_with_many_results() {
let scores: Vec<(u64, f64)> = (0..50).map(|i| (i, 1.0 / (i as f64 + 1.0))).collect();
let result = AlgorithmResult {
algorithm: "pagerank".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Scores(scores),
};
let html = render_result(&result).0;
assert!(html.contains("NODE SCORES"));
}
#[test]
fn test_result_rendering_communities_without_modularity() {
let result = AlgorithmResult {
algorithm: "label_propagation".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Communities(CommunityData {
count: 5,
modularity: None,
communities: vec![(1, 100), (2, 50)],
}),
};
let html = render_result(&result).0;
assert!(html.contains("COMMUNITY DETECTION"));
assert!(html.contains("COMMUNITIES:"));
}
#[test]
fn test_result_rendering_path_without_weight() {
let result = AlgorithmResult {
algorithm: "bfs".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Path(PathData {
nodes: vec![1, 2, 3],
weight: None,
found: true,
}),
};
let html = render_result(&result).0;
assert!(html.contains("PATH RESULT"));
assert!(html.contains("3 nodes"));
}
#[test]
fn test_result_rendering_structure_with_many_items() {
let items: Vec<(String, String)> = (0..100)
.map(|i| (format!("Item {}", i), format!("Value {}", i)))
.collect();
let result = AlgorithmResult {
algorithm: "scc".to_string(),
status: ResultStatus::Success,
elapsed_ms: 200,
data: ResultData::Structure(StructureData {
summary: HashMap::from([("count".to_string(), "100".to_string())]),
items,
}),
};
let html = render_result(&result).0;
assert!(html.contains("STRUCTURE ANALYSIS"));
}
#[test]
fn test_algorithm_category_deserialization() {
let json = "\"centrality\"";
let category: AlgorithmCategory = serde_json::from_str(json).unwrap();
assert_eq!(category, AlgorithmCategory::Centrality);
let json = "\"pathfinding\"";
let category: AlgorithmCategory = serde_json::from_str(json).unwrap();
assert_eq!(category, AlgorithmCategory::Pathfinding);
let json = "\"structure\"";
let category: AlgorithmCategory = serde_json::from_str(json).unwrap();
assert_eq!(category, AlgorithmCategory::Structure);
let json = "\"similarity\"";
let category: AlgorithmCategory = serde_json::from_str(json).unwrap();
assert_eq!(category, AlgorithmCategory::Similarity);
}
#[test]
fn test_result_status_serialization_roundtrip() {
let success = serde_json::to_string(&ResultStatus::Success).unwrap();
assert!(success.contains("success"));
let error = serde_json::to_string(&ResultStatus::Error).unwrap();
assert!(error.contains("error"));
let no_data = serde_json::to_string(&ResultStatus::NoData).unwrap();
assert!(no_data.contains("no_data"));
}
#[test]
fn test_community_data_serialization_roundtrip() {
let data = CommunityData {
count: 5,
modularity: Some(0.7),
communities: vec![(1, 100), (2, 50)],
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"count\":5"));
assert!(json.contains("0.7"));
assert!(json.contains("[1,100]"));
}
#[test]
fn test_path_data_serialization_roundtrip() {
let data = PathData {
nodes: vec![1, 2, 3],
weight: Some(5.5),
found: true,
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("[1,2,3]"));
assert!(json.contains("5.5"));
assert!(json.contains("true"));
}
#[test]
fn test_structure_data_serialization_roundtrip() {
let data = StructureData {
summary: HashMap::from([("key".to_string(), "value".to_string())]),
items: vec![("label".to_string(), "desc".to_string())],
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("key"));
assert!(json.contains("value"));
assert!(json.contains("label"));
}
#[test]
fn test_algorithm_result_serialization_roundtrip() {
let result = AlgorithmResult {
algorithm: "pagerank".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Scores(vec![(1, 0.5), (2, 0.3)]),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("pagerank"));
assert!(json.contains("success"));
assert!(json.contains("100"));
}
#[test]
fn test_render_algorithm_card_all_categories() {
for algo in ALGORITHMS {
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains(algo.name));
assert!(html.contains("CONFIGURE"));
assert!(html.contains(algo.category.label()));
}
}
#[test]
fn test_algorithms_have_valid_categories() {
for algo in ALGORITHMS {
let label = algo.category.label();
assert!(!label.is_empty());
}
}
#[test]
fn test_algorithms_params_are_valid() {
for algo in ALGORITHMS {
for param in algo.params {
assert!(!param.name.is_empty());
assert!(!param.label.is_empty());
assert!(!param.description.is_empty());
}
}
}
#[test]
fn test_dashboard_params_category_filter() {
let params: DashboardParams = serde_json::from_str(r#"{"category": "community"}"#).unwrap();
assert_eq!(params.category.as_deref(), Some("community"));
let params: DashboardParams =
serde_json::from_str(r#"{"category": "pathfinding"}"#).unwrap();
assert_eq!(params.category.as_deref(), Some("pathfinding"));
}
#[test]
fn test_result_rendering_structure_empty_summary() {
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Structure(StructureData {
summary: HashMap::new(),
items: vec![("Item 1".to_string(), "Desc 1".to_string())],
}),
};
let html = render_result(&result).0;
assert!(html.contains("STRUCTURE ANALYSIS"));
}
#[test]
fn test_result_rendering_similarity_boundary_values() {
let result = AlgorithmResult {
algorithm: "similarity".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Similarity(0.0),
};
let html = render_result(&result).0;
assert!(html.contains("SIMILARITY SCORE"));
assert!(html.contains("0.000000"));
let result = AlgorithmResult {
algorithm: "similarity".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Similarity(1.0),
};
let html = render_result(&result).0;
assert!(html.contains("1.000000"));
}
#[test]
fn test_algorithm_category_equality() {
assert_eq!(AlgorithmCategory::Centrality, AlgorithmCategory::Centrality);
assert_ne!(AlgorithmCategory::Centrality, AlgorithmCategory::Community);
}
#[test]
fn test_algorithm_category_copy() {
let cat = AlgorithmCategory::Pathfinding;
let cat_copy = cat;
assert_eq!(cat, cat_copy);
}
#[test]
fn test_algorithm_category_debug() {
let cat = AlgorithmCategory::Structure;
let debug_str = format!("{:?}", cat);
assert!(debug_str.contains("Structure"));
}
#[test]
fn test_result_status_debug() {
let status = ResultStatus::Success;
let debug_str = format!("{:?}", status);
assert!(debug_str.contains("Success"));
}
#[test]
fn test_result_data_debug_scores() {
let data = ResultData::Scores(vec![(1, 0.5)]);
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Scores"));
}
#[test]
fn test_result_data_debug_communities() {
let data = ResultData::Communities(CommunityData {
count: 1,
modularity: None,
communities: vec![],
});
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Communities"));
}
#[test]
fn test_result_data_debug_path() {
let data = ResultData::Path(PathData {
nodes: vec![],
weight: None,
found: false,
});
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Path"));
}
#[test]
fn test_result_data_debug_structure() {
let data = ResultData::Structure(StructureData {
summary: HashMap::new(),
items: vec![],
});
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Structure"));
}
#[test]
fn test_result_data_debug_similarity() {
let data = ResultData::Similarity(0.5);
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Similarity"));
}
#[test]
fn test_result_data_debug_error() {
let data = ResultData::Error("test".to_string());
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Error"));
}
#[test]
fn test_result_data_debug_empty() {
let data = ResultData::Empty;
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("Empty"));
}
#[test]
fn test_community_data_debug() {
let data = CommunityData {
count: 5,
modularity: Some(0.5),
communities: vec![(1, 10)],
};
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("count"));
}
#[test]
fn test_path_data_debug() {
let data = PathData {
nodes: vec![1, 2, 3],
weight: Some(5.0),
found: true,
};
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("nodes"));
}
#[test]
fn test_structure_data_debug() {
let data = StructureData {
summary: HashMap::new(),
items: vec![],
};
let debug_str = format!("{:?}", data);
assert!(debug_str.contains("summary"));
}
#[test]
fn test_algorithm_result_debug() {
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Success,
elapsed_ms: 0,
data: ResultData::Empty,
};
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("algorithm"));
}
#[test]
fn test_parse_direction_empty() {
assert!(matches!(parse_direction(""), Direction::Both));
}
#[test]
fn test_parse_direction_whitespace() {
assert!(matches!(parse_direction(" "), Direction::Both));
}
#[test]
fn test_parse_direction_mixed_case() {
assert!(matches!(parse_direction("OutGoing"), Direction::Outgoing));
assert!(matches!(parse_direction("InComing"), Direction::Incoming));
}
#[test]
fn test_parse_similarity_metric_empty() {
assert!(matches!(
parse_similarity_metric(""),
SimilarityMetric::Jaccard
));
}
#[test]
fn test_parse_similarity_metric_whitespace() {
assert!(matches!(
parse_similarity_metric(" "),
SimilarityMetric::Jaccard
));
}
#[test]
fn test_parse_similarity_metric_uppercase() {
assert!(matches!(
parse_similarity_metric("JACCARD"),
SimilarityMetric::Jaccard
));
assert!(matches!(
parse_similarity_metric("COSINE"),
SimilarityMetric::Cosine
));
}
#[test]
fn test_result_rendering_scores_single_item() {
let result = AlgorithmResult {
algorithm: "pagerank".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Scores(vec![(1, 0.99)]),
};
let html = render_result(&result).0;
assert!(html.contains("NODE SCORES"));
assert!(html.contains("0.99"));
}
#[test]
fn test_result_rendering_communities_single() {
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Communities(CommunityData {
count: 1,
modularity: Some(0.1),
communities: vec![(1, 5)],
}),
};
let html = render_result(&result).0;
assert!(html.contains("COMMUNITY DETECTION"));
}
#[test]
fn test_result_rendering_path_single_node() {
let result = AlgorithmResult {
algorithm: "bfs".to_string(),
status: ResultStatus::Success,
elapsed_ms: 5,
data: ResultData::Path(PathData {
nodes: vec![1],
weight: None,
found: true,
}),
};
let html = render_result(&result).0;
assert!(html.contains("PATH RESULT"));
assert!(html.contains("1 node"));
}
#[test]
fn test_result_rendering_structure_summary_only() {
let result = AlgorithmResult {
algorithm: "kcore".to_string(),
status: ResultStatus::Success,
elapsed_ms: 20,
data: ResultData::Structure(StructureData {
summary: HashMap::from([
("total_nodes".to_string(), "100".to_string()),
("max_degree".to_string(), "50".to_string()),
]),
items: vec![],
}),
};
let html = render_result(&result).0;
assert!(html.contains("STRUCTURE ANALYSIS"));
}
#[test]
fn test_execute_params_betweenness() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "betweenness",
"top_k": 15,
"direction": "outgoing"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "betweenness");
assert_eq!(params.top_k, Some(15));
assert_eq!(params.direction, Some("outgoing".to_string()));
}
#[test]
fn test_execute_params_closeness() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "closeness",
"top_k": 10,
"direction": "incoming"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "closeness");
assert_eq!(params.direction, Some("incoming".to_string()));
}
#[test]
fn test_execute_params_eigenvector() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "eigenvector",
"top_k": 20,
"max_iterations": 150,
"tolerance": 0.00001
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "eigenvector");
assert_eq!(params.max_iterations, Some(150));
}
#[test]
fn test_execute_params_label_propagation() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "label_propagation",
"max_iterations": 100
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "label_propagation");
}
#[test]
fn test_execute_params_scc() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "scc"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "scc");
}
#[test]
fn test_execute_params_biconnected() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "biconnected"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "biconnected");
}
#[test]
fn test_execute_params_triangles() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "triangles",
"top_k": 25
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "triangles");
assert_eq!(params.top_k, Some(25));
}
#[test]
fn test_execute_params_mst() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "mst",
"weight_property": "weight"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "mst");
assert_eq!(params.weight_property, Some("weight".to_string()));
}
#[test]
fn test_render_algorithm_card_centrality_category() {
let algo = &ALGORITHMS[0]; let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains("text-white"));
assert!(html.contains(algo.id));
}
#[test]
fn test_render_algorithm_card_with_many_params() {
let algo = ALGORITHMS.iter().find(|a| a.params.len() >= 3);
if let Some(algo) = algo {
let card = render_algorithm_card(algo);
let html = card.0;
assert!(html.contains(algo.name));
}
}
#[test]
fn test_algorithms_count_per_category() {
let centrality_count = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Centrality)
.count();
let community_count = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Community)
.count();
let pathfinding_count = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Pathfinding)
.count();
let structure_count = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Structure)
.count();
let similarity_count = ALGORITHMS
.iter()
.filter(|a| a.category == AlgorithmCategory::Similarity)
.count();
let total = centrality_count
+ community_count
+ pathfinding_count
+ structure_count
+ similarity_count;
assert_eq!(total, ALGORITHMS.len());
}
#[test]
fn test_dashboard_params_debug() {
let params = DashboardParams { category: None };
let debug_str = format!("{:?}", params);
assert!(debug_str.contains("DashboardParams"));
}
#[test]
fn test_dashboard_params_with_structure() {
let params: DashboardParams = serde_json::from_str(r#"{"category": "structure"}"#).unwrap();
assert_eq!(params.category.as_deref(), Some("structure"));
}
#[test]
fn test_dashboard_params_with_similarity() {
let params: DashboardParams =
serde_json::from_str(r#"{"category": "similarity"}"#).unwrap();
assert_eq!(params.category.as_deref(), Some("similarity"));
}
#[test]
fn test_execute_params_all_pathfinding_fields() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "dijkstra",
"from": "source",
"to": "target",
"direction": "both",
"max_depth": 100,
"weight_property": "cost"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "dijkstra");
assert_eq!(params.from, Some("source".to_string()));
assert_eq!(params.to, Some("target".to_string()));
assert_eq!(params.direction, Some("both".to_string()));
assert_eq!(params.max_depth, Some(100));
assert_eq!(params.weight_property, Some("cost".to_string()));
}
#[test]
fn test_execute_params_bfs() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "bfs",
"from": "start",
"to": "end",
"max_depth": 10
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "bfs");
assert_eq!(params.max_depth, Some(10));
}
#[test]
fn test_execute_params_dijkstra() {
let params: ExecuteParams = serde_json::from_str(
r#"{
"algorithm": "dijkstra",
"from": "1",
"to": "100",
"weight_property": "distance"
}"#,
)
.unwrap();
assert_eq!(params.algorithm, "dijkstra");
assert_eq!(params.weight_property, Some("distance".to_string()));
}
#[test]
fn test_result_rendering_all_status_types() {
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Success,
elapsed_ms: 10,
data: ResultData::Scores(vec![(1, 0.5)]),
};
let html = render_result(&result).0;
assert!(!html.is_empty());
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Error,
elapsed_ms: 0,
data: ResultData::Error("failed".to_string()),
};
let html = render_result(&result).0;
assert!(html.contains("ERROR"));
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::NoData,
elapsed_ms: 0,
data: ResultData::Empty,
};
let html = render_result(&result).0;
assert!(html.contains("NO DATA"));
}
#[test]
fn test_result_rendering_large_path() {
let nodes: Vec<u64> = (0..100).collect();
let result = AlgorithmResult {
algorithm: "astar".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Path(PathData {
nodes,
weight: Some(500.0),
found: true,
}),
};
let html = render_result(&result).0;
assert!(html.contains("PATH RESULT"));
assert!(html.contains("100 nodes"));
}
#[test]
fn test_result_rendering_negative_similarity() {
let result = AlgorithmResult {
algorithm: "similarity".to_string(),
status: ResultStatus::Success,
elapsed_ms: 5,
data: ResultData::Similarity(-0.5),
};
let html = render_result(&result).0;
assert!(html.contains("-0.500000"));
}
#[test]
fn test_parse_direction_case_insensitive() {
assert!(matches!(parse_direction("OUTGOING"), Direction::Outgoing));
assert!(matches!(parse_direction("Incoming"), Direction::Incoming));
assert!(matches!(parse_direction("Both"), Direction::Both));
assert!(matches!(parse_direction("OUT"), Direction::Outgoing));
assert!(matches!(parse_direction("IN"), Direction::Incoming));
}
#[test]
fn test_parse_similarity_metric_case_insensitive() {
assert!(matches!(
parse_similarity_metric("JACCARD"),
SimilarityMetric::Jaccard
));
assert!(matches!(
parse_similarity_metric("Cosine"),
SimilarityMetric::Cosine
));
assert!(matches!(
parse_similarity_metric("ADAMIC_ADAR"),
SimilarityMetric::AdamicAdar
));
}
#[test]
fn test_algorithms_all_have_params() {
for algo in ALGORITHMS {
assert!(algo.params.len() <= 10, "Too many params for {}", algo.id);
}
}
#[test]
fn test_algorithm_def_fields_non_empty() {
for algo in ALGORITHMS {
assert!(!algo.id.is_empty());
assert!(!algo.name.is_empty());
assert!(!algo.description.is_empty());
}
}
#[test]
fn test_algorithm_param_def_fields() {
for algo in ALGORITHMS {
for param in algo.params {
assert!(!param.name.is_empty());
assert!(!param.label.is_empty());
assert!(!param.description.is_empty());
}
}
}
#[test]
fn test_structure_data_with_many_summary_keys() {
let mut summary = HashMap::new();
for i in 0..10 {
summary.insert(format!("key_{}", i), format!("value_{}", i));
}
let result = AlgorithmResult {
algorithm: "test".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Structure(StructureData {
summary,
items: vec![],
}),
};
let html = render_result(&result).0;
assert!(html.contains("STRUCTURE ANALYSIS"));
}
#[test]
fn test_community_data_high_modularity() {
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::Success,
elapsed_ms: 100,
data: ResultData::Communities(CommunityData {
count: 5,
modularity: Some(0.99),
communities: vec![(1, 100), (2, 50), (3, 30), (4, 15), (5, 5)],
}),
};
let html = render_result(&result).0;
assert!(html.contains("0.99"));
}
#[test]
fn test_community_data_negative_modularity() {
let result = AlgorithmResult {
algorithm: "louvain".to_string(),
status: ResultStatus::Success,
elapsed_ms: 50,
data: ResultData::Communities(CommunityData {
count: 1,
modularity: Some(-0.1),
communities: vec![(1, 10)],
}),
};
let html = render_result(&result).0;
assert!(html.contains("-0.1"));
}
#[test]
fn test_render_algorithm_card_all_param_types() {
for algo in ALGORITHMS {
let card = render_algorithm_card(algo);
let html = card.0;
assert!(!html.is_empty());
assert!(html.contains(algo.name));
}
}
fn create_test_context() -> AdminContext {
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
)
}
fn create_test_context_with_graph() -> AdminContext {
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
let graph = GraphEngine::new();
let n1 = graph.create_node("Person", HashMap::new()).unwrap();
let n2 = graph.create_node("Person", HashMap::new()).unwrap();
let n3 = graph.create_node("Person", HashMap::new()).unwrap();
graph
.create_edge(n1, n2, "KNOWS", HashMap::new(), true)
.ok();
graph
.create_edge(n2, n3, "KNOWS", HashMap::new(), true)
.ok();
graph
.create_edge(n1, n3, "KNOWS", HashMap::new(), true)
.ok();
AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(graph),
)
}
#[test]
fn test_execute_algorithm_pagerank_empty_graph() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "pagerank".to_string(),
damping: Some(0.85),
tolerance: Some(1e-6),
max_iterations: Some(100),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "pagerank");
assert!(matches!(
result.status,
ResultStatus::NoData | ResultStatus::Success
));
}
#[test]
fn test_execute_algorithm_pagerank_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "pagerank".to_string(),
damping: Some(0.85),
tolerance: Some(1e-6),
max_iterations: Some(100),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "pagerank");
assert!(matches!(
result.status,
ResultStatus::Success | ResultStatus::NoData
));
}
#[test]
fn test_execute_algorithm_betweenness_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "betweenness".to_string(),
direction: Some("both".to_string()),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "betweenness");
}
#[test]
fn test_execute_algorithm_betweenness_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "betweenness".to_string(),
direction: Some("outgoing".to_string()),
top_k: Some(5),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "betweenness");
}
#[test]
fn test_execute_algorithm_closeness_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "closeness".to_string(),
direction: Some("incoming".to_string()),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "closeness");
}
#[test]
fn test_execute_algorithm_closeness_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "closeness".to_string(),
direction: Some("both".to_string()),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "closeness");
}
#[test]
fn test_execute_algorithm_eigenvector_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "eigenvector".to_string(),
max_iterations: Some(50),
tolerance: Some(1e-5),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "eigenvector");
}
#[test]
fn test_execute_algorithm_eigenvector_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "eigenvector".to_string(),
max_iterations: Some(100),
tolerance: Some(1e-6),
top_k: Some(5),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "eigenvector");
}
#[test]
fn test_execute_algorithm_louvain_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "louvain".to_string(),
resolution: Some(1.0),
max_passes: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "louvain");
}
#[test]
fn test_execute_algorithm_louvain_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "louvain".to_string(),
resolution: Some(0.5),
max_passes: Some(5),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "louvain");
}
#[test]
fn test_execute_algorithm_label_propagation_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "label_propagation".to_string(),
max_iterations: Some(50),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "label_propagation");
}
#[test]
fn test_execute_algorithm_label_propagation_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "label_propagation".to_string(),
max_iterations: Some(100),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "label_propagation");
}
#[test]
fn test_execute_algorithm_connected_components_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "connected_components".to_string(),
direction: Some("both".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "connected_components");
}
#[test]
fn test_execute_algorithm_connected_components_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "connected_components".to_string(),
direction: Some("outgoing".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "connected_components");
}
#[test]
fn test_execute_algorithm_scc_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "scc".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "scc");
}
#[test]
fn test_execute_algorithm_scc_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "scc".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "scc");
}
#[test]
fn test_execute_algorithm_biconnected_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "biconnected".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "biconnected");
}
#[test]
fn test_execute_algorithm_biconnected_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "biconnected".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "biconnected");
}
#[test]
fn test_execute_algorithm_kcore_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "kcore".to_string(),
min_k: Some(2),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "kcore");
}
#[test]
fn test_execute_algorithm_kcore_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "kcore".to_string(),
min_k: Some(1),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "kcore");
}
#[test]
fn test_execute_algorithm_triangles_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "triangles".to_string(),
top_k: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "triangles");
}
#[test]
fn test_execute_algorithm_triangles_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "triangles".to_string(),
top_k: Some(5),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "triangles");
}
#[test]
fn test_execute_algorithm_mst_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "mst".to_string(),
weight_property: Some("weight".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "mst");
}
#[test]
fn test_execute_algorithm_mst_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "mst".to_string(),
weight_property: None,
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "mst");
}
#[test]
fn test_execute_algorithm_bfs_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "bfs".to_string(),
from: Some("1".to_string()),
to: Some("2".to_string()),
max_depth: Some(10),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "bfs");
}
#[test]
fn test_execute_algorithm_bfs_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "bfs".to_string(),
from: Some("1".to_string()),
to: Some("3".to_string()),
max_depth: Some(5),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "bfs");
}
#[test]
fn test_execute_algorithm_dijkstra_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "dijkstra".to_string(),
from: Some("1".to_string()),
to: Some("2".to_string()),
weight_property: Some("cost".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "dijkstra");
}
#[test]
fn test_execute_algorithm_dijkstra_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "dijkstra".to_string(),
from: Some("1".to_string()),
to: Some("3".to_string()),
weight_property: None,
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "dijkstra");
}
#[test]
fn test_execute_algorithm_astar_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "astar".to_string(),
from: Some("1".to_string()),
to: Some("2".to_string()),
weight_property: Some("weight".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "astar");
}
#[test]
fn test_execute_algorithm_astar_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "astar".to_string(),
from: Some("1".to_string()),
to: Some("3".to_string()),
weight_property: None,
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "astar");
}
#[test]
fn test_execute_algorithm_similarity_empty() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "similarity".to_string(),
node_a: Some("1".to_string()),
node_b: Some("2".to_string()),
metric: Some("jaccard".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "similarity");
}
#[test]
fn test_execute_algorithm_similarity_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "similarity".to_string(),
node_a: Some("1".to_string()),
node_b: Some("2".to_string()),
metric: Some("cosine".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "similarity");
}
#[test]
fn test_execute_algorithm_similarity_adamic_adar() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "similarity".to_string(),
node_a: Some("1".to_string()),
node_b: Some("3".to_string()),
metric: Some("adamic_adar".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "similarity");
}
#[test]
fn test_execute_algorithm_unknown() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "unknown_algorithm".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "unknown_algorithm");
assert!(matches!(result.status, ResultStatus::Error));
}
#[test]
fn test_execute_algorithm_missing_from_param() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "bfs".to_string(),
from: None,
to: Some("2".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "bfs");
}
#[test]
fn test_execute_algorithm_missing_to_param() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "dijkstra".to_string(),
from: Some("1".to_string()),
to: None,
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "dijkstra");
}
#[tokio::test]
async fn test_dashboard_handler_with_graph() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Query(DashboardParams { category: None });
let result = dashboard(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("ALGORITHM DASHBOARD"));
assert!(html.contains("NODES"));
assert!(html.contains("EDGES"));
}
#[tokio::test]
async fn test_dashboard_handler_empty_graph() {
let ctx = Arc::new(create_test_context());
let params = Query(DashboardParams { category: None });
let result = dashboard(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("ALGORITHM DASHBOARD"));
}
#[tokio::test]
async fn test_dashboard_category_filter() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Query(DashboardParams {
category: Some("centrality".to_string()),
});
let result = dashboard(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("ALGORITHM DASHBOARD"));
}
#[tokio::test]
async fn test_execute_form_handler() {
let ctx = Arc::new(create_test_context());
let params = Query(ExecuteParams {
algorithm: "pagerank".to_string(),
..Default::default()
});
let result = execute_form(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTE ALGORITHM"));
}
#[tokio::test]
async fn test_execute_form_unknown_algorithm() {
let ctx = Arc::new(create_test_context());
let params = Query(ExecuteParams {
algorithm: "nonexistent".to_string(),
..Default::default()
});
let result = execute_form(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("ALGORITHM NOT FOUND") || html.contains("EXECUTE ALGORITHM"));
}
#[tokio::test]
async fn test_execute_submit_pagerank() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Form(ExecuteParams {
algorithm: "pagerank".to_string(),
top_k: Some(10),
..Default::default()
});
let result = execute_submit(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTION STATS"));
assert!(html.contains("pagerank"));
}
#[tokio::test]
async fn test_execute_submit_components() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Form(ExecuteParams {
algorithm: "components".to_string(),
..Default::default()
});
let result = execute_submit(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTION STATS"));
}
#[tokio::test]
async fn test_execute_submit_bfs_with_from() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Form(ExecuteParams {
algorithm: "bfs".to_string(),
from: Some("1".to_string()),
max_depth: Some(3),
..Default::default()
});
let result = execute_submit(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTION STATS"));
}
#[tokio::test]
async fn test_execute_submit_degree() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Form(ExecuteParams {
algorithm: "degree".to_string(),
top_k: Some(5),
..Default::default()
});
let result = execute_submit(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTION STATS"));
}
#[tokio::test]
async fn test_execute_submit_louvain() {
let ctx = Arc::new(create_test_context_with_graph());
let params = Form(ExecuteParams {
algorithm: "louvain".to_string(),
..Default::default()
});
let result = execute_submit(State(ctx), params).await;
let html = result.into_string();
assert!(html.contains("EXECUTION STATS"));
}
#[test]
fn test_execute_algorithm_variable_paths_empty_graph() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "variable_paths".to_string(),
from: Some("1".to_string()),
to: Some("2".to_string()),
min_hops: Some(1),
max_hops: Some(3),
max_paths: Some(50),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "variable_paths");
}
#[test]
fn test_execute_algorithm_variable_paths_with_graph() {
let ctx = create_test_context_with_graph();
let params = ExecuteParams {
algorithm: "variable_paths".to_string(),
from: Some("1".to_string()),
to: Some("3".to_string()),
min_hops: Some(1),
max_hops: Some(3),
max_paths: Some(100),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "variable_paths");
assert!(!matches!(result.status, ResultStatus::Error));
}
#[test]
fn test_execute_algorithm_variable_paths_invalid_ids() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "variable_paths".to_string(),
from: Some("not_a_number".to_string()),
to: Some("2".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert!(matches!(result.status, ResultStatus::Error));
assert!(matches!(result.data, ResultData::Error(ref s) if s.contains("Invalid")));
}
#[test]
fn test_execute_algorithm_astar_invalid_ids() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "astar".to_string(),
from: Some("abc".to_string()),
to: Some("2".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert!(matches!(result.status, ResultStatus::Error));
assert!(matches!(result.data, ResultData::Error(ref s) if s.contains("Invalid")));
}
#[test]
fn test_execute_algorithm_dijkstra_path_not_found() {
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
let graph = GraphEngine::new();
let n1 = graph.create_node("A", HashMap::new()).unwrap();
let n2 = graph.create_node("B", HashMap::new()).unwrap();
let ctx = AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(graph),
);
let params = ExecuteParams {
algorithm: "dijkstra".to_string(),
from: Some(n1.to_string()),
to: Some(n2.to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "dijkstra");
assert!(matches!(
result.status,
ResultStatus::NoData | ResultStatus::Error
));
}
#[test]
fn test_execute_algorithm_dijkstra_invalid_ids() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "dijkstra".to_string(),
from: Some("xyz".to_string()),
to: Some("abc".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert!(matches!(result.status, ResultStatus::Error));
}
#[test]
fn test_execute_algorithm_similarity_invalid_ids() {
let ctx = create_test_context();
let params = ExecuteParams {
algorithm: "similarity".to_string(),
node_a: Some("xyz".to_string()),
node_b: Some("2".to_string()),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert!(matches!(result.status, ResultStatus::Error));
assert!(matches!(result.data, ResultData::Error(ref s) if s.contains("Invalid")));
}
#[test]
fn test_execute_algorithm_scc_large_component() {
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
let graph = GraphEngine::new();
let mut nodes = Vec::new();
for _ in 0..6 {
nodes.push(graph.create_node("Node", HashMap::new()).unwrap());
}
for i in 0..6 {
graph
.create_edge(nodes[i], nodes[(i + 1) % 6], "NEXT", HashMap::new(), true)
.ok();
}
let ctx = AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(graph),
);
let params = ExecuteParams {
algorithm: "scc".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "scc");
assert!(!matches!(result.status, ResultStatus::Error));
}
#[test]
fn test_execute_algorithm_biconnected_with_articulation() {
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
let graph = GraphEngine::new();
let a = graph.create_node("A", HashMap::new()).unwrap();
let b = graph.create_node("B", HashMap::new()).unwrap();
let c = graph.create_node("C", HashMap::new()).unwrap();
graph.create_edge(a, b, "LINK", HashMap::new(), false).ok();
graph.create_edge(b, a, "LINK", HashMap::new(), false).ok();
graph.create_edge(b, c, "LINK", HashMap::new(), false).ok();
graph.create_edge(c, b, "LINK", HashMap::new(), false).ok();
let ctx = AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(graph),
);
let params = ExecuteParams {
algorithm: "biconnected".to_string(),
..Default::default()
};
let result = execute_algorithm(&ctx, ¶ms);
assert_eq!(result.algorithm, "biconnected");
}
}