diff --git a/Bonsai.Core.Tests/InspectBuilderTests.cs b/Bonsai.Core.Tests/InspectBuilderTests.cs index 7979da79..4c6f3aad 100644 --- a/Bonsai.Core.Tests/InspectBuilderTests.cs +++ b/Bonsai.Core.Tests/InspectBuilderTests.cs @@ -13,7 +13,7 @@ namespace Bonsai.Core.Tests [TestClass] public class InspectBuilderTests { - void RunInspector(params ExpressionBuilder[] builders) + static void RunInspector(params ExpressionBuilder[] builders) { var workflowBuilder = new WorkflowBuilder(); var previous = default(Node); @@ -83,42 +83,85 @@ public void Build_CombinatorTransformError_ThrowsRuntimeException() [TestMethod] public void Build_GroupInspectBuilder_ReturnNestedVisualizerElement() { - var workflowBuilder = new WorkflowBuilder(); - var source = workflowBuilder.Workflow.Add(new UnitBuilder()); - var group = workflowBuilder.Workflow.Add(new GroupWorkflowBuilder()); - var groupBuilder = (GroupWorkflowBuilder)group.Value; - workflowBuilder.Workflow.AddEdge(source, group, new ExpressionBuilderArgument()); - - var input = groupBuilder.Workflow.Add(new WorkflowInputBuilder()); - var combinator = groupBuilder.Workflow.Add(new UnitBuilder()); - var output = groupBuilder.Workflow.Add(new WorkflowOutputBuilder()); - groupBuilder.Workflow.AddEdge(input, combinator, new ExpressionBuilderArgument()); - groupBuilder.Workflow.AddEdge(combinator, output, new ExpressionBuilderArgument()); - - var inspectable = workflowBuilder.Workflow.ToInspectableGraph(); - var inspectGroup = (InspectBuilder)inspectable.ElementAt(1).Value; - var result = inspectable.Build(); - var visualizerElement = ExpressionBuilder.GetVisualizerElement(inspectGroup); - Assert.AreEqual(combinator.Value, visualizerElement.Builder); + ExpressionBuilder target = null; + var workflow = new TestWorkflow() + .AppendUnit() + .AppendNested( + input => input + .AppendUnit() + .Capture(out target) + .AppendOutput(), + workflow => new GroupWorkflowBuilder(workflow)) + .ToInspectableGraph(); + workflow.Build(); + + var output = workflow[workflow.Count - 1].Value; + var visualizerElement = ExpressionBuilder.GetVisualizerElement(output); + Assert.AreSame(target, visualizerElement.Builder); } [TestMethod] public void Build_PropertyMappedInspectBuilderToWorkflowOutput_ReturnVisualizerElement() { - var workflowBuilder = new WorkflowBuilder(); - var source = workflowBuilder.Workflow.Add(new CombinatorBuilder { Combinator = new Reactive.Range() }); - var valueSource = workflowBuilder.Workflow.Add(new CombinatorBuilder { Combinator = new IntProperty() }); - var propertyMapping = workflowBuilder.Workflow.Add(new PropertyMappingBuilder { PropertyMappings = { new PropertyMapping { Name = "Count" } } }); - var output = workflowBuilder.Workflow.Add(new WorkflowOutputBuilder()); - workflowBuilder.Workflow.AddEdge(valueSource, propertyMapping, new ExpressionBuilderArgument()); - workflowBuilder.Workflow.AddEdge(propertyMapping, source, new ExpressionBuilderArgument()); - workflowBuilder.Workflow.AddEdge(source, output, new ExpressionBuilderArgument()); - - var inspectable = workflowBuilder.Workflow.ToInspectableGraph(); - var inspectOutput = (InspectBuilder)inspectable.ElementAt(workflowBuilder.Workflow.Count - 1).Value; - inspectable.Build(); - var visualizerElement = ExpressionBuilder.GetVisualizerElement(inspectOutput); - Assert.AreEqual(source.Value, visualizerElement.Builder); + ExpressionBuilder target; + var workflow = new TestWorkflow() + .AppendValue(0) + .AppendPropertyMapping(nameof(Reactive.Range.Count)) + .AppendCombinator(new Reactive.Range()) + .Capture(out target) + .AppendOutput() + .ToInspectableGraph(); + workflow.Build(); + + var output = workflow[workflow.Count - 1].Value; + var visualizerElement = ExpressionBuilder.GetVisualizerElement(output); + Assert.AreSame(target, visualizerElement.Builder); + } + + [TestMethod] + public void Build_SinkInspectBuilder_ReturnSourceVisualizerElement() + { + // related to https://github.com/bonsai-rx/bonsai/issues/1860 + var workflow = new TestWorkflow() + .AppendValue(1) + .AppendNested( + input => input + .AppendValue(string.Empty) + .AppendOutput(), + workflow => new Reactive.Sink(workflow)) + .AppendOutput() + .ToInspectableGraph(); + workflow.Build(); + + var sourceVisualizer = ExpressionBuilder.GetVisualizerElement(workflow[0].Value); + var outputVisualizer = ExpressionBuilder.GetVisualizerElement(workflow[workflow.Count - 1].Value); + Assert.AreSame(sourceVisualizer, outputVisualizer); + } + + [TestMethod] + public void Build_VisualizerInspectBuilder_ReplaceSourceVisualizerElement() + { + // related to https://github.com/bonsai-rx/bonsai/issues/1860 + var workflow = new TestWorkflow() + .AppendValue(1) + .AppendNested( + input => input + .AppendValue(string.Empty) + .AppendOutput(), + workflow => new Reactive.Visualizer(workflow)) + .AppendOutput() + .ToInspectableGraph(); + workflow.Build(); + + var sourceBuilder = (InspectBuilder)workflow[0].Value; + var outputBuilder = (InspectBuilder)workflow[workflow.Count - 1].Value; + Assert.AreEqual(typeof(int), sourceBuilder.ObservableType); + Assert.AreEqual(sourceBuilder.ObservableType, outputBuilder.ObservableType); + + var sourceVisualizer = ExpressionBuilder.GetVisualizerElement(sourceBuilder); + var outputVisualizer = ExpressionBuilder.GetVisualizerElement(outputBuilder); + Assert.AreNotSame(sourceVisualizer, outputVisualizer); + Assert.AreEqual(typeof(string), outputVisualizer.ObservableType); } #region Error Classes diff --git a/Bonsai.Core.Tests/TestWorkflow.cs b/Bonsai.Core.Tests/TestWorkflow.cs new file mode 100644 index 00000000..27a5dac1 --- /dev/null +++ b/Bonsai.Core.Tests/TestWorkflow.cs @@ -0,0 +1,101 @@ +using System; +using Bonsai.Dag; +using Bonsai.Expressions; + +namespace Bonsai.Core.Tests +{ + public readonly struct TestWorkflow + { + public TestWorkflow() + : this(new ExpressionBuilderGraph(), null) + { + } + + private TestWorkflow(ExpressionBuilderGraph workflow, Node cursor) + { + Workflow = workflow ?? throw new ArgumentNullException(nameof(workflow)); + Cursor = cursor; + } + + public ExpressionBuilderGraph Workflow { get; } + + public Node Cursor { get; } + + public TestWorkflow ResetCursor() + { + if (Cursor != null) + return new TestWorkflow(Workflow, null); + else + return this; + } + + public TestWorkflow Capture(out ExpressionBuilder builder) + { + builder = Cursor?.Value; + return this; + } + + public TestWorkflow Append(ExpressionBuilder builder) + { + var node = Workflow.Add(builder); + if (Cursor != null) + Workflow.AddEdge(Cursor, node, new ExpressionBuilderArgument()); + return new TestWorkflow(Workflow, node); + } + + public TestWorkflow AppendCombinator(TCombinator combinator) where TCombinator : new() + { + var combinatorBuilder = new CombinatorBuilder { Combinator = combinator }; + return Append(combinatorBuilder); + } + + public TestWorkflow AppendValue(TValue value) + { + var workflowProperty = new WorkflowProperty { Value = value }; + return AppendCombinator(workflowProperty); + } + + public TestWorkflow AppendUnit() + { + return Append(new UnitBuilder()); + } + + public TestWorkflow AppendInput(int index = 0) + { + return ResetCursor().Append(new WorkflowInputBuilder { Index = index }); + } + + public TestWorkflow AppendOutput() + { + return Append(new WorkflowOutputBuilder()); + } + + public TestWorkflow AppendPropertyMapping(params string[] propertyNames) + { + var mappingBuilder = new PropertyMappingBuilder(); + foreach (var name in propertyNames) + { + mappingBuilder.PropertyMappings.Add(new PropertyMapping { Name = name }); + } + return Append(mappingBuilder); + } + + public TestWorkflow AppendNested( + Func selector, + Func constructor) + where TWorkflowExpressionBuilder : ExpressionBuilder, IWorkflowExpressionBuilder + { + var nestedWorkflow = new TestWorkflow(); + if (Cursor != null) + nestedWorkflow = nestedWorkflow.AppendInput(); + nestedWorkflow = selector(nestedWorkflow); + var workflowBuilder = constructor(nestedWorkflow.Workflow); + return Append(workflowBuilder); + } + + public ExpressionBuilderGraph ToInspectableGraph() + { + return Workflow.ToInspectableGraph(); + } + } +} diff --git a/Bonsai.Core/Reactive/Sink.cs b/Bonsai.Core/Reactive/Sink.cs index 3e1bc9f1..0f517be1 100644 --- a/Bonsai.Core/Reactive/Sink.cs +++ b/Bonsai.Core/Reactive/Sink.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Xml.Serialization; using Bonsai.Expressions; +using System.Reflection; namespace Bonsai.Reactive { @@ -62,13 +63,17 @@ public override Expression Build(IEnumerable arguments) var selector = Expression.Lambda(selectorBody, selectorParameter); var selectorObservableType = selector.ReturnType.GetGenericArguments()[0]; return Expression.Call( - typeof(Sink), nameof(Process), - new [] { source.Type.GetGenericArguments()[0], selectorObservableType }, + GetProcessMethod(source.Type.GetGenericArguments()[0], selectorObservableType), source, selector); }); } - static IObservable Process(IObservable source, Func, IObservable> sink) + internal virtual MethodInfo GetProcessMethod(params Type[] typeArguments) + { + return typeof(Sink).GetMethod(nameof(Process), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(typeArguments); + } + + internal static IObservable Process(IObservable source, Func, IObservable> sink) { return source.Publish(ps => MergeDependencies(ps, sink(ps).IgnoreElements().Select(xs => default(TSource)))); } diff --git a/Bonsai.Core/Reactive/Visualizer.cs b/Bonsai.Core/Reactive/Visualizer.cs index 368f4fe7..c5f83553 100644 --- a/Bonsai.Core/Reactive/Visualizer.cs +++ b/Bonsai.Core/Reactive/Visualizer.cs @@ -1,4 +1,6 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; +using System.Reflection; using System.Xml.Serialization; using Bonsai.Expressions; @@ -33,5 +35,15 @@ public Visualizer(ExpressionBuilderGraph workflow) : base(workflow) { } + + internal override MethodInfo GetProcessMethod(params Type[] typeArguments) + { + return typeof(Visualizer).GetMethod(nameof(Process), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(typeArguments); + } + + static new IObservable Process(IObservable source, Func, IObservable> sink) + { + return Sink.Process(source, sink); + } } } diff --git a/tooling/Common.csproj.props b/tooling/Common.csproj.props index fd39c36d..6d1aa7a8 100644 --- a/tooling/Common.csproj.props +++ b/tooling/Common.csproj.props @@ -1,7 +1,7 @@ - 9.0 + 10.0 strict true