From 98e7cf7f6f007e05a1426b377fc3e6f4027c58ec Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 9 Aug 2026 09:53:09 +0000 Subject: [PATCH] Put every node through every pipeline, not only limits The limit crash of #829 was found because somebody took a limit of a floor. Nothing made limits special: a node reaches a dozen pipelines, AddingNode.cs lists them, and anything missed there is missed silently, because the node that breaks is exactly the one nobody wrote a test for. Seven node types crashed that way and the suite stayed green. So this generalises LimitTerminatesOnEveryNodeTest: the node types are enumerated by reflection -- 45 of them -- and each is put through Stringize, Latexise, ToString, InnerSimplified, Evaled, Simplify, Expand, Factorize, Differentiate, Integrate, Substitute, Vars, Nodes, Complexity and Solve. What is asserted is only what every pipeline owes a caller: return, in finite time, either an answer or one of the library's own exceptions. Not that the answer is right -- the per-node tests are for that -- and not that there is one, since an unevaluated node is a legitimate answer. Everything passes today, so this adds no fix. It is a floor under the next node. Checked against a deliberately throwing pipeline to make sure it is not vacuous: the failure is reported with the pipeline named, which is what makes it useful when it does fire. Tests: 6049 passing, 0 failed, 14 skipped; F# 130. Co-Authored-By: Claude Opus 5 (1M context) --- .../EveryNodeSurvivesEveryPipelineTest.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 Sources/Tests/UnitTests/Common/EveryNodeSurvivesEveryPipelineTest.cs diff --git a/Sources/Tests/UnitTests/Common/EveryNodeSurvivesEveryPipelineTest.cs b/Sources/Tests/UnitTests/Common/EveryNodeSurvivesEveryPipelineTest.cs new file mode 100644 index 000000000..665914695 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/EveryNodeSurvivesEveryPipelineTest.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// Every node survives every pipeline: no crash, no hang, no exception the caller has no + /// reason to expect. + /// + /// + /// + /// The generalisation of LimitTerminatesOnEveryNodeTest. Limits were where the + /// hole showed — seven node types killed the process by inheriting a default that + /// returned the node instead of — but nothing made limits special. + /// A node reaches a dozen pipelines and Docs/Contributing/AddingNode.cs lists + /// them; anything missed there is missed silently, because the node that breaks is + /// exactly the one nobody wrote a test for. + /// + /// + /// So the node types are enumerated rather than listed, and each is put through every + /// pipeline. What is asserted is only what every pipeline owes a caller: return, in + /// finite time, either an answer or an . + /// Not that the answer is right — the per-node tests are for that — and not that there + /// is one at all, since an unevaluated node is a legitimate answer here. + /// + /// https://github.com/asc-community/AngouriMath/issues/829 + /// + [Trait("Area", "Common")] + public sealed class EveryNodeSurvivesEveryPipelineTest + { + private static readonly Entity.Variable X = MathS.Var("x"); + + public static IEnumerable EveryNodeType() + { + var built = new List(); + foreach (var type in typeof(Entity).Assembly.GetTypes() + .Where(t => t.IsSealed && !t.IsAbstract && typeof(Entity).IsAssignableFrom(t)) + .OrderBy(t => t.FullName, StringComparer.Ordinal)) + { + var constructor = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .Where(c => c.GetParameters().Length > 0 + && c.GetParameters().All(p => p.ParameterType == typeof(Entity))) + .OrderBy(c => c.GetParameters().Length) + .FirstOrDefault(); + if (constructor is null) + continue; + try + { + built.Add((Entity)constructor.Invoke( + constructor.GetParameters().Select(_ => (object)(Entity)X).ToArray())); + } + catch (Exception) { /* a node that will not take a bare variable is not the subject */ } + } + Assert.NotEmpty(built); + return built.Select(n => new object[] { n }); + } + + /// + /// The pipelines a node reaches, named so a failure says which one broke. + /// + private static readonly (string Name, Action Run)[] Pipelines = + { + ("Stringize", e => e.Stringize()), + ("Latexise", e => e.Latexise()), + ("ToString", e => e.ToString()), + ("InnerSimplified", e => { _ = e.InnerSimplified; }), + ("Evaled", e => { _ = e.Evaled; }), + ("Simplify", e => e.Simplify()), + ("Expand", e => e.Expand()), + ("Factorize", e => e.Factorize()), + ("Differentiate", e => e.Differentiate(X)), + ("Integrate", e => e.Integrate(X)), + ("Substitute", e => e.Substitute(X, 2)), + ("Vars", e => { _ = e.Vars.Count; }), + ("Nodes", e => { _ = e.Nodes.Count(); }), + ("Complexity", e => { _ = e.Complexity; }), + ("Solve", e => e.SolveEquation(X)), + }; + + [Theory] + [MemberData(nameof(EveryNodeType))] + public void EveryPipelineReturns(Entity node) + { + var broken = new List(); + foreach (var (name, run) in Pipelines) + { + // Each in a task with a timeout: a regression here is non-termination, and + // the suite must fail rather than hang. A stack overflow cannot be caught at + // all, so the signal for that one is the run aborting. + string? failure = null; + var finished = Task.Run(() => + { + try { run(node); } + catch (AngouriMath.Core.Exceptions.AngouriMathBaseException) { /* the library's own, and legitimate */ } + catch (NotSupportedException) { /* documented refusal, e.g. uncompilable shapes */ } + catch (Exception e) { failure = e.GetType().FullName + ": " + e.Message.Split('\n')[0]; } + }).Wait(TimeSpan.FromSeconds(30)); + + if (!finished) + broken.Add($"{name} did not terminate"); + else if (failure is { }) + broken.Add($"{name} threw {failure}"); + } + + Assert.True(broken.Count is 0, + $"{node.Stringize()} [{node.GetType().Name}]:\n " + string.Join("\n ", broken)); + } + } +}