From 32ea041c5e62e73c6a23a50b3e44c4227b36eea5 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:22:47 +0000 Subject: [PATCH 1/6] Added IFC app name (#648) --- Importers/Ifc/Speckle.Importers.Ifc/Import.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Importers/Ifc/Speckle.Importers.Ifc/Import.cs b/Importers/Ifc/Speckle.Importers.Ifc/Import.cs index e343fcd58..0acb11518 100644 --- a/Importers/Ifc/Speckle.Importers.Ifc/Import.cs +++ b/Importers/Ifc/Speckle.Importers.Ifc/Import.cs @@ -45,7 +45,7 @@ public static class Import public static void AddIFCImporter(this ServiceCollection serviceCollection) { - serviceCollection.AddSpeckleSdk(HostApplications.Other, HostAppVersion.v2024, "IFC-Importer"); + serviceCollection.AddSpeckleSdk(new("IFC", "ifc"), HostAppVersion.v2024, "IFC-Importer"); serviceCollection.AddSpeckleWebIfc(); serviceCollection.AddMatchingInterfacesAsTransient(Assembly.GetExecutingAssembly()); } @@ -103,7 +103,7 @@ public static class Import // 8 - Create the version (commit) using var apiClient = clientFactory.Create(account); var commit = await apiClient.Version.Create( - new CreateVersionInput(rootId, modelId, streamId, message: commitMessage) + new CreateVersionInput(rootId, modelId, streamId, message: commitMessage, sourceApplication: "IFC") ); ms = ms2; ms2 = stopwatch.ElapsedMilliseconds; From b807be35ff54c7c54729f4b6d4fc5a75b6fca576 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 7 Mar 2025 13:27:58 +0000 Subject: [PATCH 2/6] test: add tests for threadcontext (#651) * add tests for threadcontext * add test for extensions * remove needed usage --- .../Bridge/BrowserBridge.cs | 4 - .../Speckle.Connectors.Common.Tests.csproj | 3 +- .../Threading/Funcs.cs | 18 +++ .../Threading/TestThreadContext.cs | 103 ++++++++++++++++++ .../Threading/ThreadContextExtensionTests.cs | 93 ++++++++++++++++ .../Threading/ThreadContextTests.cs | 93 ++++++++++++++++ .../packages.lock.json | 37 ++++--- .../Threading/DefaultThreadContext.cs | 3 + .../Threading/ThreadContext.cs | 28 +++-- .../Threading/ThreadOptions.cs | 11 -- 10 files changed, 351 insertions(+), 42 deletions(-) create mode 100644 Sdk/Speckle.Connectors.Common.Tests/Threading/Funcs.cs create mode 100644 Sdk/Speckle.Connectors.Common.Tests/Threading/TestThreadContext.cs create mode 100644 Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextExtensionTests.cs create mode 100644 Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextTests.cs delete mode 100644 Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs diff --git a/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs b/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs index aecf20211..228775334 100644 --- a/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs +++ b/DUI3/Speckle.Connectors.DUI/Bridge/BrowserBridge.cs @@ -32,7 +32,6 @@ public sealed class BrowserBridge : IBrowserBridge private readonly ITopLevelExceptionHandler _topLevelExceptionHandler; private readonly IThreadContext _threadContext; - private readonly IThreadOptions _threadOptions; private readonly IBrowserScriptExecutor _browserScriptExecutor; private readonly IJsonSerializer _jsonSerializer; @@ -64,16 +63,13 @@ public sealed class BrowserBridge : IBrowserBridge IJsonSerializer jsonSerializer, ILogger logger, IBrowserScriptExecutor browserScriptExecutor, - IThreadOptions threadOptions, ITopLevelExceptionHandler topLevelExceptionHandler ) { _threadContext = threadContext; _jsonSerializer = jsonSerializer; _logger = logger; - // Capture the main thread's SynchronizationContext _browserScriptExecutor = browserScriptExecutor; - _threadOptions = threadOptions; _topLevelExceptionHandler = topLevelExceptionHandler; } diff --git a/Sdk/Speckle.Connectors.Common.Tests/Speckle.Connectors.Common.Tests.csproj b/Sdk/Speckle.Connectors.Common.Tests/Speckle.Connectors.Common.Tests.csproj index 9454563b8..965d2164b 100644 --- a/Sdk/Speckle.Connectors.Common.Tests/Speckle.Connectors.Common.Tests.csproj +++ b/Sdk/Speckle.Connectors.Common.Tests/Speckle.Connectors.Common.Tests.csproj @@ -11,13 +11,12 @@ - - + diff --git a/Sdk/Speckle.Connectors.Common.Tests/Threading/Funcs.cs b/Sdk/Speckle.Connectors.Common.Tests/Threading/Funcs.cs new file mode 100644 index 000000000..58c0b41a9 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common.Tests/Threading/Funcs.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Speckle.Connectors.Common.Tests.Threading; + +[SuppressMessage("Design", "CA1008:Enums should have zero value")] +public enum Funcs +{ + RunMain, + RunWorker, + RunMainAsync, + RunWorkerAsync, + WorkerToMainAsync, + MainToWorker, + WorkerToMain, + MainToWorkerAsync, + RunMainAsync_T, + RunWorkerAsync_T +} diff --git a/Sdk/Speckle.Connectors.Common.Tests/Threading/TestThreadContext.cs b/Sdk/Speckle.Connectors.Common.Tests/Threading/TestThreadContext.cs new file mode 100644 index 000000000..d3d39ebe7 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common.Tests/Threading/TestThreadContext.cs @@ -0,0 +1,103 @@ +using FluentAssertions; +using Speckle.Connectors.Common.Threading; + +namespace Speckle.Connectors.Common.Tests.Threading; + +public class TestThreadContext(bool isMain) : ThreadContext +{ + public override bool IsMainThread => isMain; + + public Funcs? Func { get; set; } + + protected override void RunMain(Action action) + { + action(); + Func.Should().BeNull(); + Func = Funcs.RunMain; + } + + protected override void RunWorker(Action action) + { + action(); + Func.Should().BeNull(); + Func = Funcs.RunWorker; + } + + protected override async Task WorkerToMainAsync(Func> action) + { + var x = await action(); + Func.Should().BeNull(); + Func = Funcs.WorkerToMainAsync; + return x; + } + + protected override async Task MainToWorkerAsync(Func> action) + { + var x = await action(); + Func.Should().BeNull(); + Func = Funcs.MainToWorkerAsync; + return x; + } + + protected override Task WorkerToMain(Func action) + { + var x = action(); + Func.Should().BeNull(); + Func = Funcs.WorkerToMain; + return Task.FromResult(x); + } + + protected override Task MainToWorker(Func action) + { + var x = action(); + Func.Should().BeNull(); + Func = Funcs.MainToWorker; + return Task.FromResult(x); + } + + protected override Task RunMainAsync(Func action) + { + var x = action(); + Func.Should().BeNull(); + Func = Funcs.RunMainAsync_T; + return Task.FromResult(x); + } + + protected override Task RunWorkerAsync(Func action) + { + var x = action(); + Func.Should().BeNull(); + Func = Funcs.RunWorkerAsync_T; + return Task.FromResult(x); + } + + protected override async Task RunMainAsync(Func action) + { + await action(); + Func.Should().BeNull(); + Func = Funcs.RunMainAsync; + } + + protected override async Task RunWorkerAsync(Func action) + { + await action(); + Func.Should().BeNull(); + Func = Funcs.RunWorkerAsync; + } + + protected override async Task RunMainAsync(Func> action) + { + var x = await action(); + Func.Should().BeNull(); + Func = Funcs.RunMainAsync_T; + return x; + } + + protected override async Task RunWorkerAsync(Func> action) + { + var x = await action(); + Func.Should().BeNull(); + Func = Funcs.RunWorkerAsync_T; + return x; + } +} diff --git a/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextExtensionTests.cs b/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextExtensionTests.cs new file mode 100644 index 000000000..3ca61ec49 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextExtensionTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Speckle.Connectors.Common.Threading; +using Speckle.Testing; + +namespace Speckle.Connectors.Common.Tests.Threading; + +public class ThreadContextExtensionTests : MoqTest +{ + [Test] + public async Task RunOnMain() + { + Action a = () => { }; + var tc = Create(); + tc.Setup(x => x.RunOnThread(a, true)).Returns(Task.CompletedTask); + await tc.Object.RunOnMain(a); + } + + [Test] + public async Task RunOnWorker() + { + Action a = () => { }; + var tc = Create(); + tc.Setup(x => x.RunOnThread(a, false)).Returns(Task.CompletedTask); + await tc.Object.RunOnWorker(a); + } + + [Test] + public async Task RunOnMain_T() + { + Func a = () => true; + var tc = Create(); + tc.Setup(x => x.RunOnThread(a, true)).ReturnsAsync(true); + (await tc.Object.RunOnMain(a)).Should().BeTrue(); + } + + [Test] + public async Task RunOnWorker_T() + { + Func a = () => true; + var tc = Create(); + tc.Setup(x => x.RunOnThread(a, false)).ReturnsAsync(true); + (await tc.Object.RunOnWorker(a)).Should().BeTrue(); + } + + [Test] + public async Task RunOnMainAsync() + { + Func a = () => Task.CompletedTask; + var tc = Create(); + tc.Setup(x => x.RunOnThreadAsync(a, true)).Returns(Task.CompletedTask); + await tc.Object.RunOnMainAsync(a); + } + + [Test] + public async Task RunOnWorkerAsync() + { + Func a = () => Task.CompletedTask; + var tc = Create(); + tc.Setup(x => x.RunOnThreadAsync(a, false)).Returns(Task.CompletedTask); + await tc.Object.RunOnWorkerAsync(a); + } + + [Test] + public async Task RunOnMainAsync_T() + { + Func> a = () => Task.FromResult(true); + var tc = Create(); + tc.Setup(x => x.RunOnThreadAsync(a, true)).ReturnsAsync(true); + (await tc.Object.RunOnMainAsync(a)).Should().BeTrue(); + } + + [Test] + public async Task RunOnWorkerAsync_T() + { + Func> a = () => Task.FromResult(true); + var tc = Create(); + tc.Setup(x => x.RunOnThreadAsync(a, false)).ReturnsAsync(true); + await tc.Object.RunOnWorkerAsync(a); + (await tc.Object.RunOnWorkerAsync(a)).Should().BeTrue(); + } + + [Test] +#pragma warning disable CA1030 + public async Task FireAndForget() +#pragma warning restore CA1030 + { + //kind of does nothing, just making sure there's no error + Task.CompletedTask.FireAndForget(); + await Task.Delay(500); + } +} diff --git a/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextTests.cs b/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextTests.cs new file mode 100644 index 000000000..48b2a69e3 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common.Tests/Threading/ThreadContextTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using NUnit.Framework; +using Speckle.Testing; + +namespace Speckle.Connectors.Common.Tests.Threading; + +public class ThreadContextTests : MoqTest +{ + [Test] + [TestCase(true, true, Funcs.RunMain)] + [TestCase(true, false, Funcs.MainToWorker)] + [TestCase(false, true, Funcs.WorkerToMain)] + [TestCase(false, false, Funcs.RunWorker)] + public async Task RunOnThread(bool isMain, bool useMain, Funcs? result) + { + var tc = new TestThreadContext(isMain); + bool resultRan = false; + await tc.RunOnThread( + () => + { + resultRan = true; + }, + useMain + ); + resultRan.Should().BeTrue(); + tc.Func.Should().Be(result); + } + + [Test] + [TestCase(true, true, Funcs.RunMainAsync_T)] + [TestCase(true, false, Funcs.MainToWorker)] + [TestCase(false, true, Funcs.WorkerToMain)] + [TestCase(false, false, Funcs.RunWorkerAsync_T)] + public async Task RunOnThread_T(bool isMain, bool useMain, Funcs? result) + { + var tc = new TestThreadContext(isMain); + bool resultRan = false; + var x = await tc.RunOnThread( + () => + { + resultRan = true; + return false; + }, + useMain + ); + resultRan.Should().BeTrue(); + x.Should().BeFalse(); + tc.Func.Should().Be(result); + } + + [Test] + [TestCase(true, true, Funcs.RunMainAsync)] + [TestCase(true, false, Funcs.MainToWorkerAsync)] + [TestCase(false, true, Funcs.WorkerToMainAsync)] + [TestCase(false, false, Funcs.RunWorkerAsync)] + public async Task RunOnThreadAsync(bool isMain, bool useMain, Funcs? result) + { + var tc = new TestThreadContext(isMain); + bool resultRan = false; + await tc.RunOnThreadAsync( + () => + { + resultRan = true; + return Task.CompletedTask; + }, + useMain + ); + resultRan.Should().BeTrue(); + tc.Func.Should().Be(result); + } + + [Test] + [TestCase(true, true, Funcs.RunMainAsync_T)] + [TestCase(true, false, Funcs.MainToWorkerAsync)] + [TestCase(false, true, Funcs.WorkerToMainAsync)] + [TestCase(false, false, Funcs.RunWorkerAsync_T)] + public async Task RunOnThreadAsync_T(bool isMain, bool useMain, Funcs? result) + { + var tc = new TestThreadContext(isMain); + bool resultRan = false; + var x = await tc.RunOnThreadAsync( + () => + { + resultRan = true; + return Task.FromResult(false); + }, + useMain + ); + resultRan.Should().BeTrue(); + x.Should().BeFalse(); + tc.Func.Should().Be(result); + } +} diff --git a/Sdk/Speckle.Connectors.Common.Tests/packages.lock.json b/Sdk/Speckle.Connectors.Common.Tests/packages.lock.json index 392e9d161..215f61a10 100644 --- a/Sdk/Speckle.Connectors.Common.Tests/packages.lock.json +++ b/Sdk/Speckle.Connectors.Common.Tests/packages.lock.json @@ -55,21 +55,6 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, - "Moq": { - "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", - "dependencies": { - "Castle.Core": "5.1.1" - } - }, - "NUnit": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "MT/DpAhjtiytzhTgTqIhBuWx4y26PKfDepYUHUM+5uv4TsryHC2jwFo5e6NhWkApCm/G6kZ80dRjdJFuAxq3rg==" - }, "NUnit.Analyzers": { "type": "Direct", "requested": "[4.2.0, )", @@ -336,6 +321,13 @@ "speckle.connectors.logging": { "type": "Project" }, + "speckle.testing": { + "type": "Project", + "dependencies": { + "Moq": "[4.20.70, )", + "NUnit": "[4.1.0, )" + } + }, "Microsoft.Extensions.Logging": { "type": "CentralTransitive", "requested": "[2.2.0, )", @@ -354,6 +346,21 @@ "resolved": "2.2.0", "contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==" }, + "Moq": { + "type": "CentralTransitive", + "requested": "[4.20.70, )", + "resolved": "4.20.70", + "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "NUnit": { + "type": "CentralTransitive", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "MT/DpAhjtiytzhTgTqIhBuWx4y26PKfDepYUHUM+5uv4TsryHC2jwFo5e6NhWkApCm/G6kZ80dRjdJFuAxq3rg==" + }, "Speckle.DoubleNumerics": { "type": "CentralTransitive", "requested": "[4.1.0, )", diff --git a/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs b/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs index 92c22902e..92a6dc8a3 100644 --- a/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs +++ b/Sdk/Speckle.Connectors.Common/Threading/DefaultThreadContext.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace Speckle.Connectors.Common.Threading; +[ExcludeFromCodeCoverage] public class DefaultThreadContext : ThreadContext { //should be always newed up on the host app's main thread diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs index 8a6deec78..644568f67 100644 --- a/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs +++ b/Sdk/Speckle.Connectors.Common/Threading/ThreadContext.cs @@ -6,7 +6,7 @@ namespace Speckle.Connectors.Common.Threading; public abstract class ThreadContext : IThreadContext { private static readonly Task s_empty = Task.FromResult(null); - public static bool IsMainThread => Environment.CurrentManagedThreadId == 1; + public virtual bool IsMainThread => Environment.CurrentManagedThreadId == 1; public async Task RunOnThread(Action action, bool useMain) { @@ -18,7 +18,7 @@ public abstract class ThreadContext : IThreadContext } else { - await WorkerToMainAsync(() => + await WorkerToMain(() => { action(); return s_empty; @@ -29,7 +29,7 @@ public abstract class ThreadContext : IThreadContext { if (IsMainThread) { - await MainToWorkerAsync(() => + await MainToWorker(() => { action(); return s_empty; @@ -37,12 +37,12 @@ public abstract class ThreadContext : IThreadContext } else { - RunMain(action); + RunWorker(action); } } } - public virtual Task RunOnThread(Func action, bool useMain) + public Task RunOnThread(Func action, bool useMain) { if (useMain) { @@ -58,7 +58,7 @@ public abstract class ThreadContext : IThreadContext return MainToWorker(action); } - return RunMainAsync(action); + return RunWorkerAsync(action); } public async Task RunOnThreadAsync(Func action, bool useMain) @@ -74,7 +74,7 @@ public abstract class ThreadContext : IThreadContext await WorkerToMainAsync(async () => { await action(); - return Task.FromResult(null); + return s_empty; }); } } @@ -85,7 +85,7 @@ public abstract class ThreadContext : IThreadContext await MainToWorkerAsync(async () => { await action(); - return Task.FromResult(null); + return s_empty; }); } else @@ -96,7 +96,7 @@ public abstract class ThreadContext : IThreadContext } else { - await action(); + await RunWorkerAsync(action); } } } @@ -117,7 +117,7 @@ public abstract class ThreadContext : IThreadContext { return MainToWorkerAsync(action); } - return RunMainAsync(action); + return RunWorkerAsync(action); } protected abstract Task WorkerToMainAsync(Func> action); @@ -129,9 +129,17 @@ public abstract class ThreadContext : IThreadContext protected virtual void RunMain(Action action) => action(); + protected virtual void RunWorker(Action action) => action(); + protected virtual Task RunMainAsync(Func action) => Task.FromResult(action()); + protected virtual Task RunWorkerAsync(Func action) => Task.FromResult(action()); + protected virtual Task RunMainAsync(Func action) => Task.FromResult(action()); + protected virtual Task RunWorkerAsync(Func action) => Task.FromResult(action()); + protected virtual Task RunMainAsync(Func> action) => action(); + + protected virtual Task RunWorkerAsync(Func> action) => action(); } diff --git a/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs b/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs deleted file mode 100644 index 38ead87e7..000000000 --- a/Sdk/Speckle.Connectors.Common/Threading/ThreadOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Speckle.InterfaceGenerator; -using Speckle.Sdk; -using Speckle.Sdk.Host; - -namespace Speckle.Connectors.Common.Threading; - -[GenerateAutoInterface] -public class ThreadOptions(ISpeckleApplication speckleApplication) : IThreadOptions -{ - public bool RunReceiveBuildOnMainThread => speckleApplication.HostApplication != HostApplications.Rhino.Name; -} From 63c4d31467688be6b5db91e7434e0dfde16117b3 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 7 Mar 2025 13:32:28 +0000 Subject: [PATCH 3/6] fix for looking for model store (#654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com> --- DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs b/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs index 3548dc90b..dda4f8df4 100644 --- a/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs +++ b/DUI3/Speckle.Connectors.DUI/Models/DocumentModelStore.cs @@ -42,8 +42,11 @@ public abstract class DocumentModelStore(IJsonSerializer serializer) // In theory this should never really happen, but if it does public ModelCard GetModelById(string id) { - var model = _models.First(model => model.ModelCardId == id) ?? throw new ModelNotFoundException(); - return model; + lock (_models) + { + var model = _models.FirstOrDefault(model => model.ModelCardId == id) ?? throw new ModelNotFoundException(); + return model; + } } public void AddModel(ModelCard model) From 4aed602089ffa293e6c74877ae626ff83b142212 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 7 Mar 2025 13:44:21 +0000 Subject: [PATCH 4/6] test: Send Operation tests (#652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add tests for threadcontext * add test for extensions * remove needed usage * move cancellation * Only add send operation tests --------- Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com> --- .../CancellationManagerTests.cs | 2 +- .../Operations/SendOperationTests.cs | 131 ++++++++++++++++++ .../Operations/AccountService.cs | 6 +- .../Operations/SendOperation.cs | 20 +-- .../SendOperationVersionRecorder.cs | 27 ++++ 5 files changed, 167 insertions(+), 19 deletions(-) rename Sdk/Speckle.Connectors.Common.Tests/{ => Cancellation}/CancellationManagerTests.cs (98%) create mode 100644 Sdk/Speckle.Connectors.Common.Tests/Operations/SendOperationTests.cs create mode 100644 Sdk/Speckle.Connectors.Common/Operations/SendOperationVersionRecorder.cs diff --git a/Sdk/Speckle.Connectors.Common.Tests/CancellationManagerTests.cs b/Sdk/Speckle.Connectors.Common.Tests/Cancellation/CancellationManagerTests.cs similarity index 98% rename from Sdk/Speckle.Connectors.Common.Tests/CancellationManagerTests.cs rename to Sdk/Speckle.Connectors.Common.Tests/Cancellation/CancellationManagerTests.cs index 06aa34bd1..034d57093 100644 --- a/Sdk/Speckle.Connectors.Common.Tests/CancellationManagerTests.cs +++ b/Sdk/Speckle.Connectors.Common.Tests/Cancellation/CancellationManagerTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Speckle.Connectors.Common.Cancellation; -namespace Speckle.Connectors.Common.Tests; +namespace Speckle.Connectors.Common.Tests.Cancellation; public class CancellationManagerTests { diff --git a/Sdk/Speckle.Connectors.Common.Tests/Operations/SendOperationTests.cs b/Sdk/Speckle.Connectors.Common.Tests/Operations/SendOperationTests.cs new file mode 100644 index 000000000..4d6abd3f8 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common.Tests/Operations/SendOperationTests.cs @@ -0,0 +1,131 @@ +using System.Reflection; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Speckle.Connectors.Common.Builders; +using Speckle.Connectors.Common.Caching; +using Speckle.Connectors.Common.Conversion; +using Speckle.Connectors.Common.Operations; +using Speckle.Connectors.Common.Threading; +using Speckle.Sdk.Api; +using Speckle.Sdk.Credentials; +using Speckle.Sdk.Host; +using Speckle.Sdk.Logging; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; +using Speckle.Sdk.Serialisation.V2.Send; +using Speckle.Testing; + +namespace Speckle.Connectors.Common.Tests.Operations; + +public class SendOperationTests : MoqTest +{ +#pragma warning disable CA1034 + [SpeckleType("TestBase")] + public class TestBase : Base; +#pragma warning restore CA1034 + [Test] +#pragma warning disable CA1506 + public async Task Execute() +#pragma warning restore CA1506 + { + TypeLoader.Reset(); + TypeLoader.Initialize(Assembly.GetExecutingAssembly()); + var rootObjectBuilder = Create>(); + var sendConversionCache = Create(); + var accountService = Create(); + var sendProgress = Create(); + var operations = Create(); + var sendOperationVersionRecorder = Create(); + var activityFactory = Create(); + var threadContext = Create(); + + var ct = new CancellationToken(); + var objects = new List(); + var sendInfo = new SendInfo(string.Empty, new Uri("https://localhost"), string.Empty, string.Empty, string.Empty); + var progress = Create>(); + + var conversionResults = new List(); + var rootResult = new RootObjectBuilderResult(new TestBase(), conversionResults); + rootObjectBuilder.Setup(x => x.Build(objects, sendInfo, progress.Object, ct)).ReturnsAsync(rootResult); + + var rootId = "rootId"; + var refs = new Dictionary(); + var serializeProcessResults = new SerializeProcessResults(rootId, refs); + threadContext + .Setup(x => x.RunOnThreadAsync(It.IsAny>>(), false)) + .ReturnsAsync(serializeProcessResults); + + var sendOperation = new SendOperation( + rootObjectBuilder.Object, + sendConversionCache.Object, + accountService.Object, + sendProgress.Object, + operations.Object, + sendOperationVersionRecorder.Object, + activityFactory.Object, + threadContext.Object + ); + var result = await sendOperation.Execute(objects, sendInfo, progress.Object, ct); + result.Should().NotBeNull(); + rootResult.RootObject["version"].Should().Be(3); + result.RootObjId.Should().Be(rootId); + result.ConvertedReferences.Should().BeSameAs(refs); + result.ConversionResults.Should().BeSameAs(conversionResults); + } + + [Test] + public async Task Send() + { + TypeLoader.Reset(); + TypeLoader.Initialize(Assembly.GetExecutingAssembly()); + var rootObjectBuilder = Create>(); + var sendConversionCache = Create(); + var accountService = Create(); + var sendProgress = Create(); + var operations = Create(); + var sendOperationVersionRecorder = Create(); + var activityFactory = Create(); + var threadContext = Create(); + + var commitObject = new TestBase(); + var projectId = "projectId"; + var modelId = "modelId"; + var accountId = "accountId"; + var url = new Uri("https://localhost"); + var sendInfo = new SendInfo(accountId, url, projectId, modelId, string.Empty); + var progress = Create>(MockBehavior.Loose); + + var ct = new CancellationToken(); + + var token = "token"; + var account = new Account() { token = token }; + var rootId = "rootId"; + var refs = new Dictionary(); + var serializeProcessResults = new SerializeProcessResults(rootId, refs); + accountService.Setup(x => x.GetAccountWithServerUrlFallback(accountId, url)).Returns(account); + activityFactory.Setup(x => x.Start("SendOperation", "Send")).Returns((ISdkActivity?)null); + + operations + .Setup(x => x.Send2(url, projectId, token, commitObject, It.IsAny(), ct)) + .ReturnsAsync(serializeProcessResults); + + sendConversionCache.Setup(x => x.StoreSendResult(projectId, refs)); + sendProgress.Setup(x => x.Begin()); + + sendOperationVersionRecorder.Setup(x => x.RecordVersion(rootId, sendInfo, account, ct)).Returns(Task.CompletedTask); + + var sendOperation = new SendOperation( + rootObjectBuilder.Object, + sendConversionCache.Object, + accountService.Object, + sendProgress.Object, + operations.Object, + sendOperationVersionRecorder.Object, + activityFactory.Object, + threadContext.Object + ); + var result = await sendOperation.Send(commitObject, sendInfo, progress.Object, ct); + result.Should().Be(serializeProcessResults); + } +} diff --git a/Sdk/Speckle.Connectors.Common/Operations/AccountService.cs b/Sdk/Speckle.Connectors.Common/Operations/AccountService.cs index 665d24c2d..6e1986143 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/AccountService.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/AccountService.cs @@ -1,4 +1,5 @@ -using Speckle.Sdk.Credentials; +using Speckle.InterfaceGenerator; +using Speckle.Sdk.Credentials; namespace Speckle.Connectors.Common.Operations; @@ -7,7 +8,8 @@ namespace Speckle.Connectors.Common.Operations; /// Note: Be sure it is registered on refactorings. Otherwise, we won't be able to do any send/receive ops. /// This can safely be registered as singleton. /// -public class AccountService(IAccountManager accountManager) +[GenerateAutoInterface] +public class AccountService(IAccountManager accountManager) : IAccountService { /// /// Account to retrieve with its id, if not exist try to retrieve from matching serverUrl. diff --git a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs index 840e29da7..d1baf6fae 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/SendOperation.cs @@ -4,7 +4,6 @@ using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Threading; using Speckle.Connectors.Logging; using Speckle.Sdk.Api; -using Speckle.Sdk.Api.GraphQL.Inputs; using Speckle.Sdk.Credentials; using Speckle.Sdk.Logging; using Speckle.Sdk.Models; @@ -16,10 +15,10 @@ namespace Speckle.Connectors.Common.Operations; public sealed class SendOperation( IRootObjectBuilder rootObjectBuilder, ISendConversionCache sendConversionCache, - AccountService accountService, + IAccountService accountService, ISendProgress sendProgress, IOperations operations, - IClientFactory clientFactory, + ISendOperationVersionRecorder sendOperationVersionRecorder, ISdkActivityFactory activityFactory, IThreadContext threadContext ) @@ -49,7 +48,7 @@ public sealed class SendOperation( return new(rootObjId, convertedReferences, buildResult.ConversionResults); } - private async Task Send( + public async Task Send( Base commitObject, SendInfo sendInfo, IProgress onOperationProgressed, @@ -81,18 +80,7 @@ public sealed class SendOperation( onOperationProgressed.Report(new("Linking version to model...", null)); // 8 - Create the version (commit) - using var apiClient = clientFactory.Create(account); - _ = await apiClient - .Version.Create( - new CreateVersionInput( - sendResult.RootId, - sendInfo.ModelId, - sendInfo.ProjectId, - sourceApplication: sendInfo.SourceApplication - ), - ct - ) - .ConfigureAwait(true); + await sendOperationVersionRecorder.RecordVersion(sendResult.RootId, sendInfo, account, ct); return sendResult; } diff --git a/Sdk/Speckle.Connectors.Common/Operations/SendOperationVersionRecorder.cs b/Sdk/Speckle.Connectors.Common/Operations/SendOperationVersionRecorder.cs new file mode 100644 index 000000000..946ad9f31 --- /dev/null +++ b/Sdk/Speckle.Connectors.Common/Operations/SendOperationVersionRecorder.cs @@ -0,0 +1,27 @@ +using Speckle.InterfaceGenerator; +using Speckle.Sdk.Api; +using Speckle.Sdk.Api.GraphQL.Inputs; +using Speckle.Sdk.Credentials; + +namespace Speckle.Connectors.Common.Operations; + +[GenerateAutoInterface] +//this is unnecessary if IClientFactory.Create returned an interface +public class SendOperationVersionRecorder(IClientFactory clientFactory) : ISendOperationVersionRecorder +{ + public async Task RecordVersion(string rootId, SendInfo sendInfo, Account account, CancellationToken ct) + { + using var apiClient = clientFactory.Create(account); + _ = await apiClient + .Version.Create( + new CreateVersionInput( + rootId, + sendInfo.ModelId, + sendInfo.ProjectId, + sourceApplication: sendInfo.SourceApplication + ), + ct + ) + .ConfigureAwait(true); + } +} From f152cff619cf64f76f5dff5546678a8f5cc32260 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Fri, 7 Mar 2025 14:33:15 +0000 Subject: [PATCH 5/6] chore (autocad/civil): add regen after receive (#650) * regenerates doc * moves regen to receive base binding * Update AutocadReceiveBaseBinding.cs --- .../Bindings/AutocadReceiveBaseBinding.cs | 6 ++++++ .../Operations/Receive/AutocadHostObjectBuilder.cs | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadReceiveBaseBinding.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadReceiveBaseBinding.cs index 0bfb492dc..eb58a6783 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadReceiveBaseBinding.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Bindings/AutocadReceiveBaseBinding.cs @@ -108,6 +108,12 @@ public abstract class AutocadReceiveBaseBinding : IReceiveBinding { // reenable document activation Application.DocumentManager.DocumentActivationEnabled = true; + + // regenerate doc to flush graphics, sometimes some objects (ellipses, nurbs curves) do not appear fully visible after receive. + // Adding a regen (must be run on main thread) here, but it doesn't seem to work: + // it's run on main thread, tried sending the "regen" string to execute, also tried regen after every object bake, but still can't fix. + // the objects should appear visible if you manually call the "regen" command after the operation finishes, or click on a view on the view cube which also calls regen. + Application.DocumentManager.CurrentDocument.Editor.Regen(); } } } diff --git a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs index fed149c01..6cba21874 100644 --- a/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs +++ b/Connectors/Autocad/Speckle.Connectors.AutocadShared/Operations/Receive/AutocadHostObjectBuilder.cs @@ -83,7 +83,7 @@ public class AutocadHostObjectBuilder( colorBaker.ParseColors(unpackedRoot.ColorProxies, onOperationProgressed); } - // 5 - Convert atomic objects + // 4 - Convert atomic objects HashSet results = new(); HashSet bakedObjectIds = new(); Dictionary> applicationIdMap = new(); @@ -116,7 +116,7 @@ public class AutocadHostObjectBuilder( } } - // 6 - Convert instances + // 5 - Convert instances var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = instanceBaker.BakeInstances( instanceComponentsWithPath, applicationIdMap, @@ -129,7 +129,7 @@ public class AutocadHostObjectBuilder( results.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId)); results.UnionWith(instanceConversionResults); - // 7 - Create groups + // 6 - Create groups if (unpackedRoot.GroupProxies != null) { IReadOnlyCollection groupResults = groupBaker.CreateGroups( From d71b36c2f7824d294547130b48203f1e4263b72b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 9 Mar 2025 17:42:22 +0000 Subject: [PATCH 6/6] Sped up Rhino receive hot spots (#596) * Sped up Rhino receive hot spots * formatting --------- Co-authored-by: Claire Kuang --- .../Raw/FlatPointListToHostConverter.cs | 18 ++-- .../ToHost/Raw/MeshToHostConverter.cs | 99 +++++++++++-------- 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/FlatPointListToHostConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/FlatPointListToHostConverter.cs index 5dd995966..501018160 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/FlatPointListToHostConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/FlatPointListToHostConverter.cs @@ -5,10 +5,15 @@ using Speckle.Sdk.Common.Exceptions; namespace Speckle.Converters.Rhino.ToHost.Raw; +public interface IFlatPointListToHostConverter : ITypedConverter, Point3dList> +{ + IEnumerable ConvertToEnum(IReadOnlyList target); +} + /// /// Converts a flat list of raw double values to a Point3dList. /// -public class FlatPointListToHostConverter : ITypedConverter, Point3dList> +public class FlatPointListToHostConverter : IFlatPointListToHostConverter { /// /// Converts a flat list of raw double values to a Point3dList. @@ -20,20 +25,19 @@ public class FlatPointListToHostConverter : ITypedConverter /// Throws when the input list count is not a multiple of 3. - public Point3dList Convert(IReadOnlyList target) + public Point3dList Convert(IReadOnlyList target) => new(ConvertToEnum(target)); + + //avoids temporary collection by using this when necessary + public IEnumerable ConvertToEnum(IReadOnlyList target) { if (target.Count % 3 != 0) { throw new ValidationException("Array malformed: length%3 != 0."); } - var points = new List(target.Count / 3); - for (int i = 2; i < target.Count; i += 3) { - points.Add(new RG.Point3d(target[i - 2], target[i - 1], target[i])); + yield return new RG.Point3d(target[i - 2], target[i - 1], target[i]); } - - return new Point3dList(points); } } diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/MeshToHostConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/MeshToHostConverter.cs index 7d1fb8ee8..389d4fe48 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/MeshToHostConverter.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToHost/Raw/MeshToHostConverter.cs @@ -1,20 +1,12 @@ -using System.Drawing; -using Rhino.Collections; +using System.Drawing; using Speckle.Converters.Common.Objects; using Speckle.Objects.Utils; using Speckle.Sdk; namespace Speckle.Converters.Rhino.ToHost.Raw; -public class MeshToHostConverter : ITypedConverter +public class MeshToHostConverter(IFlatPointListToHostConverter pointListConverter) : ITypedConverter { - private readonly ITypedConverter, Point3dList> _pointListConverter; - - public MeshToHostConverter(ITypedConverter, Point3dList> pointListConverter) - { - _pointListConverter = pointListConverter; - } - /// /// Converts a Speckle mesh object to a Rhino mesh object. /// @@ -25,26 +17,35 @@ public class MeshToHostConverter : ITypedConverter { RG.Mesh m = new(); - var vertices = _pointListConverter.Convert(target.vertices); - var colors = ConvertVertexColors(target.colors); - var vertexNormals = ConvertVertexNormals(target.vertexNormals); - var textureCoordinates = ConvertTextureCoordinates(target.textureCoordinates); + var vertices = pointListConverter.ConvertToEnum(target.vertices); m.Vertices.AddVertices(vertices); - if (colors.Length != 0 && !m.VertexColors.SetColors(colors)) + if (target.colors.Count != 0) { - throw new SpeckleException("Failed to set Vertex Colors"); + var colors = ConvertVertexColors(target.colors); + if (!m.VertexColors.SetColors(colors)) + { + throw new SpeckleException("Failed to set Vertex Colors"); + } } - if (vertexNormals.Length != 0 && !m.Normals.SetNormals(vertexNormals)) + if (target.vertexNormals.Count != 0) { - throw new SpeckleException("Failed to set Vertex Normals"); + var vertexNormals = ConvertVertexNormals(target.vertexNormals); + if (!m.Normals.SetNormals(vertexNormals)) + { + throw new SpeckleException("Failed to set Vertex Normals"); + } } - if (textureCoordinates.Length != 0 && !m.TextureCoordinates.SetTextureCoordinates(textureCoordinates)) + if (target.textureCoordinates.Count != 0) { - throw new SpeckleException("Failed to set Texture Coordinates"); + var textureCoordinates = ConvertTextureCoordinates(target.textureCoordinates); + if (!m.TextureCoordinates.SetTextureCoordinates(textureCoordinates)) + { + throw new SpeckleException("Failed to set Texture Coordinates"); + } } AssignMeshFaces(target, m); @@ -60,6 +61,7 @@ public class MeshToHostConverter : ITypedConverter while (i < target.faces.Count) { int n = target.faces[i]; + // For backwards compatibility. Old meshes will have "0" for triangle face, "1" for quad face. // Newer meshes have "3" for triangle face, "4" for quad" face and "5...n" for n-gon face. if (n < 3) @@ -67,32 +69,25 @@ public class MeshToHostConverter : ITypedConverter n += 3; // 0 -> 3, 1 -> 4 } - if (n == 3) + switch (n) { - // triangle - m.Faces.AddFace(new RG.MeshFace(target.faces[i + 1], target.faces[i + 2], target.faces[i + 3])); - } - else if (n == 4) - { - // quad - m.Faces.AddFace( - new RG.MeshFace(target.faces[i + 1], target.faces[i + 2], target.faces[i + 3], target.faces[i + 4]) - ); - } - else - { - // n-gon - var triangles = MeshTriangulationHelper.TriangulateFace(i, target, false); - - var faceIndices = new List(triangles.Count); - for (int t = 0; t < triangles.Count; t += 3) + case 3: + // triangle + m.Faces.AddFace(target.faces[i + 1], target.faces[i + 2], target.faces[i + 3]); + break; + case 4: + // quad + m.Faces.AddFace(target.faces[i + 1], target.faces[i + 2], target.faces[i + 3], target.faces[i + 4]); + break; + default: { - var face = new RG.MeshFace(triangles[t], triangles[t + 1], triangles[t + 2]); - faceIndices.Add(m.Faces.AddFace(face)); + // n-gon + var faceIndices = GetNgonFaceIndices(target, i, m).ToList(); + var vertexIndices = GetNgonVertexIndices(target.faces, i, n).ToList(); + RG.MeshNgon ngon = RG.MeshNgon.Create(vertexIndices, faceIndices); + m.Ngons.AddNgon(ngon); + break; } - - RG.MeshNgon ngon = RG.MeshNgon.Create(target.faces.GetRange(i + 1, n), faceIndices); - m.Ngons.AddNgon(ngon); } i += n + 1; @@ -102,6 +97,24 @@ public class MeshToHostConverter : ITypedConverter m.Faces.CullDegenerateFaces(); } + private static IEnumerable GetNgonFaceIndices(SOG.Mesh target, int start, RG.Mesh m) + { + var triangles = MeshTriangulationHelper.TriangulateFace(start, target, false); + for (int t = 0; t < triangles.Count; t += 3) + { + int faceIndex = m.Faces.AddFace(triangles[t], triangles[t + 1], triangles[t + 2]); + yield return faceIndex; + } + } + + private static IEnumerable GetNgonVertexIndices(List faces, int start, int vertexCount) + { + for (int n = 0; n < vertexCount; n++) + { + yield return faces[start + 1 + n]; + } + } + private static RG.Point2f[] ConvertTextureCoordinates(IReadOnlyList textureCoordinates) { var converted = new RG.Point2f[textureCoordinates.Count / 2];