diff --git a/Bonsai.Core.Tests/CombinatorBuilderTests.cs b/Bonsai.Core.Tests/CombinatorBuilderTests.cs index c45f2a3e..3d6c4e64 100644 --- a/Bonsai.Core.Tests/CombinatorBuilderTests.cs +++ b/Bonsai.Core.Tests/CombinatorBuilderTests.cs @@ -22,8 +22,10 @@ Expression CreateObservableExpression(IObservable source) IObservable TestCombinatorBuilder(object combinator, params Expression[] arguments) { + Expression buildResult; var builder = new CombinatorBuilder { Combinator = combinator }; - var buildResult = builder.Build(arguments); + try { buildResult = builder.Build(arguments); } + catch (Exception ex) { throw new WorkflowBuildException(ex.Message, builder, ex); } var lambda = Expression.Lambda>>(buildResult); var resultFactory = lambda.Compile(); var result = resultFactory(); diff --git a/Bonsai.Core.Tests/OverloadedCombinatorBuilderTests.cs b/Bonsai.Core.Tests/OverloadedCombinatorBuilderTests.cs index 09348136..26d43259 100644 --- a/Bonsai.Core.Tests/OverloadedCombinatorBuilderTests.cs +++ b/Bonsai.Core.Tests/OverloadedCombinatorBuilderTests.cs @@ -188,8 +188,8 @@ public void Build_ListTupleOverloadedMethodCalledWithIntTuple_ReturnsIntValue() } [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public void Build_AmbiguousOverloadedMethodCalledWithIntTuple_ThrowsInvalidOperationException() + [ExpectedException(typeof(WorkflowBuildException))] + public void Build_AmbiguousOverloadedMethodCalledWithIntTuple_ThrowsWorkflowBuildException() { var value = 5; var combinator = new AmbiguousOverloadedCombinatorMock(); diff --git a/Bonsai.Core.Tests/SubjectTests.cs b/Bonsai.Core.Tests/SubjectTests.cs index 2cb7d9be..f590dc6c 100644 --- a/Bonsai.Core.Tests/SubjectTests.cs +++ b/Bonsai.Core.Tests/SubjectTests.cs @@ -9,6 +9,30 @@ namespace Bonsai.Core.Tests [TestClass] public class SubjectTests { + [Combinator] + class TypeCombinatorMock : Combinator + { + public override IObservable Process(IObservable source) + { + return source; + } + } + + [TestMethod] + [ExpectedException(typeof(WorkflowBuildException))] + public void Build_MulticastInterfaceToSubjectOfDifferentInterface_ThrowsBuildException() + { + var builder = new WorkflowBuilder(); + builder.Workflow.Add(new BehaviorSubject { Name = nameof(BehaviorSubject) }); + var source = builder.Workflow.Add(new CombinatorBuilder { Combinator = new DoubleProperty { Value = 5.5 } }); + var convert1 = builder.Workflow.Add(new CombinatorBuilder { Combinator = new TypeCombinatorMock() }); + var convert2 = builder.Workflow.Add(new MulticastSubject { Name = nameof(BehaviorSubject) }); + builder.Workflow.AddEdge(source, convert1, new ExpressionBuilderArgument()); + builder.Workflow.AddEdge(convert1, convert2, new ExpressionBuilderArgument()); + var expression = builder.Workflow.Build(); + Assert.IsNotNull(expression); + } + [TestMethod] public void ResourceSubject_SourceTerminatesExceptionally_ShouldNotTryToDispose() { diff --git a/Bonsai.Core.Tests/TypeConversionTests.cs b/Bonsai.Core.Tests/TypeConversionTests.cs new file mode 100644 index 00000000..8281a6ec --- /dev/null +++ b/Bonsai.Core.Tests/TypeConversionTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Reactive.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bonsai.Core.Tests +{ + public partial class CombinatorBuilderTests + { + [Combinator] + class TypeCombinatorMock : Combinator + { + public override IObservable Process(IObservable source) + { + return source; + } + } + + private void BuildConvertCast(TSource value, out TResult result) + { + var combinator = new TypeCombinatorMock(); + var source = CreateObservableExpression(Observable.Return(value)); + var resultProvider = TestCombinatorBuilder(combinator, source); + result = Last(resultProvider).Result; + } + + [TestMethod] + public void Build_ConvertDoubleToInt_ReturnsTruncatedValue() + { + var value = 5.5; + BuildConvertCast(value, out int result); + Assert.AreEqual((int)value, result); + } + + [TestMethod] + public void Build_ConvertObjectToImplementedInterface_ReturnsValidObject() + { + BuildConvertCast(5.5, out IComparable result); + Assert.IsNotNull(result); + } + + [TestMethod] + [ExpectedException(typeof(WorkflowBuildException))] + public void Build_ConvertObjectToNotImplementedInterface_ThrowsBuildException() + { + BuildConvertCast(5.5, out IDisposable result); + Assert.IsNotNull(result); + } + + [TestMethod] + [ExpectedException(typeof(WorkflowBuildException))] + public void Build_ConvertInterfaceToDifferentInterface_ThrowsBuildException() + { + BuildConvertCast(5.5, out IComparable result); + BuildConvertCast(result, out IDisposable disposable); + Assert.AreSame((IDisposable)result, disposable); + } + } +} diff --git a/Bonsai.Core/Expressions/MulticastSubject.cs b/Bonsai.Core/Expressions/MulticastSubject.cs index 78e541a8..44a9fe08 100644 --- a/Bonsai.Core/Expressions/MulticastSubject.cs +++ b/Bonsai.Core/Expressions/MulticastSubject.cs @@ -60,6 +60,13 @@ public override Expression Build(IEnumerable arguments) var subjectType = subjectExpression.Type.GetGenericArguments()[0]; if (observableType != subjectType) { + if (!HasConversion(observableType, subjectType)) + { + throw new InvalidOperationException( + $"No coercion operator is defined between types '{observableType}' and '{subjectType}'." + ); + } + source = CoerceMethodArgument(typeof(IObservable<>).MakeGenericType(subjectType), source); observableType = subjectType; }