using MeterVault.Core.Analysis.Virtual; namespace MeterVault.Core.Tests.Analysis; /// /// Nested virtual meters are evaluated dependencies first, a loop is reported with the path through it and /// withheld from evaluation, and a request expands to the physical meters it finally reads (D-27). /// public sealed class DependencyGraphTests { private static DependencyGraph Graph(params (int Meter, int[] Dependencies)[] meters) => DependencyGraph.Build(meters.ToDictionary(m => m.Meter, m => (IReadOnlyList)m.Dependencies)); [Fact] public void Nested_virtual_meters_are_ordered_dependencies_first_and_expand_to_their_physical_leaves() { // 10 = m1 + m2, 11 = m10 - m3, 12 = m11 + m10 var graph = Graph((12, [11, 10]), (11, [10, 3]), (10, [1, 2])); Assert.Equal([10, 11, 12], graph.EvaluationOrder); Assert.Equal([1, 2, 3], graph.PhysicalLeaves(12)); Assert.Equal([1, 2], graph.PhysicalLeaves(10)); Assert.Equal([7], graph.PhysicalLeaves(7)); Assert.Equal([12, 10, 1], graph.PathTo(12, 1)); // the shortest way, not 12 → 11 → 10 → 1 Assert.Null(graph.PathTo(10, 3)); Assert.Empty(graph.Cycles); } [Fact] public void Evaluation_order_for_a_request_holds_only_what_it_reaches() { var graph = Graph((12, [11]), (11, [10]), (10, [1]), (20, [2])); Assert.Equal([10, 11], graph.EvaluationOrderFor([11])); Assert.Equal([20], graph.EvaluationOrderFor([20, 1])); } [Fact] public void A_loop_is_reported_with_its_path_and_blocks_everything_that_depends_on_it() { // 10 → 11 → 12 → 10 is a loop; 13 reads it; 14 is independent. var graph = Graph((10, [11]), (11, [12, 1]), (12, [10]), (13, [10, 2]), (14, [1])); var cycle = Assert.Single(graph.Cycles); Assert.Equal([10, 11, 12, 10], cycle); Assert.Equal([11, 12, 10, 11], graph.CycleFor(11)); Assert.Equal([13, 10, 11, 12, 10], graph.CycleFor(13)); Assert.False(graph.IsEvaluable(13)); Assert.True(graph.IsEvaluable(14)); Assert.True(graph.IsEvaluable(1)); Assert.Null(graph.CycleFor(14)); Assert.Equal([14], graph.EvaluationOrder); } [Fact] public void A_meter_referring_to_itself_is_a_loop_of_one() { var graph = Graph((10, [10, 1])); Assert.Equal([10, 10], Assert.Single(graph.Cycles)); Assert.Empty(graph.EvaluationOrder); Assert.Equal([1], graph.PhysicalLeaves(10)); } [Fact] public void Dependents_are_the_virtual_meters_a_deletion_would_break() { var graph = Graph((10, [1, 2]), (11, [10, 3]), (12, [3])); Assert.Equal([10, 11], graph.Dependents(1)); Assert.Equal([11, 12], graph.Dependents(3)); Assert.Equal([11], graph.Dependents(10)); Assert.Empty(graph.Dependents(11)); } [Fact] public void A_legacy_virtual_meter_without_a_calculation_is_not_mistaken_for_a_physical_leaf() { var catalog = new MeterCatalog(VirtualFixtures.SeededElectricity()); // Summe Solar (6) has no definition var graph = DependencyGraph.FromCatalog(catalog); Assert.True(graph.IsVirtual(6)); Assert.Empty(graph.PhysicalLeaves(6)); Assert.False(graph.IsVirtual(4)); } [Fact] public void A_chain_of_five_thousand_virtual_meters_is_walked_without_recursion() { var chain = Enumerable.Range(1, 5000).Select(i => (Meter: 10_000 + i, Dependencies: new[] { i == 1 ? 1 : 10_000 + i - 1 })).ToArray(); var graph = Graph(chain); Assert.Equal(5000, graph.EvaluationOrder.Count); Assert.Equal(10_001, graph.EvaluationOrder[0]); Assert.Equal([1], graph.PhysicalLeaves(15_000)); Assert.Equal(5001, graph.PathTo(15_000, 1)!.Count); var looped = Graph([.. chain.Skip(1), (10_001, [15_000])]); Assert.Equal(5001, Assert.Single(looped.Cycles).Count); } [Fact] public void Paths_format_as_meter_ids_joined_by_the_separator() { Assert.Equal("12>7>12", DependencyGraph.FormatPath([12, 7, 12])); } }