Skip to content

Commit 6c1d495

Browse files
Put every node through every pipeline, not only limits (#837)
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) <noreply@anthropic.com>
1 parent aa0046d commit 6c1d495

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// Copyright (c) 2019-2026 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Reflection;
12+
using System.Threading.Tasks;
13+
using AngouriMath;
14+
using AngouriMath.Extensions;
15+
using Xunit;
16+
17+
namespace AngouriMath.Tests.Common
18+
{
19+
/// <summary>
20+
/// Every node survives every pipeline: no crash, no hang, no exception the caller has no
21+
/// reason to expect.
22+
/// </summary>
23+
/// <remarks>
24+
/// <para>
25+
/// The generalisation of <c>LimitTerminatesOnEveryNodeTest</c>. Limits were where the
26+
/// hole showed — seven node types killed the process by inheriting a default that
27+
/// returned the node instead of <see langword="null"/> — but nothing made limits special.
28+
/// A node reaches a dozen pipelines and <c>Docs/Contributing/AddingNode.cs</c> lists
29+
/// them; anything missed there is missed silently, because the node that breaks is
30+
/// exactly the one nobody wrote a test for.
31+
/// </para>
32+
/// <para>
33+
/// So the node types are enumerated rather than listed, and each is put through every
34+
/// pipeline. What is asserted is only what every pipeline owes a caller: return, in
35+
/// finite time, either an answer or an <see cref="AngouriMath.Core.Exceptions.AngouriMathBaseException"/>.
36+
/// Not that the answer is right — the per-node tests are for that — and not that there
37+
/// is one at all, since an unevaluated node is a legitimate answer here.
38+
/// </para>
39+
/// https://github.com/asc-community/AngouriMath/issues/829
40+
/// </remarks>
41+
[Trait("Area", "Common")]
42+
public sealed class EveryNodeSurvivesEveryPipelineTest
43+
{
44+
private static readonly Entity.Variable X = MathS.Var("x");
45+
46+
public static IEnumerable<object[]> EveryNodeType()
47+
{
48+
var built = new List<Entity>();
49+
foreach (var type in typeof(Entity).Assembly.GetTypes()
50+
.Where(t => t.IsSealed && !t.IsAbstract && typeof(Entity).IsAssignableFrom(t))
51+
.OrderBy(t => t.FullName, StringComparer.Ordinal))
52+
{
53+
var constructor = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
54+
.Where(c => c.GetParameters().Length > 0
55+
&& c.GetParameters().All(p => p.ParameterType == typeof(Entity)))
56+
.OrderBy(c => c.GetParameters().Length)
57+
.FirstOrDefault();
58+
if (constructor is null)
59+
continue;
60+
try
61+
{
62+
built.Add((Entity)constructor.Invoke(
63+
constructor.GetParameters().Select(_ => (object)(Entity)X).ToArray()));
64+
}
65+
catch (Exception) { /* a node that will not take a bare variable is not the subject */ }
66+
}
67+
Assert.NotEmpty(built);
68+
return built.Select(n => new object[] { n });
69+
}
70+
71+
/// <summary>
72+
/// The pipelines a node reaches, named so a failure says which one broke.
73+
/// </summary>
74+
private static readonly (string Name, Action<Entity> Run)[] Pipelines =
75+
{
76+
("Stringize", e => e.Stringize()),
77+
("Latexise", e => e.Latexise()),
78+
("ToString", e => e.ToString()),
79+
("InnerSimplified", e => { _ = e.InnerSimplified; }),
80+
("Evaled", e => { _ = e.Evaled; }),
81+
("Simplify", e => e.Simplify()),
82+
("Expand", e => e.Expand()),
83+
("Factorize", e => e.Factorize()),
84+
("Differentiate", e => e.Differentiate(X)),
85+
("Integrate", e => e.Integrate(X)),
86+
("Substitute", e => e.Substitute(X, 2)),
87+
("Vars", e => { _ = e.Vars.Count; }),
88+
("Nodes", e => { _ = e.Nodes.Count(); }),
89+
("Complexity", e => { _ = e.Complexity; }),
90+
("Solve", e => e.SolveEquation(X)),
91+
};
92+
93+
[Theory]
94+
[MemberData(nameof(EveryNodeType))]
95+
public void EveryPipelineReturns(Entity node)
96+
{
97+
var broken = new List<string>();
98+
foreach (var (name, run) in Pipelines)
99+
{
100+
// Each in a task with a timeout: a regression here is non-termination, and
101+
// the suite must fail rather than hang. A stack overflow cannot be caught at
102+
// all, so the signal for that one is the run aborting.
103+
string? failure = null;
104+
var finished = Task.Run(() =>
105+
{
106+
try { run(node); }
107+
catch (AngouriMath.Core.Exceptions.AngouriMathBaseException) { /* the library's own, and legitimate */ }
108+
catch (NotSupportedException) { /* documented refusal, e.g. uncompilable shapes */ }
109+
catch (Exception e) { failure = e.GetType().FullName + ": " + e.Message.Split('\n')[0]; }
110+
}).Wait(TimeSpan.FromSeconds(30));
111+
112+
if (!finished)
113+
broken.Add($"{name} did not terminate");
114+
else if (failure is { })
115+
broken.Add($"{name} threw {failure}");
116+
}
117+
118+
Assert.True(broken.Count is 0,
119+
$"{node.Stringize()} [{node.GetType().Name}]:\n " + string.Join("\n ", broken));
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)