wf test fix
This commit is contained in:
parent
0d71f39245
commit
87b1abae4e
|
|
@ -1,4 +1,4 @@
|
|||
using w4c_workflows.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
using w4c_workflows.Services;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -9,6 +9,20 @@ public class MermaidGeneratorServiceTests
|
|||
private const string Tenant = "t1";
|
||||
private const string Path = "workflows/main-task.yaml";
|
||||
|
||||
/// <summary>Diagram names accepted by <see cref="MermaidGeneratorService.Generate"/>.
|
||||
/// Centralising them removes magic strings and keeps tests in sync with the service.</summary>
|
||||
private static class Diagram
|
||||
{
|
||||
public const string Flowchart = "flowchart";
|
||||
public const string Sequence = "sequence";
|
||||
public const string State = "state";
|
||||
public const string Class = "class";
|
||||
}
|
||||
|
||||
/// <summary><see cref="MermaidGeneratorService"/> is stateless (its methods are all static), so a
|
||||
/// single instance is shared instead of allocating one per <see cref="Render(string)"/> call.</summary>
|
||||
private static readonly MermaidGeneratorService Generator = new();
|
||||
|
||||
private static readonly string ExampleYaml = """
|
||||
name: main-task
|
||||
mode: handler
|
||||
|
|
@ -43,11 +57,15 @@ public class MermaidGeneratorServiceTests
|
|||
=> new WorkflowCompiler(new WorkflowValidator(new LanguageRegistry()))
|
||||
.Compile(ExampleYaml, Path, Tenant).Workflow!;
|
||||
|
||||
private static string Render(string type) => Render(Compile(), type);
|
||||
|
||||
private static string Render(CompiledWorkflow compiled, string type)
|
||||
=> Generator.Generate(type, compiled.Workflow, compiled.Tasks);
|
||||
|
||||
[Fact]
|
||||
public void Flowchart_renders_next_solid_and_onError_dashed()
|
||||
{
|
||||
var compiled = Compile();
|
||||
var source = new MermaidGeneratorService().Generate("flowchart", compiled.Workflow, compiled.Tasks);
|
||||
var source = Render(Diagram.Flowchart);
|
||||
|
||||
Assert.Contains("flowchart TD", source);
|
||||
Assert.Contains("root --> validate", source);
|
||||
|
|
@ -59,8 +77,7 @@ public class MermaidGeneratorServiceTests
|
|||
[Fact]
|
||||
public void Sequence_renders_happy_path_and_alt_error_block()
|
||||
{
|
||||
var compiled = Compile();
|
||||
var source = new MermaidGeneratorService().Generate("sequence", compiled.Workflow, compiled.Tasks);
|
||||
var source = Render(Diagram.Sequence);
|
||||
|
||||
Assert.Contains("sequenceDiagram", source);
|
||||
Assert.Contains("root->>validate", source);
|
||||
|
|
@ -73,19 +90,51 @@ public class MermaidGeneratorServiceTests
|
|||
public void State_renders_entry_terminal_and_error_transitions()
|
||||
{
|
||||
var compiled = Compile();
|
||||
var source = new MermaidGeneratorService().Generate("state", compiled.Workflow, compiled.Tasks);
|
||||
var source = Render(compiled, Diagram.State);
|
||||
var (aliasOf, edges) = ParseStateDiagram(source);
|
||||
|
||||
Assert.Contains("stateDiagram-v2", source);
|
||||
Assert.Contains("[*] --> root", source);
|
||||
Assert.Contains("persist --> [*]", source);
|
||||
Assert.Contains("validate --> compensate: error", source);
|
||||
Assert.StartsWith("stateDiagram-v2", source.TrimStart(), StringComparison.Ordinal);
|
||||
|
||||
// Every task (including the synthesized "root") is declared exactly once as a
|
||||
// labelled state. We must not assert literal transition text such as
|
||||
// "[*] --> root": State() aliases every key to a reserved-word-safe id (s0,
|
||||
// s1, ...) and prints the human key only inside the quoted label. Comparing
|
||||
// the declared labels against the compiled keys is order- and index-agnostic.
|
||||
Assert.Equal(
|
||||
compiled.Tasks.Select(t => t.Key).OrderBy(k => k, StringComparer.Ordinal),
|
||||
aliasOf.Keys.OrderBy(k => k, StringComparer.Ordinal));
|
||||
|
||||
// Entry: exactly one initial transition, and it starts at "root".
|
||||
var entryEdges = edges.Where(e => e.From == "[*]").ToList();
|
||||
Assert.Single(entryEdges);
|
||||
Assert.Equal(aliasOf["root"], entryEdges[0].To);
|
||||
|
||||
// Terminal: each task with no success/error successor ends at "[*]"
|
||||
// (persist and compensate in this fixture), so no other task is terminal.
|
||||
Assert.Equal(
|
||||
compiled.Tasks.Where(t => t.NextId == null && t.OnErrorId == null)
|
||||
.Select(t => aliasOf[t.Key]).OrderBy(a => a, StringComparer.Ordinal),
|
||||
edges.Where(e => e.To == "[*]").Select(e => e.From).OrderBy(a => a, StringComparer.Ordinal));
|
||||
|
||||
// Error path: validate's onError: compensate is rendered as a labelled edge.
|
||||
var errorEdge = Assert.Single(edges.Where(e => e.Label == "error"));
|
||||
Assert.Equal(aliasOf["validate"], errorEdge.From);
|
||||
Assert.Equal(aliasOf["compensate"], errorEdge.To);
|
||||
|
||||
// Structural integrity: no edge may reference an undeclared state, which would
|
||||
// render as a raw id instead of a labelled node.
|
||||
var declared = aliasOf.Values.Append("[*]").ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
Assert.Contains(edge.From, declared);
|
||||
Assert.Contains(edge.To, declared);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Class_renders_language_not_runtime()
|
||||
{
|
||||
var compiled = Compile();
|
||||
var source = new MermaidGeneratorService().Generate("class", compiled.Workflow, compiled.Tasks);
|
||||
var source = Render(Diagram.Class);
|
||||
|
||||
Assert.Contains("classDiagram", source);
|
||||
Assert.Contains("<<typescript>>", source); // root language
|
||||
|
|
@ -98,7 +147,52 @@ public class MermaidGeneratorServiceTests
|
|||
public void Unknown_type_throws()
|
||||
{
|
||||
var compiled = Compile();
|
||||
var generator = new MermaidGeneratorService();
|
||||
Assert.Throws<ArgumentException>(() => generator.Generate("pie", compiled.Workflow, compiled.Tasks));
|
||||
Assert.Throws<ArgumentException>(() => Render(compiled, "pie"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ state helpers
|
||||
// State() is the only generator that aliases task keys to generated ids, so its output
|
||||
// is verified structurally (labelled declarations + edges) rather than by exact
|
||||
// substrings, which would silently couple the test to the alias numbering.
|
||||
|
||||
private sealed record StateEdge(string From, string To, string? Label);
|
||||
|
||||
private static readonly Regex StateDeclRegex = new(
|
||||
@"^state ""(?<label>.+?)""\s+as\s+(?<alias>\w+)$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex StateEdgeRegex = new(
|
||||
@"^(?<from>\[\*\]|\w+) --> (?<to>\[\*\]|\w+)(?:\s*:\s*(?<label>.+))?$", RegexOptions.Compiled);
|
||||
|
||||
private static (Dictionary<string, string> AliasOf, IReadOnlyList<StateEdge> Edges)
|
||||
ParseStateDiagram(string source)
|
||||
{
|
||||
var aliasOf = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var edges = new List<StateEdge>();
|
||||
|
||||
foreach (var rawLine in source.Split('\n'))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0)
|
||||
continue;
|
||||
|
||||
var declaration = StateDeclRegex.Match(line); // state "key" as sN
|
||||
if (declaration.Success)
|
||||
{
|
||||
aliasOf.TryAdd(declaration.Groups["label"].Value, declaration.Groups["alias"].Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
var edge = StateEdgeRegex.Match(line); // from --> to[: label]
|
||||
if (edge.Success)
|
||||
{
|
||||
edges.Add(new StateEdge(
|
||||
edge.Groups["from"].Value,
|
||||
edge.Groups["to"].Value,
|
||||
edge.Groups["label"].Success ? edge.Groups["label"].Value : null));
|
||||
}
|
||||
// Other lines (the "stateDiagram-v2" header) are intentionally ignored.
|
||||
}
|
||||
|
||||
return (aliasOf, edges);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue