Commit bf1cecdf authored by shrabbit's avatar shrabbit
Browse files

添加图测试和节点编辑器,更新图执行逻辑和序列化选项

对Graph进行测试和修复(完成了BasicReload, Linking, Node, NodeReload,失败LinkingReload)
parent 890bff30
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -46,6 +46,11 @@ public abstract class Function : IFunction
    }
}

public abstract class FunctionByName<T> : Function
{
    public override string Name { get; } = nameof(T);
}

/// <summary>
/// 一个快速配置的函数类,它将所有配置和执行都放置于构造函数中。
/// </summary>
+299 −0
Original line number Diff line number Diff line
using System.Text.Json.Nodes;
using Nocube.Common;
using Nocube.Common.IO;
using Nocube.Common.Nodes;
using Nocube.Common.Packages;
using Nocube.Common.Properties;
using Nocube.Engine.Graphs;
using Nocube.Engine.Shared;

namespace Nocube.Engine.Test.Graphs;

[TestFixture]
[TestOf(typeof(Graph))]
public class GraphTest
{
    [Test]
    [Description("基本")]
    public async Task BasicMetadataAndReadTest()
    {
        // 初始化图
        var descTarget = "DESC";
        var nameTarget = "NAME";
        var metaKeyTarget = "META";
        var metaValueTarget = new JsonObject
        {
            ["a"] = "b"
        };
        var verTarget = new Version(1, 2);

        var engine = new Engine();
        
        var graph = new Graph(engine)
        {
            Description = descTarget,
            EngineVersion = verTarget,
            Name = nameTarget,
            Metadata =
            {
                [metaKeyTarget] = metaValueTarget
            }
        };
        
        // 存储图
        var json = await graph.SaveAsync();
        
        // 从 JSON 读取
        var newGraph = new Graph(engine);
        await newGraph.ReadAsync(json);

        Assert.Multiple(() =>
        {
            // 进行断言
            Assert.That(graph.Description, Is.EqualTo(descTarget));
            Assert.That(graph.Name, Is.EqualTo(nameTarget));
            Assert.That(graph.EngineVersion.Major, Is.EqualTo(verTarget.Major));
            Assert.That(graph.EngineVersion.Minor, Is.EqualTo(verTarget.Minor));
            Assert.That(graph.Metadata.TryGetValue(metaKeyTarget, out var j), Is.True);
            Assert.That(j.ToJsonString(), Is.EqualTo(metaValueTarget.ToJsonString()));
        });
    }

    [Test]
    public async Task TestNodeTest()
    {
        var node = new TestNode();
        var a = 123;
        var c = true;
        var b = "Hello";
        var template = $"A: {a}, B: {b}, C: {c}";
        
        // 模拟节点输入
        node.Properties[0].Data = a;
        node.Properties[1].Data = b;
        node.Properties[2].Data = c;
        
        // 运行节点
        await node.Execute();
        
        // 检查结果
        Assert.That(node.Properties[3].Data, Is.EqualTo(template));
    }

    [Test]
    public async Task NodeTest()
    {
        var engine = new Engine();
        var graph = new Graph(engine);

        var node = new TestNode();
        
        // 模拟节点输入
        var a = 123;
        var c = true;
        var b = "Hello";
        var template = $"A: {a}, B: {b}, C: {c}";
        
        node.Properties[0].Data = a;
        node.Properties[1].Data = b;
        node.Properties[2].Data = c;
        
        // 加入节点
        graph.Nodes.Add(new GraphNodeTree([], [ new NodeInfo(node, 0, 0) ]));
        
        // 运行节点
        await graph.ExecuteAsync();
        
        // 检查结果
        Assert.That(node.Properties[3].Data, Is.EqualTo(template));
    }

    [Test]
    public async Task NodeAndReloadTest()
    {
        var engine = new Engine();
        var graph = new Graph(engine);

        var node = new TestNode();

        // 模拟节点输入
        var a = 123;
        var c = true;
        var b = "Hello";
        var template = $"A: {a}, B: {b}, C: {c}";

        node.Properties[0].Data = a;
        node.Properties[1].Data = b;
        node.Properties[2].Data = c;

        // 加入节点
        graph.Nodes.Add(new GraphNodeTree([], [new NodeInfo(node, 0, 0)]));

        // 运行节点
        await graph.ExecuteAsync();

        // 检查结果
        Assert.That(node.Properties[3].Data, Is.EqualTo(template));
        
        // 现在保存并从 JSON 中重新加载
        var json = await graph.SaveAsync();
        var newGraph = new Graph(engine);
        await newGraph.ReadAsync(json);
        
        // 给节点输入值
        var newNode = newGraph.Nodes[0].Nodes[0].Node;

        newNode.Properties[0].Data = a;
        newNode.Properties[1].Data = b;
        newNode.Properties[2].Data = c;
        
        // 执行节点
        await newGraph.ExecuteAsync();

        // 检查结果
        Assert.That(newNode.Properties[3].Data, Is.EqualTo(template));
    }

    [Test]
    public async Task LinkingTest()
    {
        var engine = new Engine();
        var graph = new Graph(engine);

        var result = "Hello";

        // 加载10个节点,提取第一个作为入口
        for (var i = 0; i < 10; i++)
        {
            var node = new LinkedNode();
            graph.Nodes.Add(new GraphNodeTree([], [new NodeInfo(node, 0, 0)]));
        }
        
        // 给节点输入值
        var first = graph.Nodes[0].Nodes[0].Node;
        first.Properties[0].Data = result;
        
        // 连接节点
        for (var i = 1; i < 10; i++)
        {
            var current = graph.Nodes[i].Nodes[0].Node;
            var before = graph.Nodes[i - 1].Nodes[0].Node;
            
            // 把上一个节点的输出给下一个节点的输入
            graph.Flows.Add(new GraphFlow(before.Properties[1], current.Properties[0]));
        }
        
        // 执行节点
        await graph.ExecuteAsync();
        
        // 检查结果
        var index = 0;
        foreach (var tree in graph.Nodes)
        {
            var pro = tree.Nodes[0].Node.Properties[1];
            Assert.That(pro.Data, Is.EqualTo(result), $"节点 {index} 的输出不正确。");
            index++;
        }
    }

    [Test]
    public async Task LinkingNodeAndReloadTest()
    {
        var engine = new Engine();
        var graph = new Graph(engine);

        var result = "Hello";

        // 加载10个节点,提取第一个作为入口
        for (var i = 0; i < 10; i++)
        {
            var node = new LinkedNode();
            graph.Nodes.Add(new GraphNodeTree([], [new NodeInfo(node, 0, 0)]));
        }

        // 给节点输入值
        var first = graph.Nodes[0].Nodes[0].Node;
        first.Properties[0].Data = result;

        // 连接节点
        for (var i = 1; i < 10; i++)
        {
            var current = graph.Nodes[i].Nodes[0].Node;
            var before = graph.Nodes[i - 1].Nodes[0].Node;

            // 把上一个节点的输出给下一个节点的输入
            graph.Flows.Add(new GraphFlow(before.Properties[1], current.Properties[0]));
        }

        // 执行节点
        await graph.ExecuteAsync();

        // 检查结果
        var index = 0;
        foreach (var tree in graph.Nodes)
        {
            var pro = tree.Nodes[0].Node.Properties[1];
            Assert.That(pro.Data, Is.EqualTo(result), $"节点 {index} 的输出不正确。");
            index++;
        }
        
        // 再次进行存储
        var json = await graph.SaveAsync();

        // 从JSON读取图
        var newGraph = new Graph(engine);
        await newGraph.ReadAsync(json);
        
        // 为第一个节点输入数据
        var newFirst = newGraph.Nodes[0].Nodes[0].Node;
        newFirst.Properties[0].Data = result;
        
        // 执行节点
        await graph.ExecuteAsync();
        
        // 检查结果
        var newIndex = 0;
        foreach (var tree in newGraph.Nodes)
        {
            var pro = tree.Nodes[0].Node.Properties[1];
            Assert.That(pro.Data, Is.EqualTo(result), $"节点 {newIndex} 的输出不正确。");
            newIndex++;
        }
    }
}

internal class Engine : IEngineContext
{
    public IEnumerable<INode> GetNodes()
    {
        yield return new TestNode();
        yield return new LinkedNode();
    }

    public CommonContext CommonContext { get; } = new ();
    public IConsole Console { get; } = null!;
    public ILogger Logger { get; } = null!;
}

internal class LinkedNode : FunctionByName<LinkedNode>
{
    public override string Description => "[测试用] 链式传递测试节点";
    public override string Path => "testing/";

    private readonly Property<string> _in;
    private readonly Property<string> _out;

    public LinkedNode()
    {
        _in = AddToProperties(new Property<string>("IN", "IN", FlowDirect.In));
        _out = AddToProperties(new Property<string>("OUT", "OUT", FlowDirect.Out));
    }
    
    public override Task Execute(CancellationToken cancellationToken = default)
    {
        // 将in转递给out
        _out.Data = _in.Data;
        
        return Task.CompletedTask;
    }
}
 No newline at end of file
+40 −17
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ public class Graph : IName, IDescription

    public string Name { get; set; } = "";
    public string Description { get; set; } = "";
    public Version EngineVersion { get; set; } = null!;
    public Version EngineVersion { get; set; } = new(0, 0);
    public Dictionary<string, JsonNode> Metadata { get; private set; } = [];

    public NodeRegistry NodeRegistry { get; } = new();
@@ -318,6 +318,8 @@ public class Graph : IName, IDescription
        return await Task.Run(() =>
        {
            // 将内部的 GraphFlow 转换为 GraphFlowRef 以便序列化
            var opt = JsonSerializerOptions.Default;
            
            var flowRefs = new List<GraphFlowRef>();
            foreach (var flow in Flows)
            {
@@ -339,8 +341,8 @@ public class Graph : IName, IDescription
            
            // 将节点转换为可序列化的形式
            var serializableNodes = ConvertToSerializable(Nodes);
            var flowJ = JsonSerializer.SerializeToNode(flowRefs);
            var node = JsonSerializer.SerializeToNode(serializableNodes);
            var flowJ = JsonSerializer.SerializeToNode(flowRefs, opt);
            var node = JsonSerializer.SerializeToNode(serializableNodes, opt);

            // 存储元数据
            var data = new JsonArray();
@@ -352,9 +354,13 @@ public class Graph : IName, IDescription
                ["data"] = data
            };

            foreach (var single in Metadata.Select(kvp => (KeyValuePair<string, JsonNode?>[])[kvp!]))
            foreach (var kvp in Metadata)
            {
                var o = new JsonObject
                {
                data.Add(single);
                    [kvp.Key] = kvp.Value
                };
                data.Add(o);
            }

            var obj = new JsonObject
@@ -364,11 +370,13 @@ public class Graph : IName, IDescription
                ["meta"] = meta
            };

            return obj.ToJsonString(new JsonSerializerOptions
            var options = new JsonSerializerOptions(JsonSerializerOptions.Default)
            {
                WriteIndented = true,
                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
            });
            };

            return obj.ToJsonString(options);
        });
    }

@@ -379,7 +387,18 @@ public class Graph : IName, IDescription
    /// <returns>异步任务</returns>
    public async Task ExecuteAsync(CancellationToken cancellationToken = default)
    {
        // 收集所有可执行的函数节点
        // 对于链式结构,我们需要按照依赖顺序执行节点
        // 这里采用一种简单的方法:执行-传播-再执行,重复多次,直到所有节点的数据都被正确传播
        
        // 最大执行次数,防止无限循环
        const int maxIterations = 10;
        
        for (int i = 0; i < maxIterations; i++)
        {
            // 1. 传播数据,确保所有节点都能接收到输入数据
            PropagateData();
            
            // 2. 收集所有可执行的函数节点
            var functions = new List<IFunction>();
            foreach (var tree in Nodes)
            {
@@ -392,8 +411,12 @@ public class Graph : IName, IDescription
                }
            }

        // 并行执行所有函数节点
            // 3. 并行执行所有函数节点
            await Task.WhenAll(functions.Select(f => f.Execute(cancellationToken)));
            
            // 4. 再次传播数据,确保执行结果被正确传递
            PropagateData();
        }
    }

    /// <summary>
+8 −0
Original line number Diff line number Diff line
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="Nocube.UI.Controls.Editors.NodeEditors.NodeEditor">
    
</UserControl>
+131 −0
Original line number Diff line number Diff line
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dock.Model.Controls;
using Dock.Model.Core;

namespace Nocube.UI.Controls.Editors.NodeEditors;

public partial class NodeEditor : UserControl, IDocument
{
    public NodeEditor()
    {
        InitializeComponent();
    }

    public string? GetControlRecyclingId()
    {
        throw new System.NotImplementedException();
    }

    public bool OnClose()
    {
        throw new System.NotImplementedException();
    }

    public void OnSelected()
    {
        throw new System.NotImplementedException();
    }

    public void GetVisibleBounds(out double x, out double y, out double width, out double height)
    {
        throw new System.NotImplementedException();
    }

    public void SetVisibleBounds(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void OnVisibleBoundsChanged(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void GetPinnedBounds(out double x, out double y, out double width, out double height)
    {
        throw new System.NotImplementedException();
    }

    public void SetPinnedBounds(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void OnPinnedBoundsChanged(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void GetTabBounds(out double x, out double y, out double width, out double height)
    {
        throw new System.NotImplementedException();
    }

    public void SetTabBounds(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void OnTabBoundsChanged(double x, double y, double width, double height)
    {
        throw new System.NotImplementedException();
    }

    public void GetPointerPosition(out double x, out double y)
    {
        throw new System.NotImplementedException();
    }

    public void SetPointerPosition(double x, double y)
    {
        throw new System.NotImplementedException();
    }

    public void OnPointerPositionChanged(double x, double y)
    {
        throw new System.NotImplementedException();
    }

    public void GetPointerScreenPosition(out double x, out double y)
    {
        throw new System.NotImplementedException();
    }

    public void SetPointerScreenPosition(double x, double y)
    {
        throw new System.NotImplementedException();
    }

    public void OnPointerScreenPositionChanged(double x, double y)
    {
        throw new System.NotImplementedException();
    }

    public string Id { get; set; }
    public string Title { get; set; }
    public object? Context { get; set; }
    public IDockable? Owner { get; set; }
    public IDockable? OriginalOwner { get; set; }
    public IFactory? Factory { get; set; }
    public bool IsEmpty { get; set; }
    public bool IsCollapsable { get; set; }
    public double Proportion { get; set; }
    public DockMode Dock { get; set; }
    public int Column { get; set; }
    public int Row { get; set; }
    public int ColumnSpan { get; set; }
    public int RowSpan { get; set; }
    public bool IsSharedSizeScope { get; set; }
    public double CollapsedProportion { get; set; }
    public bool CanClose { get; set; }
    public bool CanPin { get; set; }
    public bool KeepPinnedDockableVisible { get; set; }
    public bool CanFloat { get; set; }
    public bool CanDrag { get; set; }
    public bool CanDrop { get; set; }
    public bool CanDockAsDocument { get; set; }
    public bool IsModified { get; set; }
    public string? DockGroup { get; set; }
}
 No newline at end of file
Loading