/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
tests/ClothesGraph.Model.Tests/DraftGraphTests.cs
300 строк
13 KB
docNemo
Запрос на слияние 'interface-to-spec' (
#1
) из interface-to-spec в main
09 авг 2026, 20:28
Верифицирован
09 авг 2026, 20:28
1bbc436
Код
Авторство
О чём код?
using System.Collections.Generic; using System.Linq; using ClothesGraph.Geometry; using ClothesGraph.Model.Drafting; using ClothesGraph.Model.Formulas; using ClothesGraph.Model.Measurements; using Xunit; namespace ClothesGraph.Model.Tests; public class DraftGraphTests { private sealed class Scope(DraftGraph graph, MeasurementSet measurements) : IFormulaScope { public MeasurementId? FindMeasurement(string name) => measurements.FindByName(name)?.Id; public NodeId? FindNode(string name) => graph.FindByName(name)?.Id; public string? GetMeasurementName(MeasurementId id) => measurements.TryGet(id, out var measurement) ? measurement!.DisplayName : null; public string? GetNodeName(NodeId id) => graph.TryGet(id, out var node) ? node!.Name : null; } private static Dictionary<MeasurementId, Length> ValuesOf(MeasurementSet set) => set.Measurements.ToDictionary(m => m.Id, m => m.Value); /// <summary>Три точки в ряд: A — начало, B правее на Ог/4, C правее B на 10.</summary> private static (DraftGraph Graph, MeasurementSet Set, NodeId A, NodeId B, NodeId C) BuildChain() { var graph = new DraftGraph(); var set = new MeasurementSet(); set.Add("Ог", Length.FromCentimetres(92)); var scope = new Scope(graph, set); var a = graph.Add(new FreePoint(NodeId.New(), "A", Formula.Constant(0), Formula.Constant(0))).Id; var b = graph.Add(new OffsetPoint(NodeId.New(), "B", a, Formula.Parse("Ог/4", scope), Formula.Constant(0))).Id; var c = graph.Add(new OffsetPoint(NodeId.New(), "C", b, Formula.Constant(10), Formula.Constant(0))).Id; return (graph, set, a, b, c); } [Fact] public void Построения_вычисляются_в_порядке_зависимостей() { var (graph, set, a, b, c) = BuildChain(); var evaluator = new DraftEvaluator(graph); var evaluation = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.True(evaluation.IsComplete); Assert.Equal(Point2.Origin, evaluation.Points[a]); Assert.Equal(Length.FromCentimetres(23), evaluation.Points[b].X); Assert.Equal(Length.FromCentimetres(33), evaluation.Points[c].X); } [Fact] public void Пересчитывается_только_затронутое_поддерево() { var (graph, set, _, b, _) = BuildChain(); var evaluator = new DraftEvaluator(graph); evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.Equal(3, evaluator.LastRecomputedCount); // Правка B задевает B и C, но не A. graph.Invalidate(b); evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.Equal(2, evaluator.LastRecomputedCount); } [Fact] public void Повторный_пересчёт_без_изменений_ничего_не_делает() { var (graph, set, _, _, _) = BuildChain(); var evaluator = new DraftEvaluator(graph); evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.Equal(0, evaluator.LastRecomputedCount); } [Fact] public void Изменение_мерки_задевает_только_зависящие_построения() { var (graph, set, _, _, _) = BuildChain(); var evaluator = new DraftEvaluator(graph); evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); var chest = set.FindByName("Ог")!; set.SetValue(chest.Id, Length.FromCentimetres(100)); graph.InvalidateMeasurement(chest.Id); var evaluation = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); // A от мерки не зависит и не пересчитывался; B и C — пересчитались. Assert.Equal(2, evaluator.LastRecomputedCount); Assert.Equal(Length.FromCentimetres(25), evaluation.Points[graph.FindByName("B")!.Id].X); Assert.Equal(Length.FromCentimetres(35), evaluation.Points[graph.FindByName("C")!.Id].X); } [Fact] public void Цикл_обнаруживается_с_перечислением_участников() { var graph = new DraftGraph(); var set = new MeasurementSet(); var scope = new Scope(graph, set); var a = graph.Add(new FreePoint(NodeId.New(), "A", Formula.Constant(0), Formula.Constant(0))).Id; var b = graph.Add(new OffsetPoint(NodeId.New(), "B", a, Formula.Constant(10), Formula.Constant(0))).Id; // Попытка сослать A на длину отрезка, который зависит от A. var segment = graph.Add(new SegmentNode(NodeId.New(), "AB", a, b)).Id; Assert.True(graph.WouldCreateCycle(a, segment)); var error = Assert.Throws<DraftCycleException>(() => graph.Update(a, node => { var point = (FreePoint)node; point.X = Formula.Parse("AB.длина", scope); })); Assert.Contains("A", error.CycleNames); Assert.Contains("AB", error.CycleNames); } [Fact] public void Прямая_ссылка_на_себя_распознаётся() { var (graph, _, a, _, _) = BuildChain(); Assert.True(graph.WouldCreateCycle(a, a)); } [Fact] public void Удаление_с_зависимыми_требует_подтверждения() { var (graph, _, a, _, _) = BuildChain(); var error = Assert.Throws<DraftDependentsException>(() => graph.Remove(a)); Assert.Equal(["B", "C"], error.Ordered().ToArray()); Assert.Equal(3, graph.Count); graph.Remove(a, force: true); Assert.Equal(0, graph.Count); } /// <summary> /// Удалённое построение не остаётся в результате пересчёта. /// </summary> /// <remarks> /// Вычислитель хранит геометрию между вызовами, а обход считает только то, /// что есть в графе, — удалённого он не касается, и его точка оставалась в /// кэше навсегда. Наружу она выходила наравне с живыми: удалённая линия /// продолжала бы рисоваться, а поиск её в графе по идентификатору из того /// же результата обрывался бы ошибкой. /// </remarks> [Fact] public void Удалённое_построение_уходит_из_результата_пересчёта() { var (graph, set, a, b, _) = BuildChain(); var evaluator = new DraftEvaluator(graph); var before = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.Contains(b, before.Points.Keys); // Удаление каскадное: C зависит от B и уходит вместе с ним. graph.Remove(b, force: true); var after = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.DoesNotContain(b, after.Points.Keys); Assert.Contains(a, after.Points.Keys); foreach (var id in after.Points.Keys) Assert.True(graph.Contains(id), $"построение {id} есть в пересчёте, но не в графе"); } [Fact] public void Неразрешимое_построение_не_рушит_остальной_чертёж() { var graph = new DraftGraph(); var set = new MeasurementSet(); var a = graph.Add(new FreePoint(NodeId.New(), "A", Formula.Constant(0), Formula.Constant(0))).Id; var b = graph.Add(new OffsetPoint(NodeId.New(), "B", a, Formula.Constant(10), Formula.Constant(0))).Id; var c = graph.Add(new FreePoint(NodeId.New(), "C", Formula.Constant(0), Formula.Constant(10))).Id; var d = graph.Add(new OffsetPoint(NodeId.New(), "D", c, Formula.Constant(10), Formula.Constant(0))).Id; // Два параллельных отрезка — их пересечение неразрешимо. var first = graph.Add(new SegmentNode(NodeId.New(), "AB", a, b)).Id; var second = graph.Add(new SegmentNode(NodeId.New(), "CD", c, d)).Id; var crossing = graph.Add(new IntersectionPoint(NodeId.New(), "X", first, second)).Id; var dependent = graph.Add(new OffsetPoint(NodeId.New(), "Y", crossing, Formula.Constant(5), Formula.Constant(0))).Id; var evaluation = new DraftEvaluator(graph).Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.False(evaluation.IsComplete); // Неразрешимы только точка пересечения и то, что от неё зависит. Assert.Equal(2, evaluation.Failures.Count); Assert.Contains("не пересекаются", evaluation.Failures[crossing].Reason); Assert.Equal(crossing, evaluation.Failures[dependent].CausedBy); // Всё остальное вычислено и пригодно для работы. Assert.True(evaluation.TryGetPoint(a, out _)); Assert.True(evaluation.TryGetPoint(d, out _)); Assert.True(evaluation.TryGetCurve(first, out _)); } [Fact] public void Точка_на_кривой_по_доле_и_по_расстоянию() { var graph = new DraftGraph(); var set = new MeasurementSet(); var a = graph.Add(new FreePoint(NodeId.New(), "A", Formula.Constant(0), Formula.Constant(0))).Id; var b = graph.Add(new OffsetPoint(NodeId.New(), "B", a, Formula.Constant(10), Formula.Constant(0))).Id; var segment = graph.Add(new SegmentNode(NodeId.New(), "AB", a, b)).Id; var middle = graph.Add(new PointOnCurve(NodeId.New(), "M", segment, Formula.Constant(0.5), PointOnCurveMode.Parameter)).Id; var quarter = graph.Add(new PointOnCurve(NodeId.New(), "Q", segment, Formula.Constant(2.5), PointOnCurveMode.Distance)).Id; var evaluation = new DraftEvaluator(graph).Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.Equal(Length.FromCentimetres(5), evaluation.Points[middle].X); Assert.Equal(Length.FromCentimetres(2.5), evaluation.Points[quarter].X); } [Fact] public void Кривая_Безье_перестраивается_вслед_за_опорными_точками() { var graph = new DraftGraph(); var set = new MeasurementSet(); set.Add("Ог", Length.FromCentimetres(92)); var scope = new Scope(graph, set); var start = graph.Add(new FreePoint(NodeId.New(), "P0", Formula.Constant(0), Formula.Constant(0))).Id; var control1 = graph.Add(new FreePoint(NodeId.New(), "P1", Formula.Constant(0), Formula.Constant(5))).Id; var control2 = graph.Add(new FreePoint(NodeId.New(), "P2", Formula.Parse("Ог/4", scope), Formula.Constant(5))).Id; var end = graph.Add(new FreePoint(NodeId.New(), "P3", Formula.Parse("Ог/4", scope), Formula.Constant(0))).Id; var curve = graph.Add(new CubicCurveNode(NodeId.New(), "C", start, control1, control2, end)).Id; var evaluator = new DraftEvaluator(graph); var before = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre).Curves[curve].Length; var chest = set.FindByName("Ог")!; set.SetValue(chest.Id, Length.FromCentimetres(120)); graph.InvalidateMeasurement(chest.Id); var after = evaluator.Evaluate(ValuesOf(set), LengthUnit.Centimetre).Curves[curve].Length; Assert.True(after > before); } [Fact] public void Ошибка_в_формуле_не_разрушает_остальное() { var graph = new DraftGraph(); var set = new MeasurementSet(); var a = graph.Add(new FreePoint(NodeId.New(), "A", Formula.Constant(0), Formula.Constant(0))).Id; var b = graph.Add(new OffsetPoint(NodeId.New(), "B", a, Formula.Parse("10 / 0", new Scope(graph, set)), Formula.Constant(0))).Id; var evaluation = new DraftEvaluator(graph).Evaluate(ValuesOf(set), LengthUnit.Centimetre); Assert.True(evaluation.TryGetPoint(a, out _)); Assert.Contains("ноль", evaluation.Failures[b].Reason); } }