Compare commits

...

25 Commits

Author SHA1 Message Date
Adam Hathcock 4b82db8ea2 Merge pull request #361 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev
2025-07-23 10:23:04 +01:00
Adam Hathcock 9e7f26f7a6 Add ModelCacheManager class and use it (#356)
* Introduce ModelCacheManager to manage cache and sizes and deletions

* move and abstract

* add tests and format

* Update src/Speckle.Sdk/Caching/ModelCacheManager.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Clean up

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 10:07:57 +01:00
Adam Hathcock b19f8c4219 Merge pull request #358 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
(NO SQUASH) Main to dev for release
2025-07-21 15:49:49 +01:00
Adam Hathcock c517e61517 Merge pull request #360 from specklesystems/main-dev
Main to dev
2025-07-21 15:47:38 +01:00
Adam Hathcock b3e0623856 Merge remote-tracking branch 'origin/main' into main-dev 2025-07-21 15:36:30 +01:00
Adam Hathcock e5d1ef2448 Merge pull request #354 from specklesystems/main-dev
Merge pull request #348 from specklesystems/dev
2025-07-21 11:12:54 +01:00
Adam Hathcock 83c3de05fa Merge remote-tracking branch 'origin/dev' into main-dev 2025-07-21 11:02:02 +01:00
Adam Hathcock 507ded7d4a Fix shallow copy allocations and perf (#357)
* add more DynamicBase Tests

* Move ShallowCopy to dynamic and try to be faster with copy

* Correct tests for macOS

* use cache obsolete attribute

* Update src/Speckle.Sdk/Models/DynamicBase.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix AI

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 11:01:00 +01:00
Adam Hathcock e15029bab3 Merge pull request #350 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
(dev to main) If we're already cancelling, ignore extra exceptions (#349)
2025-07-14 10:34:06 +01:00
Adam Hathcock a43fd44206 Stop recording an exception that's rethrown (#355)
* Stop recording an exception that's rethrown

* Update src/Speckle.Sdk/Api/GraphQL/Client.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-14 10:15:40 +01:00
Adam Hathcock 1bcd8ac3a4 Merge branch 'dev' into main-dev 2025-07-03 10:42:05 +01:00
Adam Hathcock a8dc93e22b Adds detail to message so that user isn't going WTF SDK (#351)
* Adds detail to message so that user isn't going WTF SDK

* update exception tests
2025-07-03 09:39:53 +00:00
Jedd Morgan 5a0f883b98 Add compatibility with :local docker images (#353)
Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
2025-07-03 10:28:35 +01:00
Adam Hathcock a5d035671a If we're already cancelling, ignore extra exceptions (#349)
* If we're already cancelling, ignore extra exceptions

* Do more robust cancellation

* Try to have more robust disposal and cancellation check
2025-07-01 10:20:32 +01:00
Adam Hathcock cd6ebad619 Merge pull request #348 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
New release for connectors
2025-06-30 15:15:51 +01:00
Adam Hathcock 33c2e6e1a4 Better handle graphql commit errors (#343)
* Better handle graphql commit errors

* add graphql error test
2025-06-30 10:54:44 +00:00
Adam Hathcock b97702adb1 Small fixes to SDK (#347)
* Increase channel capacity to make things more performant

* Avoid logging send cancellation exceptions, caller did it

* Try to avoid collection modified errors when cancelling by more aggressive checks

* oops, rethrow, don't catch
2025-06-30 10:37:30 +00:00
Adam Hathcock 80c4f694ec Merge pull request #346 from specklesystems/main-dev
Main to dev
2025-06-30 11:26:36 +01:00
Adam Hathcock fb5042004f Merge remote-tracking branch 'origin/dev' into main-dev 2025-06-30 08:58:57 +01:00
Adam Hathcock c0a9291632 Merge pull request #344 from specklesystems/oguzhan/level-proxies
.NET Build and Publish / build (push) Has been cancelled
Feat(objects): level proxies
2025-06-23 15:05:15 +01:00
oguzhankoral b783d2acb6 Format 2025-06-23 15:57:24 +03:00
oguzhankoral 93539adc1e Add level proxies 2025-06-23 15:42:48 +03:00
Adam Hathcock 98005933de Remove DistinctBy as we don't use it (#342) 2025-06-19 09:34:34 +01:00
Adam Hathcock 50906b172a Merge pull request #340 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
2025-06-11 17:35:27 +01:00
Adam Hathcock 05f7353925 Revert "Merge pull request #335 from specklesystems/adam/cnx-1786-allow-multiple-sends-to-access-sqlite-in-a-non-locking-2" (#339)
This reverts commit 59019bf846, reversing
changes made to 3afaf61a1a.

Co-authored-by: Adam Hathcock <adam@Adams-Mac-mini.localdomain>
2025-06-11 15:32:06 +00:00
48 changed files with 545 additions and 318 deletions
+20
View File
@@ -0,0 +1,20 @@
using Speckle.Objects.Data;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
/// <summary>
/// Proxy for levels as DataObject value.
/// <remarks> These proxy lives in Objects library because it depends on DataObject</remarks>
/// </summary>
[SpeckleType("Objects.Other.LevelProxy")]
public class LevelProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this level
/// </summary>
public required List<string> objects { get; set; }
public required DataObject value { get; set; }
}
@@ -1,7 +1,6 @@
using System.Drawing;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
@@ -39,20 +38,3 @@ public class RenderMaterial : Base
set => diffuse = value.ToArgb();
}
}
/// <summary>
/// Used to store render material to object relationships in root collections
/// </summary>
[SpeckleType("Objects.Other.RenderMaterialProxy")]
public class RenderMaterialProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this render material
/// </summary>
public required List<string> objects { get; set; }
/// <summary>
/// The render material used by <see cref="objects"/>
/// </summary>
public required RenderMaterial value { get; set; }
}
@@ -0,0 +1,22 @@
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
/// <summary>
/// Used to store render material to object relationships in root collections
/// <remarks> These proxy lives in Objects library because it depends on RenderMaterial</remarks>
/// </summary>
[SpeckleType("Objects.Other.RenderMaterialProxy")]
public class RenderMaterialProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this render material
/// </summary>
public required List<string> objects { get; set; }
/// <summary>
/// The render material used by <see cref="objects"/>
/// </summary>
public required RenderMaterial value { get; set; }
}
@@ -22,23 +22,4 @@ public static class Collections
public static class EnumerableExtensions
{
public static IEnumerable<int> RangeFrom(int from, int to) => Enumerable.Range(from, to - from + 1);
#if NETSTANDARD2_0
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector
)
{
var keys = new HashSet<TKey>();
foreach (var element in source)
{
if (keys.Contains(keySelector(element)))
{
continue;
}
keys.Add(keySelector(element));
yield return element;
}
}
#endif
}
@@ -5,7 +5,7 @@ namespace Speckle.Sdk.Dependencies.Serialization;
public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
{
private const int RECEIVE_CAPACITY = 5000;
private const int RECEIVE_CAPACITY = 10000;
private const int HTTP_GET_CHUNK_SIZE = 500;
private const int MAX_PARALLELISM_HTTP = 4;
@@ -109,6 +109,9 @@ public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
Exception = ex;
_channel.Writer.TryComplete(ex);
//cancel everything!
_cts.Cancel();
if (!_cts.IsCancellationRequested)
{
_cts.Cancel();
}
}
}
@@ -8,7 +8,7 @@ namespace Speckle.Sdk.Dependencies.Serialization;
public abstract class ChannelSaver<T>
where T : IHasByteSize
{
private const int SEND_CAPACITY = 1000;
private const int SEND_CAPACITY = 10000;
private const int HTTP_SEND_CHUNK_SIZE = 25_000_000; //bytes
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
private const int MAX_PARALLELISM_HTTP = 4;
@@ -41,13 +41,7 @@ internal sealed class SpeckleHttpClientHandler : DelegatingHandler
activity?.InjectHeaders((k, v) => request.Headers.TryAddWithoutValidation(k, v));
var policyResult = await _resiliencePolicy
.ExecuteAndCaptureAsync(
ctx =>
{
return base.SendAsync(request, cancellationToken);
},
context
)
.ExecuteAndCaptureAsync(ctx => base.SendAsync(request, cancellationToken), context)
.ConfigureAwait(false);
context.TryGetValue("retryCount", out var retryCount);
activity?.SetTag("retryCount", retryCount);
+11
View File
@@ -126,3 +126,14 @@ public sealed class WorkspacePermissionException : SpeckleGraphQLException
public WorkspacePermissionException(string? message, Exception? innerException)
: base(message, innerException) { }
}
public sealed class CannotCreateCommitException : SpeckleGraphQLException
{
public CannotCreateCommitException() { }
public CannotCreateCommitException(string? message)
: base(message) { }
public CannotCreateCommitException(string? message, Exception? innerException)
: base(message, innerException) { }
}
+2 -2
View File
@@ -127,10 +127,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
activity?.SetStatus(SdkActivityStatusCode.Ok);
return ret;
}
catch (Exception ex)
catch (Exception)
{
activity?.SetStatus(SdkActivityStatusCode.Error);
activity?.RecordException(ex);
// Don't record exception as it's rethrown.
throw;
}
}
@@ -33,6 +33,7 @@ internal static class GraphQLErrorHandler
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
"WORKSPACES_MODULE_DISABLED_ERROR" => new SpeckleGraphQLWorkspaceNotEnabledException(message),
"COMMIT_CREATE_ERROR" => new CannotCreateCommitException(message),
_ => new SpeckleGraphQLException(message),
};
exceptions.Add(ex);
@@ -40,7 +40,8 @@ public static class GraphQLHttpClientExtensions
response.EnsureGraphQLSuccess();
string versionString = response.Data.data.data;
if (versionString == "dev")
//Local server builds will have a non-numerical version string
if (versionString == "dev" || versionString == "custom")
{
return new Version(999, 999, 999);
}
@@ -47,6 +47,11 @@ public partial class Operations
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
return results;
}
catch (OperationCanceledException)
{
//this is handled by the caller
throw;
}
catch (Exception ex)
{
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
+22
View File
@@ -0,0 +1,22 @@
using Speckle.InterfaceGenerator;
namespace Speckle.Sdk.Caching;
/// <summary>
/// This mocks away the file system operations for testing purposes.
/// </summary>
[GenerateAutoInterface]
public class FileSystem : IFileSystem
{
public bool DirectoryExists(string path) => Directory.Exists(path);
public void CreateDirectory(string path) => Directory.CreateDirectory(path);
public IEnumerable<string> EnumerateFiles(string path) => Directory.EnumerateFiles(path);
public void DeleteFile(string path) => File.Delete(path);
public long GetFileSize(string path) => new FileInfo(path).Length;
public string Combine(params string[] paths) => Path.Combine(paths);
}
@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Caching;
/// <summary>
/// This class manages the cache for model data, providing methods to get stream paths, clear the cache, and calculate cache size.
/// </summary>
[GenerateAutoInterface]
public class ModelCacheManager(ILogger<ModelCacheManager> logger, IFileSystem fileSystem) : IModelCacheManager
{
private const string DATA_FOLDER = "Projects";
private static readonly string s_basePath = SpecklePathProvider.UserSpeckleFolderPath;
private static string CacheFolder => Path.Combine(s_basePath, DATA_FOLDER);
public string GetStreamPath(string streamId) => GetDbPath(streamId);
public static string GetDbPath(string streamId)
{
var db = Path.Combine(CacheFolder, $"{streamId}.db");
try
{
Directory.CreateDirectory(CacheFolder); //ensure dir is there
return db;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Path was invalid or could not be created {db}", ex);
}
}
public void ClearCache()
{
try
{
if (!fileSystem.DirectoryExists(CacheFolder))
{
return;
}
foreach (var db in fileSystem.EnumerateFiles(CacheFolder))
{
try
{
fileSystem.DeleteFile(db);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
{
logger.LogWarning(ex, "Failed to delete cache file {filePath}", db);
}
}
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Cache folder could not be cleared: {CacheFolder}", ex);
}
}
public long GetCacheSize()
{
try
{
if (!fileSystem.DirectoryExists(CacheFolder))
{
return 0;
}
long size = 0;
foreach (var file in fileSystem.EnumerateFiles(CacheFolder))
{
try
{
size += fileSystem.GetFileSize(file);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
{
logger.LogWarning(ex, "Failed to get size for cache file {a}", file);
}
}
return size;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Cache folder size could not be determined: {CacheFolder}", ex);
}
}
}
+4
View File
@@ -15,10 +15,14 @@ internal static class TypeLoader
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
private static ConcurrentDictionary<PropertyInfo, JsonPropertyAttribute?> s_jsonPropertyAttribute = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> s_obsolete = new();
private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> s_propInfoCache = new();
public static IEnumerable<LoadedType> Types => s_availableTypes;
public static bool IsObsolete(PropertyInfo property) =>
s_obsolete.GetOrAdd(property, p => p.IsDefined(typeof(ObsoleteAttribute), true));
public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) =>
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
+1 -29
View File
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Speckle.Newtonsoft.Json;
using Speckle.Newtonsoft.Json.Linq;
using Speckle.Sdk.Common;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.Host;
using Speckle.Sdk.Serialisation;
@@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject
var typedProps = @base.GetInstanceMembers();
foreach (var prop in typedProps.Where(p => p.CanRead))
{
bool isIgnored =
prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
if (isIgnored)
{
continue;
@@ -193,30 +191,4 @@ public class Base : DynamicBase, ISpeckleObject
return count;
}
}
/// <summary>
/// Creates a shallow copy of the current base object.
/// This operation does NOT copy/duplicate the data inside each prop.
/// The new object's property values will be pointers to the original object's property value.
/// </summary>
/// <returns>A shallow copy of the original object.</returns>
public Base ShallowCopy()
{
Type type = GetType();
Base myDuplicate = (Base)Activator.CreateInstance(type).NotNull();
myDuplicate.id = id;
myDuplicate.applicationId = applicationId;
foreach (var kvp in GetMembers())
{
var propertyInfo = type.GetProperty(kvp.Key);
if (propertyInfo is not null && !propertyInfo.CanWrite)
{
continue;
}
myDuplicate[kvp.Key] = kvp.Value;
}
return myDuplicate;
}
}
+40 -2
View File
@@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models;
/// <summary>
/// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json.
/// <para>Orginally adapted from Rick Strahl 🤘</para>
/// <para>Originally adapted from Rick Strahl 🤘</para>
/// <para>https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object</para>
/// </summary>
public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
@@ -84,6 +84,44 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
}
}
/// <summary>
/// Creates a shallow copy of the current base object.
/// This operation does NOT copy/duplicate the data inside each prop.
/// The new object's property values will be pointers to the original object's property value.
/// </summary>
/// <returns>A shallow copy of the original object.</returns>
public DynamicBase ShallowCopy()
{
Type type = GetType();
DynamicBase myDuplicate = (DynamicBase)(
Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}")
);
// Add dynamic members
foreach (var kvp in _properties)
{
myDuplicate._properties[kvp.Key] = kvp.Value;
}
var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x));
foreach (var pi in pinfos)
{
if (pi.CanWrite)
{
try
{
pi.SetValue(myDuplicate, pi.GetValue(this));
}
catch (Exception ex) when (!ex.IsFatal())
{
throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex);
}
}
}
return myDuplicate;
}
/// <inheritdoc />
/// <summary>
/// Gets properties via the dot syntax.
@@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
.GetBaseProperties(GetType())
.Where(x =>
{
var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true);
var hasObsolete = TypeLoader.IsObsolete(x);
// If obsolete is false and prop has obsolete attr
// OR
@@ -13,18 +13,15 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
{
private readonly CacheDbCommandPool _pool;
public string Path { get; }
public static ISqLiteJsonCacheManager FromMemory(int concurrency) => new SqLiteJsonCacheManager(concurrency);
private SqLiteJsonCacheManager(int concurrency)
{
Path = ":memory:";
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder
{
Pooling = false,
DataSource = Path,
DataSource = ":memory:",
Cache = SqliteCacheMode.Shared,
Mode = SqliteOpenMode.Memory,
};
@@ -37,9 +34,8 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
private SqLiteJsonCacheManager(string path, int concurrency)
{
Path = path;
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = Path };
var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = path };
_pool = new CacheDbCommandPool(builder.ToString(), concurrency);
Initialize();
}
@@ -1,11 +1,11 @@
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Serialisation.Utilities;
namespace Speckle.Sdk.SQLite;
[GenerateAutoInterface]
public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
public class SqLiteJsonCacheManagerFactory(IModelCacheManager modelCacheManager) : ISqLiteJsonCacheManagerFactory
{
public const int INITIAL_CONCURRENCY = 4;
@@ -16,5 +16,5 @@ public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
public ISqLiteJsonCacheManager CreateFromStream(string streamId) =>
Create(SqlitePaths.GetDBPath(streamId), INITIAL_CONCURRENCY);
Create(modelCacheManager.GetStreamPath(streamId), INITIAL_CONCURRENCY);
}
@@ -1,30 +0,0 @@
using Speckle.Sdk.Logging;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.Utilities;
public static class SqlitePaths
{
private const string APPLICATION_NAME = "Speckle";
private const string DATA_FOLDER = "Projects";
private static readonly string basePath = SpecklePathProvider.UserApplicationDataPath();
public static string BlobStorageFolder =>
SpecklePathProvider.BlobStoragePath(Path.Combine(basePath, APPLICATION_NAME));
public static string GetDBPath(string streamId)
{
var dir = Path.Combine(basePath, APPLICATION_NAME, DATA_FOLDER);
var db = Path.Combine(dir, $"{streamId}.db");
try
{
Directory.CreateDirectory(dir); //ensure dir is there
return db;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Path was invalid or could not be created {db}", ex);
}
}
}
@@ -7,10 +7,6 @@ namespace Speckle.Sdk.Serialisation.V2;
public class MemoryJsonCacheManager(ConcurrentDictionary<Id, Json> jsonCache) : ISqLiteJsonCacheManager
#pragma warning restore CA1063
{
#pragma warning disable CA1065
public string Path => "MemoryJsonCacheManager";
#pragma warning restore CA1065
public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() =>
jsonCache.Select(x => (x.Key.Value, x.Value.Value)).ToList();
@@ -171,7 +171,7 @@ public sealed class ObjectLoader(
cancellationToken.ThrowIfCancellationRequested();
if (Exception is not null)
{
throw new SpeckleException("Error while loading", Exception);
throw new SpeckleException($"Error while loading: {Exception.Message}", Exception);
}
}
}
@@ -38,10 +38,11 @@ public sealed class ObjectSaver(
private long _cached;
private long _objectsSerialized;
private bool _disposed;
protected override async Task SendToServerInternal(Batch<BaseItem> batch)
{
if (_cancellationTokenSource.IsCancellationRequested)
if (IsCancelled())
{
return;
}
@@ -66,7 +67,7 @@ public sealed class ObjectSaver(
}
catch (OperationCanceledException)
{
_cancellationTokenSource.Cancel();
CancelSaving();
}
#pragma warning disable CA1031
catch (Exception e)
@@ -91,7 +92,7 @@ public sealed class ObjectSaver(
public override void SaveToCache(List<BaseItem> batch)
{
if (_cancellationTokenSource.IsCancellationRequested)
if (IsCancelled())
{
return;
}
@@ -106,7 +107,7 @@ public sealed class ObjectSaver(
}
catch (OperationCanceledException)
{
_cancellationTokenSource.Cancel();
CancelSaving();
}
#pragma warning disable CA1031
catch (Exception e)
@@ -123,8 +124,23 @@ public sealed class ObjectSaver(
}
}
private bool IsCancelled() => _disposed || _cancellationTokenSource.IsCancellationRequested;
private void CancelSaving()
{
if (IsCancelled())
{
return;
}
_cancellationTokenSource.Cancel();
}
private void RecordException(Exception e)
{
if (IsCancelled())
{
return;
}
//order here matters
logger.LogError(e, "Error in SDK: {message}", e.Message);
Exception = e;
@@ -133,6 +149,7 @@ public sealed class ObjectSaver(
public void Dispose()
{
_disposed = true;
_cancellationTokenSource.Dispose();
sqLiteJsonCacheManager.Dispose();
}
@@ -1,50 +0,0 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.SQLite;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.V2.Send;
public partial interface IObjectSaverFactory : IDisposable;
[GenerateAutoInterface]
public sealed class ObjectSaverFactory(ILoggerFactory loggerFactory) : IObjectSaverFactory
{
private readonly ConcurrentDictionary<string, IObjectSaver> _savers = new();
public IObjectSaver Create(
IServerObjectManager serverObjectManager,
ISqLiteJsonCacheManager sqLiteJsonCacheManager,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken,
SerializeProcessOptions? options = null
)
{
if (!_savers.TryGetValue(sqLiteJsonCacheManager.Path, out var saver))
{
saver = new ObjectSaver(
progress,
sqLiteJsonCacheManager,
serverObjectManager,
loggerFactory.CreateLogger<ObjectSaver>(),
cancellationToken,
options
);
_savers.TryAdd(sqLiteJsonCacheManager.Path, saver);
}
return saver;
}
[AutoInterfaceIgnore]
public void Dispose()
{
foreach (var pool in _savers)
{
pool.Value.Dispose();
}
_savers.Clear();
}
}
@@ -47,6 +47,7 @@ public sealed class SerializeProcess(
cancellationToken
);
private readonly ILogger<SerializeProcess> _logger = loggerFactory.CreateLogger<SerializeProcess>();
private bool _disposed;
//async dispose
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed")]
@@ -83,9 +84,11 @@ public sealed class SerializeProcess(
[AutoInterfaceIgnore]
public async ValueTask DisposeAsync()
{
_disposed = true;
await WaitForSchedulerCompletion().ConfigureAwait(false);
await _highest.DisposeAsync().ConfigureAwait(false);
await _belowNormal.DisposeAsync().ConfigureAwait(false);
objectSaver.Dispose();
_processSource.Dispose();
}
@@ -94,7 +97,7 @@ public sealed class SerializeProcess(
//order here matters...null with cancellation means a user did it, otherwise it's a real Exception
if (objectSaver.Exception is not null)
{
throw new SpeckleException("Error while sending", objectSaver.Exception);
throw new SpeckleException($"Error while sending: {objectSaver.Exception.Message}", objectSaver.Exception);
}
_processSource.Token.ThrowIfCancellationRequested();
}
@@ -147,7 +150,7 @@ public sealed class SerializeProcess(
private void TraverseTotal(Base obj)
{
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return;
}
@@ -161,7 +164,7 @@ public sealed class SerializeProcess(
private async Task<Dictionary<Id, NodeInfo>> Traverse(Base obj)
{
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
@@ -173,7 +176,7 @@ public sealed class SerializeProcess(
{
// tmp is necessary because of the way closures close over loop variables
var tmp = child;
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
@@ -190,7 +193,7 @@ public sealed class SerializeProcess(
tasks.Add(t);
}
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
@@ -217,7 +220,7 @@ public sealed class SerializeProcess(
}
_taskResultPool.Return(tasks);
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
@@ -225,22 +228,30 @@ public sealed class SerializeProcess(
var childClosures = _childClosurePool.Get();
foreach (var childClosure in taskClosures)
{
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
foreach (var kvp in childClosure)
{
childClosures[kvp.Key] = kvp.Value;
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
}
_currentClosurePool.Return(childClosure);
}
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
var items = baseSerializer.Serialise(obj, childClosures, _options.SkipCacheRead, _processSource.Token);
if (_processSource.Token.IsCancellationRequested)
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
@@ -252,13 +263,13 @@ public sealed class SerializeProcess(
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
foreach (var item in items)
{
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
if (item.NeedsStorage)
{
if (_processSource.Token.IsCancellationRequested)
{
return EMPTY_CLOSURES;
}
Interlocked.Increment(ref _objectsSerialized);
await objectSaver.SaveAsync(item).ConfigureAwait(false);
}
@@ -289,6 +300,8 @@ public sealed class SerializeProcess(
}
}
public bool IsCancelled() => _disposed || _processSource.IsCancellationRequested;
public void RecordException(Exception e)
{
if (e is OperationCanceledException)
@@ -303,6 +316,11 @@ public sealed class SerializeProcess(
{
return;
}
if (IsCancelled())
{
//if we are already cancelled, don't log or save the exceptions
return;
}
//order here matters
_logger.LogError(e, "Error in SDK: {message}", e.Message);
objectSaver.Exception = e;
@@ -13,7 +13,6 @@ public class SerializeProcessFactory(
IObjectSerializerFactory objectSerializerFactory,
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory,
IServerObjectManagerFactory serverObjectManagerFactory,
IObjectSaverFactory objectSaverFactory,
ILoggerFactory loggerFactory
) : ISerializeProcessFactory
{
@@ -40,7 +39,13 @@ public class SerializeProcessFactory(
) =>
new SerializeProcess(
progress,
objectSaverFactory.Create(serverObjectManager, sqLiteJsonCacheManager, progress, cancellationToken, options),
new ObjectSaver(
progress,
sqLiteJsonCacheManager,
serverObjectManager,
loggerFactory.CreateLogger<ObjectSaver>(),
cancellationToken
),
baseChildFinder,
new BaseSerializer(sqLiteJsonCacheManager, objectSerializerFactory),
loggerFactory,
-2
View File
@@ -97,8 +97,6 @@ public static class ServiceRegistration
typeof(Client)
);
serviceCollection.AddMatchingInterfacesAsTransient(typeof(GraphQLRetry).Assembly);
//we want to make object savers be singletons per stream so needs a singleton factory
serviceCollection.AddSingleton<IObjectSaverFactory, ObjectSaverFactory>();
return serviceCollection;
}
@@ -3,9 +3,9 @@ using System.Diagnostics;
using System.Text;
using System.Timers;
using Microsoft.Data.Sqlite;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Serialisation.Utilities;
using Timer = System.Timers.Timer;
namespace Speckle.Sdk.Transports;
@@ -28,7 +28,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
{
_streamId = streamId;
_rootPath = SqlitePaths.GetDBPath(streamId);
_rootPath = ModelCacheManager.GetDbPath(streamId);
_connectionString = $"Data Source={_rootPath};";
@@ -50,7 +50,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
private SqliteConnection Connection { get; set; }
private readonly SemaphoreSlim _connectionLock = new(1, 1);
public string BlobStorageFolder => SqlitePaths.BlobStorageFolder;
public string BlobStorageFolder => SpecklePathProvider.UserSpeckleFolderPath;
public void SaveBlob(Blob obj)
{
@@ -41,7 +41,7 @@ public class DataObjectTests
new DummyServerObjectManager(),
null,
default,
new SerializeProcessOptions(false, false, false, true)
new SerializeProcessOptions(true, true, false, true)
);
await serializeProcess.Serialize(x);
await VerifyJson(json.Single().Value.Value).UseParameters(type);
@@ -41,7 +41,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true)
new SerializeProcessOptions(false, false, true, true)
);
await serializeProcess.Serialize(@base);
@@ -123,7 +123,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -150,7 +150,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -172,7 +172,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -239,7 +239,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true)
new SerializeProcessOptions(false, false, true, true)
);
var results = await serializeProcess.Serialize(@base);
@@ -272,7 +272,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, false, true)
new SerializeProcessOptions(false, false, true, true)
);
var results = await serializeProcess.Serialize(@base);
await VerifyJsonDictionary(objects);
@@ -373,9 +373,6 @@ public class DummyServerObjectManager : IServerObjectManager
public class DummySendCacheManager(Dictionary<string, string> objects) : ISqLiteJsonCacheManager
{
#pragma warning disable CA1065
public string Path => throw new NotImplementedException();
#pragma warning restore CA1065
public void Dispose() { }
public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException();
@@ -4,9 +4,6 @@ namespace Speckle.Sdk.Serialization.Tests;
public class DummyCancellationSqLiteSendManager : ISqLiteJsonCacheManager
{
#pragma warning disable CA1065
public string Path => throw new NotImplementedException();
#pragma warning restore CA1065
public string? GetObject(string id) => null;
public void SaveObject(string id, string json) => throw new NotImplementedException();
@@ -6,6 +6,6 @@
"Message": "The method or operation is not implemented.",
"Type": "NotImplementedException"
},
"Message": "Error while sending",
"Message": "Error while sending: The method or operation is not implemented.",
"Type": "SpeckleException"
}
@@ -6,6 +6,6 @@
"Message": "Count exceeded",
"Type": "Exception"
},
"Message": "Error while sending",
"Message": "Error while sending: Count exceeded",
"Type": "SpeckleException"
}
@@ -6,6 +6,6 @@
"Message": "The method or operation is not implemented.",
"Type": "NotImplementedException"
},
"Message": "Error while loading",
"Message": "Error while loading: The method or operation is not implemented.",
"Type": "SpeckleException"
}
@@ -6,6 +6,6 @@
"Message": "The method or operation is not implemented.",
"Type": "NotImplementedException"
},
"Message": "Error while sending",
"Message": "Error while sending: The method or operation is not implemented.",
"Type": "SpeckleException"
}
@@ -4,7 +4,6 @@ namespace Speckle.Sdk.Serialization.Tests.Framework;
public class ExceptionSendCacheManager(bool? hasObject = null, int? exceptionsAfter = null) : ISqLiteJsonCacheManager
{
public string Path => "ExceptionSendCacheManager";
private readonly object _lock = new();
private int _count;
@@ -1,90 +0,0 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Speckle.Sdk.SQLite;
using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Serialisation.V2.Send.Tests;
public class ObjectSaverFactoryTests : MoqTest
{
private readonly Mock<ILoggerFactory> _loggerFactoryMock;
private readonly Mock<ILogger<ObjectSaver>> _loggerMock;
private readonly ObjectSaverFactory _factory;
public ObjectSaverFactoryTests()
{
_loggerFactoryMock = Create<ILoggerFactory>();
_loggerMock = Create<ILogger<ObjectSaver>>();
_factory = new ObjectSaverFactory(_loggerFactoryMock.Object);
}
public override void Dispose()
{
_factory.Dispose();
base.Dispose();
}
[Fact]
public void Create_ShouldReturnObjectSaverInstance()
{
_loggerFactoryMock.Setup(f => f.CreateLogger(typeof(ObjectSaver).FullName)).Returns(_loggerMock.Object);
var cacheManagerMock = Create<ISqLiteJsonCacheManager>();
cacheManagerMock.Setup(x => x.Dispose());
cacheManagerMock.SetupGet(c => c.Path).Returns("/tmp/test1.db");
var saver = _factory.Create(
Create<IServerObjectManager>().Object,
cacheManagerMock.Object,
null,
CancellationToken.None
);
saver.Should().NotBeNull();
}
[Fact]
public void Create_ShouldReturnSameInstanceForSamePath()
{
_loggerFactoryMock.Setup(f => f.CreateLogger(typeof(ObjectSaver).FullName)).Returns(_loggerMock.Object);
var cacheManagerMock = Create<ISqLiteJsonCacheManager>();
cacheManagerMock.Setup(x => x.Dispose());
cacheManagerMock.SetupGet(c => c.Path).Returns("/tmp/test2.db");
var saver1 = _factory.Create(
Create<IServerObjectManager>().Object,
cacheManagerMock.Object,
null,
CancellationToken.None
);
var saver2 = _factory.Create(
Create<IServerObjectManager>().Object,
cacheManagerMock.Object,
null,
CancellationToken.None
);
saver1.Should().BeSameAs(saver2);
}
[Fact]
public void Dispose_ShouldDisposeAllSavers()
{
var saverMock1 = Create<IObjectSaver>();
_factory
.GetType()
.GetField("_savers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.SetValue(
_factory,
new System.Collections.Concurrent.ConcurrentDictionary<string, IObjectSaver>(
new[]
{
new System.Collections.Generic.KeyValuePair<string, IObjectSaver>("/tmp/test3.db", saverMock1.Object),
}
)
);
saverMock1.Setup(x => x.Dispose());
_factory.Dispose();
}
}
@@ -5,9 +5,6 @@ namespace Speckle.Sdk.Testing.Framework;
public sealed class DummySqLiteReceiveManager(IReadOnlyDictionary<string, string> savedObjects)
: ISqLiteJsonCacheManager
{
#pragma warning disable CA1065
public string Path => throw new NotImplementedException();
#pragma warning restore CA1065
public void Dispose() { }
public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException();
@@ -4,8 +4,6 @@ namespace Speckle.Sdk.Testing.Framework;
public class DummySqLiteSendManager : ISqLiteJsonCacheManager
{
public string Path => "DummySqLiteSendManager";
public string? GetObject(string id) => throw new NotImplementedException();
public void SaveObject(string id, string json) => throw new NotImplementedException();
+1 -1
View File
@@ -8,7 +8,7 @@ public abstract class MoqTest : IDisposable
{
protected MoqTest() => Repository = new(MockBehavior.Strict);
public virtual void Dispose() => Repository.VerifyAll();
public void Dispose() => Repository.VerifyAll();
protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict);
@@ -23,6 +23,7 @@ public class GraphQLErrorHandlerTests
];
yield return [typeof(SpeckleGraphQLException), new Map { { "foo", "bar" } }];
yield return [typeof(SpeckleGraphQLException), new Map { { "code", "CUSTOM_THING" } }];
yield return [typeof(CannotCreateCommitException), new Map { { "code", "COMMIT_CREATE_ERROR" } }];
}
[Theory]
+2 -2
View File
@@ -18,7 +18,7 @@ public class SpecklePathTests
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
pattern = @"\/Users\/.*\/\.config";
pattern = @"\/Users\/.*\/Library\/Application Support";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@@ -57,7 +57,7 @@ public class SpecklePathTests
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
pattern = @"\/Users\/.*\/\.config";
pattern = @"\/Users\/.*\/Library\/Application Support";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@@ -0,0 +1,83 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Tests.Unit;
public class ModelCacheManagerMockTests : MoqTest
{
private readonly Mock<IFileSystem> _fileSystemMock;
private readonly ModelCacheManager _manager;
public ModelCacheManagerMockTests()
{
Mock<ILogger<ModelCacheManager>> loggerMock = Create<ILogger<ModelCacheManager>>(MockBehavior.Loose);
_fileSystemMock = Create<IFileSystem>();
_manager = new ModelCacheManager(loggerMock.Object, _fileSystemMock.Object);
}
[Fact]
public void ClearCache_ShouldNotDeleteFiles_WhenDirectoryDoesNotExist()
{
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
_manager.ClearCache();
_fileSystemMock.Verify(fs => fs.EnumerateFiles(It.IsAny<string>()), Times.Never);
_fileSystemMock.Verify(fs => fs.DeleteFile(It.IsAny<string>()), Times.Never);
}
[Fact]
public void ClearCache_ShouldDeleteFiles_WhenDirectoryExists()
{
var files = new List<string> { "file1.db", "file2.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
foreach (var file in files)
{
_fileSystemMock.Setup(fs => fs.DeleteFile(file));
}
_manager.ClearCache();
}
[Fact]
public void ClearCache_ShouldLogWarning_WhenDeleteFileThrows()
{
var files = new List<string> { "file1.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.DeleteFile(It.IsAny<string>())).Throws<IOException>();
_manager.ClearCache();
}
[Fact]
public void GetCacheSize_ShouldReturnZero_WhenDirectoryDoesNotExist()
{
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
var size = _manager.GetCacheSize();
size.Should().Be(0);
}
[Fact]
public void GetCacheSize_ShouldSumFileSizes()
{
var files = new List<string> { "file1.db", "file2.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.GetFileSize("file1.db")).Returns(10);
_fileSystemMock.Setup(fs => fs.GetFileSize("file2.db")).Returns(20);
var size = _manager.GetCacheSize();
size.Should().Be(30);
}
[Fact]
public void GetCacheSize_ShouldLogWarning_WhenGetFileSizeThrows()
{
var files = new List<string> { "file1.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.GetFileSize(It.IsAny<string>())).Throws<IOException>();
var size = _manager.GetCacheSize();
size.Should().Be(0);
}
}
@@ -213,7 +213,9 @@ public class BaseTests
[Fact]
public void CanShallowCopy()
{
var sample = new SampleObject();
var sample = new SampleObject { id = "sampleId" };
dynamic x = sample;
x.test = "test";
var copy = sample.ShallowCopy();
var selectedMembers = DynamicBaseMemberType.Dynamic | DynamicBaseMemberType.Instance;
@@ -0,0 +1,141 @@
using FluentAssertions;
using Microsoft.CSharp.RuntimeBinder;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Tests.Unit.Models;
public class DynamicBaseTests
{
public DynamicBaseTests()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
}
[Fact]
public void Indexer_SetAndGet()
{
// Arrange
var dynamicBase = new DynamicBase();
var key = "testProperty";
var value = "testValue";
// Act
dynamicBase[key] = value;
var result = dynamicBase[key];
// Assert
result.Should().Be(value);
}
[Fact]
public void DynamicProperty_SetAndGet()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
var value = "dynamicValue";
// Act
dynamicBase.dynamicProperty = value;
object result = dynamicBase.dynamicProperty;
// Assert
result.Should().Be(value);
}
[Fact]
public void GetMembers_Default()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.dynamicProp = "hello";
// Act
IDictionary<string, object?> members = dynamicBase.GetMembers();
// Assert
members.Should().ContainKey("dynamicProp");
}
[Fact]
public void GetMembers_Instance()
{
// Arrange
var dynamicBase = new TestDynamicBase();
// Act
var members = dynamicBase.GetMembers(DynamicBaseMemberType.Instance);
// Assert
members.Should().ContainKey(nameof(TestDynamicBase.InstanceProperty));
members.Should().NotContainKey("dynamicProp");
}
[Fact]
public void GetDynamicMemberNames()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.prop1 = 1;
dynamicBase.prop2 = "test";
// Act
IEnumerable<string> memberNames = dynamicBase.GetDynamicMemberNames();
// Assert
memberNames.Should().BeEquivalentTo(["DynamicPropertyKeys", "prop1", "prop2"]);
}
[Fact]
public void TryGetMember_Existing()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.existingProp = "I exist";
// Act
var result = dynamicBase.existingProp;
// Assert
((object)result)
.Should()
.Be("I exist");
}
[Fact]
public void TryGetMember_NonExisting()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
// Act
Action act = () =>
{
var result = dynamicBase.nonExistingProp;
};
// Assert
act.Should().Throw<RuntimeBinderException>();
}
[Fact]
public void TrySetMember()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
// Act
dynamicBase.newProp = "newValue";
// Assert
((object)dynamicBase.newProp)
.Should()
.Be("newValue");
}
private class TestDynamicBase : DynamicBase
{
public string InstanceProperty { get; set; } = "instance";
}
}
@@ -21,6 +21,7 @@ public class SerializeProcessRecordExceptionTests : MoqTest
.Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler"))
.Returns(Create<ILogger<PriorityScheduler>>().Object);
var objectSaverMock = Create<IObjectSaver>();
objectSaverMock.Setup(x => x.Dispose());
var baseChildFinderMock = Create<IBaseChildFinder>();
var baseSerializerMock = Create<IBaseSerializer>();
using var cts = new CancellationTokenSource();
@@ -56,6 +57,7 @@ public class SerializeProcessRecordExceptionTests : MoqTest
.Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler"))
.Returns(Create<ILogger<PriorityScheduler>>().Object);
var objectSaverMock = Create<IObjectSaver>();
objectSaverMock.Setup(x => x.Dispose());
var baseChildFinderMock = Create<IBaseChildFinder>();
var baseSerializerMock = Create<IBaseSerializer>();
using var cts = new CancellationTokenSource();
@@ -86,6 +88,7 @@ public class SerializeProcessRecordExceptionTests : MoqTest
.Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler"))
.Returns(Create<ILogger<PriorityScheduler>>().Object);
var objectSaverMock = Create<IObjectSaver>();
objectSaverMock.Setup(x => x.Dispose());
var baseChildFinderMock = Create<IBaseChildFinder>();
var baseSerializerMock = Create<IBaseSerializer>();
using var cts = new CancellationTokenSource();
@@ -1,7 +1,7 @@
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Common;
using Speckle.Sdk.Serialisation.Utilities;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Tests.Unit.Transports;
@@ -13,7 +13,7 @@ public sealed class SQLiteTransport2Tests : TransportTests, IDisposable
private SQLiteTransport2? _sqlite;
private static readonly string s_name = $"test-{Guid.NewGuid()}";
private static readonly string s_basePath = SqlitePaths.GetDBPath(s_name);
private static readonly string s_basePath = ModelCacheManager.GetDbPath(s_name);
public SQLiteTransport2Tests()
{