use std::path::PathBuf;
use ripbi_core::graph::{DependencyGraph, Provenance, StructuralEdge};
use ripbi_core::identity::{NameKey, ObjectId};
use ripbi_core::ingest::{report, semantic_model};
use ripbi_core::model::TabularDatabase;
use ripbi_core::report::ReportModel;
fn fixture(groups: &[&str]) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures");
for group in groups {
path.push(group);
}
path
}
fn golden_pair() -> (TabularDatabase, ReportModel) {
let db = semantic_model(&fixture(&["tmdl", "golden", "Mini.SemanticModel"]))
.expect("golden model parses")
.value;
let report = report(&fixture(&["pbir", "golden", "Mini.Report"]))
.expect("golden report parses")
.value;
(db, report)
}
fn table_id(name: &str) -> ObjectId {
ObjectId::Table {
table: NameKey::new(name),
}
}
fn column_id(table: &str, column: &str) -> ObjectId {
ObjectId::Column {
table: NameKey::new(table),
column: NameKey::new(column),
}
}
fn measure_id(table: &str, measure: &str) -> ObjectId {
ObjectId::Measure {
table: NameKey::new(table),
measure: NameKey::new(measure),
}
}
fn hierarchy_id(table: &str, hierarchy: &str) -> ObjectId {
ObjectId::Hierarchy {
table: NameKey::new(table),
hierarchy: NameKey::new(hierarchy),
}
}
fn partition_id(table: &str, partition: &str) -> ObjectId {
ObjectId::Partition {
table: NameKey::new(table),
partition: NameKey::new(partition),
}
}
fn calculation_item_id(table: &str, item: &str) -> ObjectId {
ObjectId::CalculationItem {
table: NameKey::new(table),
item: NameKey::new(item),
}
}
fn expression_id(name: &str) -> ObjectId {
ObjectId::Expression {
name: NameKey::new(name),
}
}
fn report_measure_id(name: &str) -> ObjectId {
ObjectId::ReportMeasure {
measure: NameKey::new(name),
}
}
fn find<'a>(unused: &'a [ripbi_core::UnusedObject], id: &ObjectId) -> &'a ripbi_core::UnusedObject {
unused
.iter()
.find(|finding| &finding.id == id)
.unwrap_or_else(|| panic!("{id} expected in the unused set"))
}
fn not_unused(unused: &[ripbi_core::UnusedObject], id: &ObjectId) {
assert!(
!unused.iter().any(|finding| &finding.id == id),
"{id} must be live"
);
}
#[test]
fn golden_unused_set_is_exact() {
let (db, report) = golden_pair();
let graph = DependencyGraph::build(&db, &[&report]);
let unused = graph.unused_objects();
let mut expected = vec![
column_id("Sales", "Margin %"),
hierarchy_id("Sales", "Fiscal"),
column_id("Sales", "YearNum"),
column_id("Sales", "Month Name"),
column_id("Sales Order", "It's quoted"),
column_id("Sales", "DueDateKey"),
column_id("Sales Order", "DueDateKey"),
column_id("Sales Order", "SalesOrder"),
ObjectId::Relationship {
from_table: NameKey::new("Sales"),
from_column: NameKey::new("DueDateKey"),
to_table: NameKey::new("Sales Order"),
to_column: NameKey::new("DueDateKey"),
},
ObjectId::Relationship {
from_table: NameKey::new("Sales Order"),
from_column: NameKey::new("SalesOrder"),
to_table: NameKey::new("Sales"),
to_column: NameKey::new("SalesOrderLineKey"),
},
measure_id("Sales", "Growth %"),
report_measure_id("Budget %"),
expression_id("ServerName"),
expression_id("Calendar"),
table_id("Time Intelligence"),
calculation_item_id("Time Intelligence", "Current"),
calculation_item_id("Time Intelligence", "YoY %"),
column_id("Time Intelligence", "Time Intelligence"),
partition_id("Time Intelligence", "Partition_Time Intelligence"),
];
expected.sort();
let ids: Vec<ObjectId> = unused.iter().map(|finding| finding.id.clone()).collect();
assert_eq!(ids, expected, "exact unused set for the golden pair");
}
#[test]
fn golden_relationships_and_key_columns_stay_live() {
let (db, report) = golden_pair();
let graph = DependencyGraph::build(&db, &[&report]);
let unused = graph.unused_objects();
not_unused(&unused, &table_id("Sales"));
not_unused(&unused, &table_id("Sales Order"));
not_unused(&unused, &column_id("Sales", "Sales Amount"));
not_unused(&unused, &column_id("Sales", "SalesOrderLineKey"));
not_unused(&unused, &column_id("Sales Order", "SalesOrderLineKey"));
let relationship_id = |from_table: &str, from_column: &str, to_table: &str, to_column: &str| {
ObjectId::Relationship {
from_table: NameKey::new(from_table),
from_column: NameKey::new(from_column),
to_table: NameKey::new(to_table),
to_column: NameKey::new(to_column),
}
};
not_unused(
&unused,
&relationship_id(
"Sales",
"SalesOrderLineKey",
"Sales Order",
"SalesOrderLineKey",
),
);
not_unused(
&unused,
&ObjectId::Role {
role: NameKey::new("Administrators"),
},
);
}
#[test]
fn golden_inactive_relationship_keys_are_findings() {
let (db, report) = golden_pair();
let graph = DependencyGraph::build(&db, &[&report]);
let unused = graph.unused_objects();
not_unused(&unused, &column_id("Sales", "SalesOrderLineKey"));
not_unused(&unused, &column_id("Sales Order", "SalesOrderLineKey"));
let expected: [(ObjectId, &[ObjectId]); 2] = [
(
ObjectId::Relationship {
from_table: NameKey::new("Sales"),
from_column: NameKey::new("DueDateKey"),
to_table: NameKey::new("Sales Order"),
to_column: NameKey::new("DueDateKey"),
},
&[
column_id("Sales", "DueDateKey"),
column_id("Sales Order", "DueDateKey"),
],
),
(
ObjectId::Relationship {
from_table: NameKey::new("Sales Order"),
from_column: NameKey::new("SalesOrder"),
to_table: NameKey::new("Sales"),
to_column: NameKey::new("SalesOrderLineKey"),
},
&[column_id("Sales Order", "SalesOrder")],
),
];
for (relationship_id, keys) in expected {
let relationship = find(&unused, &relationship_id);
assert!(
relationship.used_by.iter().all(|used| matches!(
&used.provenance,
Provenance::Structural {
role: StructuralEdge::InactiveRelationship
}
)),
"{relationship_id}: only its tables record it"
);
for key in keys {
let finding = find(&unused, key);
assert_eq!(
finding.used_by.len(),
1,
"{key}: the inactive relationship is the only reference"
);
assert_eq!(finding.used_by[0].id, relationship_id);
assert!(
finding.used_by[0].also_unused,
"{key}: the relationship naming it is itself unused"
);
assert!(matches!(
&finding.used_by[0].provenance,
Provenance::Structural {
role: StructuralEdge::InactiveRelationshipEndpoint
}
));
}
}
}
#[test]
fn golden_dead_chains_are_annotated() {
let (db, report) = golden_pair();
let graph = DependencyGraph::build(&db, &[&report]);
let unused = graph.unused_objects();
let year_num = find(&unused, &column_id("Sales", "YearNum"));
assert_eq!(year_num.used_by.len(), 1);
assert_eq!(year_num.used_by[0].id, hierarchy_id("Sales", "Fiscal"));
assert!(matches!(
year_num.used_by[0].provenance,
Provenance::Structural {
role: StructuralEdge::HierarchyLevel
}
));
assert!(year_num.used_by[0].also_unused);
let group = find(&unused, &table_id("Time Intelligence"));
assert_eq!(group.used_by.len(), 3);
assert!(group.used_by.iter().all(|used| used.also_unused));
assert!(
group
.used_by
.iter()
.any(|used| used.id == calculation_item_id("Time Intelligence", "Current"))
);
assert!(group.used_by.iter().all(|used| matches!(
used.provenance,
Provenance::Structural {
role: StructuralEdge::TableMember
}
)));
let group_column = find(
&unused,
&column_id("Time Intelligence", "Time Intelligence"),
);
assert_eq!(group_column.used_by.len(), 1);
assert_eq!(group_column.used_by[0].id, table_id("Time Intelligence"));
assert!(matches!(
group_column.used_by[0].provenance,
Provenance::Structural {
role: StructuralEdge::EngineManaged
}
));
for orphan in [
measure_id("Sales", "Growth %"),
report_measure_id("Budget %"),
expression_id("ServerName"),
column_id("Sales", "Margin %"),
] {
assert_eq!(
find(&unused, &orphan).used_by,
Vec::new(),
"{orphan} is an orphan"
);
}
}
#[test]
fn golden_roots_carry_binding_provenance() {
let (db, report) = golden_pair();
let graph = DependencyGraph::build(&db, &[&report]);
let sales_roots = graph.roots_of(&measure_id("Sales", "Sales"));
assert_eq!(sales_roots.len(), 2);
let Provenance::Binding(edge) = sales_roots[0] else {
panic!("a root carries binding provenance");
};
assert!(matches!(
&edge.kind,
ripbi_core::BindingSite::FieldWell { role } if role.as_str() == "Values"
));
assert_eq!(edge.page.as_ref().map(NameKey::as_str), Some("P1"));
assert_eq!(edge.visual.as_ref().map(NameKey::as_str), Some("V2"));
assert!(edge.bookmark.is_none());
assert!(!edge.mobile);
let Provenance::Binding(mobile_edge) = sales_roots[1] else {
panic!("a root carries binding provenance");
};
assert_eq!(mobile_edge.visual.as_ref().map(NameKey::as_str), Some("VM"));
assert!(mobile_edge.mobile);
assert_eq!(graph.roots_of(&table_id("Sales")).len(), 2);
assert!(graph.roots_of(&report_measure_id("Budget %")).is_empty());
}