Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b82db8ea2 | |||
| 9e7f26f7a6 | |||
| b19f8c4219 | |||
| c517e61517 | |||
| b3e0623856 | |||
| e5d1ef2448 | |||
| 83c3de05fa | |||
| 507ded7d4a | |||
| e15029bab3 | |||
| a43fd44206 | |||
| 1bcd8ac3a4 | |||
| a8dc93e22b | |||
| 5a0f883b98 | |||
| a5d035671a | |||
| cd6ebad619 | |||
| 33c2e6e1a4 | |||
| b97702adb1 | |||
| 80c4f694ec | |||
| fb5042004f | |||
| c0a9291632 | |||
| b783d2acb6 | |||
| 93539adc1e | |||
| 98005933de |
@@ -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 System.Drawing;
|
||||||
using Speckle.Newtonsoft.Json;
|
using Speckle.Newtonsoft.Json;
|
||||||
using Speckle.Sdk.Models;
|
using Speckle.Sdk.Models;
|
||||||
using Speckle.Sdk.Models.Proxies;
|
|
||||||
|
|
||||||
namespace Speckle.Objects.Other;
|
namespace Speckle.Objects.Other;
|
||||||
|
|
||||||
@@ -39,20 +38,3 @@ public class RenderMaterial : Base
|
|||||||
set => diffuse = value.ToArgb();
|
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 class EnumerableExtensions
|
||||||
{
|
{
|
||||||
public static IEnumerable<int> RangeFrom(int from, int to) => Enumerable.Range(from, to - from + 1);
|
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)
|
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 HTTP_GET_CHUNK_SIZE = 500;
|
||||||
private const int MAX_PARALLELISM_HTTP = 4;
|
private const int MAX_PARALLELISM_HTTP = 4;
|
||||||
@@ -109,6 +109,9 @@ public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
|
|||||||
Exception = ex;
|
Exception = ex;
|
||||||
_channel.Writer.TryComplete(ex);
|
_channel.Writer.TryComplete(ex);
|
||||||
//cancel everything!
|
//cancel everything!
|
||||||
_cts.Cancel();
|
if (!_cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Speckle.Sdk.Dependencies.Serialization;
|
|||||||
public abstract class ChannelSaver<T>
|
public abstract class ChannelSaver<T>
|
||||||
where T : IHasByteSize
|
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 const int HTTP_SEND_CHUNK_SIZE = 25_000_000; //bytes
|
||||||
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
|
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
|
||||||
private const int MAX_PARALLELISM_HTTP = 4;
|
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));
|
activity?.InjectHeaders((k, v) => request.Headers.TryAddWithoutValidation(k, v));
|
||||||
|
|
||||||
var policyResult = await _resiliencePolicy
|
var policyResult = await _resiliencePolicy
|
||||||
.ExecuteAndCaptureAsync(
|
.ExecuteAndCaptureAsync(ctx => base.SendAsync(request, cancellationToken), context)
|
||||||
ctx =>
|
|
||||||
{
|
|
||||||
return base.SendAsync(request, cancellationToken);
|
|
||||||
},
|
|
||||||
context
|
|
||||||
)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
context.TryGetValue("retryCount", out var retryCount);
|
context.TryGetValue("retryCount", out var retryCount);
|
||||||
activity?.SetTag("retryCount", retryCount);
|
activity?.SetTag("retryCount", retryCount);
|
||||||
|
|||||||
@@ -126,3 +126,14 @@ public sealed class WorkspacePermissionException : SpeckleGraphQLException
|
|||||||
public WorkspacePermissionException(string? message, Exception? innerException)
|
public WorkspacePermissionException(string? message, Exception? innerException)
|
||||||
: base(message, 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) { }
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
|||||||
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
activity?.SetStatus(SdkActivityStatusCode.Error);
|
activity?.SetStatus(SdkActivityStatusCode.Error);
|
||||||
activity?.RecordException(ex);
|
// Don't record exception as it's rethrown.
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ internal static class GraphQLErrorHandler
|
|||||||
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
||||||
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
||||||
"WORKSPACES_MODULE_DISABLED_ERROR" => new SpeckleGraphQLWorkspaceNotEnabledException(message),
|
"WORKSPACES_MODULE_DISABLED_ERROR" => new SpeckleGraphQLWorkspaceNotEnabledException(message),
|
||||||
|
"COMMIT_CREATE_ERROR" => new CannotCreateCommitException(message),
|
||||||
_ => new SpeckleGraphQLException(message),
|
_ => new SpeckleGraphQLException(message),
|
||||||
};
|
};
|
||||||
exceptions.Add(ex);
|
exceptions.Add(ex);
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ public static class GraphQLHttpClientExtensions
|
|||||||
response.EnsureGraphQLSuccess();
|
response.EnsureGraphQLSuccess();
|
||||||
|
|
||||||
string versionString = response.Data.data.data;
|
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);
|
return new Version(999, 999, 999);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ public partial class Operations
|
|||||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
|
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
//this is handled by the caller
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
|
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,10 +15,14 @@ internal static class TypeLoader
|
|||||||
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
|
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
|
||||||
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
|
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
|
||||||
private static ConcurrentDictionary<PropertyInfo, JsonPropertyAttribute?> s_jsonPropertyAttribute = 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();
|
private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> s_propInfoCache = new();
|
||||||
|
|
||||||
public static IEnumerable<LoadedType> Types => s_availableTypes;
|
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) =>
|
public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) =>
|
||||||
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
|
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Speckle.Newtonsoft.Json;
|
using Speckle.Newtonsoft.Json;
|
||||||
using Speckle.Newtonsoft.Json.Linq;
|
using Speckle.Newtonsoft.Json.Linq;
|
||||||
using Speckle.Sdk.Common;
|
|
||||||
using Speckle.Sdk.Helpers;
|
using Speckle.Sdk.Helpers;
|
||||||
using Speckle.Sdk.Host;
|
using Speckle.Sdk.Host;
|
||||||
using Speckle.Sdk.Serialisation;
|
using Speckle.Sdk.Serialisation;
|
||||||
@@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject
|
|||||||
var typedProps = @base.GetInstanceMembers();
|
var typedProps = @base.GetInstanceMembers();
|
||||||
foreach (var prop in typedProps.Where(p => p.CanRead))
|
foreach (var prop in typedProps.Where(p => p.CanRead))
|
||||||
{
|
{
|
||||||
bool isIgnored =
|
bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
||||||
prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
|
||||||
if (isIgnored)
|
if (isIgnored)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -193,30 +191,4 @@ public class Base : DynamicBase, ISpeckleObject
|
|||||||
return count;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json.
|
/// 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>
|
/// <para>https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
|
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 />
|
/// <inheritdoc />
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets properties via the dot syntax.
|
/// Gets properties via the dot syntax.
|
||||||
@@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
|
|||||||
.GetBaseProperties(GetType())
|
.GetBaseProperties(GetType())
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
{
|
{
|
||||||
var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true);
|
var hasObsolete = TypeLoader.IsObsolete(x);
|
||||||
|
|
||||||
// If obsolete is false and prop has obsolete attr
|
// If obsolete is false and prop has obsolete attr
|
||||||
// OR
|
// OR
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using Speckle.InterfaceGenerator;
|
using Speckle.InterfaceGenerator;
|
||||||
|
using Speckle.Sdk.Caching;
|
||||||
using Speckle.Sdk.Logging;
|
using Speckle.Sdk.Logging;
|
||||||
using Speckle.Sdk.Serialisation.Utilities;
|
|
||||||
|
|
||||||
namespace Speckle.Sdk.SQLite;
|
namespace Speckle.Sdk.SQLite;
|
||||||
|
|
||||||
[GenerateAutoInterface]
|
[GenerateAutoInterface]
|
||||||
public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
|
public class SqLiteJsonCacheManagerFactory(IModelCacheManager modelCacheManager) : ISqLiteJsonCacheManagerFactory
|
||||||
{
|
{
|
||||||
public const int INITIAL_CONCURRENCY = 4;
|
public const int INITIAL_CONCURRENCY = 4;
|
||||||
|
|
||||||
@@ -16,5 +16,5 @@ public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
|
|||||||
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
|
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
|
||||||
|
|
||||||
public ISqLiteJsonCacheManager CreateFromStream(string streamId) =>
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -171,7 +171,7 @@ public sealed class ObjectLoader(
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (Exception is not null)
|
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 _cached;
|
||||||
|
|
||||||
private long _objectsSerialized;
|
private long _objectsSerialized;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
protected override async Task SendToServerInternal(Batch<BaseItem> batch)
|
protected override async Task SendToServerInternal(Batch<BaseItem> batch)
|
||||||
{
|
{
|
||||||
if (_cancellationTokenSource.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ public sealed class ObjectSaver(
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Cancel();
|
CancelSaving();
|
||||||
}
|
}
|
||||||
#pragma warning disable CA1031
|
#pragma warning disable CA1031
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -91,7 +92,7 @@ public sealed class ObjectSaver(
|
|||||||
|
|
||||||
public override void SaveToCache(List<BaseItem> batch)
|
public override void SaveToCache(List<BaseItem> batch)
|
||||||
{
|
{
|
||||||
if (_cancellationTokenSource.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,7 @@ public sealed class ObjectSaver(
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Cancel();
|
CancelSaving();
|
||||||
}
|
}
|
||||||
#pragma warning disable CA1031
|
#pragma warning disable CA1031
|
||||||
catch (Exception e)
|
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)
|
private void RecordException(Exception e)
|
||||||
{
|
{
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
//order here matters
|
//order here matters
|
||||||
logger.LogError(e, "Error in SDK: {message}", e.Message);
|
logger.LogError(e, "Error in SDK: {message}", e.Message);
|
||||||
Exception = e;
|
Exception = e;
|
||||||
@@ -133,6 +149,7 @@ public sealed class ObjectSaver(
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
sqLiteJsonCacheManager.Dispose();
|
sqLiteJsonCacheManager.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public sealed class SerializeProcess(
|
|||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
private readonly ILogger<SerializeProcess> _logger = loggerFactory.CreateLogger<SerializeProcess>();
|
private readonly ILogger<SerializeProcess> _logger = loggerFactory.CreateLogger<SerializeProcess>();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
//async dispose
|
//async dispose
|
||||||
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed")]
|
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed")]
|
||||||
@@ -83,6 +84,7 @@ public sealed class SerializeProcess(
|
|||||||
[AutoInterfaceIgnore]
|
[AutoInterfaceIgnore]
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
||||||
await _highest.DisposeAsync().ConfigureAwait(false);
|
await _highest.DisposeAsync().ConfigureAwait(false);
|
||||||
await _belowNormal.DisposeAsync().ConfigureAwait(false);
|
await _belowNormal.DisposeAsync().ConfigureAwait(false);
|
||||||
@@ -95,7 +97,7 @@ public sealed class SerializeProcess(
|
|||||||
//order here matters...null with cancellation means a user did it, otherwise it's a real Exception
|
//order here matters...null with cancellation means a user did it, otherwise it's a real Exception
|
||||||
if (objectSaver.Exception is not null)
|
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();
|
_processSource.Token.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
@@ -148,7 +150,7 @@ public sealed class SerializeProcess(
|
|||||||
|
|
||||||
private void TraverseTotal(Base obj)
|
private void TraverseTotal(Base obj)
|
||||||
{
|
{
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,7 +164,7 @@ public sealed class SerializeProcess(
|
|||||||
|
|
||||||
private async Task<Dictionary<Id, NodeInfo>> Traverse(Base obj)
|
private async Task<Dictionary<Id, NodeInfo>> Traverse(Base obj)
|
||||||
{
|
{
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
@@ -174,7 +176,7 @@ public sealed class SerializeProcess(
|
|||||||
{
|
{
|
||||||
// tmp is necessary because of the way closures close over loop variables
|
// tmp is necessary because of the way closures close over loop variables
|
||||||
var tmp = child;
|
var tmp = child;
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
@@ -191,7 +193,7 @@ public sealed class SerializeProcess(
|
|||||||
tasks.Add(t);
|
tasks.Add(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
@@ -218,7 +220,7 @@ public sealed class SerializeProcess(
|
|||||||
}
|
}
|
||||||
_taskResultPool.Return(tasks);
|
_taskResultPool.Return(tasks);
|
||||||
|
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
@@ -226,22 +228,30 @@ public sealed class SerializeProcess(
|
|||||||
var childClosures = _childClosurePool.Get();
|
var childClosures = _childClosurePool.Get();
|
||||||
foreach (var childClosure in taskClosures)
|
foreach (var childClosure in taskClosures)
|
||||||
{
|
{
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
return EMPTY_CLOSURES;
|
||||||
|
}
|
||||||
foreach (var kvp in childClosure)
|
foreach (var kvp in childClosure)
|
||||||
{
|
{
|
||||||
childClosures[kvp.Key] = kvp.Value;
|
childClosures[kvp.Key] = kvp.Value;
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
return EMPTY_CLOSURES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentClosurePool.Return(childClosure);
|
_currentClosurePool.Return(childClosure);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = baseSerializer.Serialise(obj, childClosures, _options.SkipCacheRead, _processSource.Token);
|
var items = baseSerializer.Serialise(obj, childClosures, _options.SkipCacheRead, _processSource.Token);
|
||||||
|
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
if (IsCancelled())
|
||||||
{
|
{
|
||||||
return EMPTY_CLOSURES;
|
return EMPTY_CLOSURES;
|
||||||
}
|
}
|
||||||
@@ -253,13 +263,13 @@ public sealed class SerializeProcess(
|
|||||||
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
|
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
return EMPTY_CLOSURES;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.NeedsStorage)
|
if (item.NeedsStorage)
|
||||||
{
|
{
|
||||||
if (_processSource.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
return EMPTY_CLOSURES;
|
|
||||||
}
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _objectsSerialized);
|
Interlocked.Increment(ref _objectsSerialized);
|
||||||
await objectSaver.SaveAsync(item).ConfigureAwait(false);
|
await objectSaver.SaveAsync(item).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -290,6 +300,8 @@ public sealed class SerializeProcess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsCancelled() => _disposed || _processSource.IsCancellationRequested;
|
||||||
|
|
||||||
public void RecordException(Exception e)
|
public void RecordException(Exception e)
|
||||||
{
|
{
|
||||||
if (e is OperationCanceledException)
|
if (e is OperationCanceledException)
|
||||||
@@ -304,6 +316,11 @@ public sealed class SerializeProcess(
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
//if we are already cancelled, don't log or save the exceptions
|
||||||
|
return;
|
||||||
|
}
|
||||||
//order here matters
|
//order here matters
|
||||||
_logger.LogError(e, "Error in SDK: {message}", e.Message);
|
_logger.LogError(e, "Error in SDK: {message}", e.Message);
|
||||||
objectSaver.Exception = e;
|
objectSaver.Exception = e;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using System.Diagnostics;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Speckle.Sdk.Caching;
|
||||||
using Speckle.Sdk.Logging;
|
using Speckle.Sdk.Logging;
|
||||||
using Speckle.Sdk.Models;
|
using Speckle.Sdk.Models;
|
||||||
using Speckle.Sdk.Serialisation.Utilities;
|
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Transports;
|
namespace Speckle.Sdk.Transports;
|
||||||
@@ -28,7 +28,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
|
|||||||
{
|
{
|
||||||
_streamId = streamId;
|
_streamId = streamId;
|
||||||
|
|
||||||
_rootPath = SqlitePaths.GetDBPath(streamId);
|
_rootPath = ModelCacheManager.GetDbPath(streamId);
|
||||||
|
|
||||||
_connectionString = $"Data Source={_rootPath};";
|
_connectionString = $"Data Source={_rootPath};";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
|
|||||||
private SqliteConnection Connection { get; set; }
|
private SqliteConnection Connection { get; set; }
|
||||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||||
|
|
||||||
public string BlobStorageFolder => SqlitePaths.BlobStorageFolder;
|
public string BlobStorageFolder => SpecklePathProvider.UserSpeckleFolderPath;
|
||||||
|
|
||||||
public void SaveBlob(Blob obj)
|
public void SaveBlob(Blob obj)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"Message": "The method or operation is not implemented.",
|
"Message": "The method or operation is not implemented.",
|
||||||
"Type": "NotImplementedException"
|
"Type": "NotImplementedException"
|
||||||
},
|
},
|
||||||
"Message": "Error while sending",
|
"Message": "Error while sending: The method or operation is not implemented.",
|
||||||
"Type": "SpeckleException"
|
"Type": "SpeckleException"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"Message": "Count exceeded",
|
"Message": "Count exceeded",
|
||||||
"Type": "Exception"
|
"Type": "Exception"
|
||||||
},
|
},
|
||||||
"Message": "Error while sending",
|
"Message": "Error while sending: Count exceeded",
|
||||||
"Type": "SpeckleException"
|
"Type": "SpeckleException"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"Message": "The method or operation is not implemented.",
|
"Message": "The method or operation is not implemented.",
|
||||||
"Type": "NotImplementedException"
|
"Type": "NotImplementedException"
|
||||||
},
|
},
|
||||||
"Message": "Error while loading",
|
"Message": "Error while loading: The method or operation is not implemented.",
|
||||||
"Type": "SpeckleException"
|
"Type": "SpeckleException"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"Message": "The method or operation is not implemented.",
|
"Message": "The method or operation is not implemented.",
|
||||||
"Type": "NotImplementedException"
|
"Type": "NotImplementedException"
|
||||||
},
|
},
|
||||||
"Message": "Error while sending",
|
"Message": "Error while sending: The method or operation is not implemented.",
|
||||||
"Type": "SpeckleException"
|
"Type": "SpeckleException"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class GraphQLErrorHandlerTests
|
|||||||
];
|
];
|
||||||
yield return [typeof(SpeckleGraphQLException), new Map { { "foo", "bar" } }];
|
yield return [typeof(SpeckleGraphQLException), new Map { { "foo", "bar" } }];
|
||||||
yield return [typeof(SpeckleGraphQLException), new Map { { "code", "CUSTOM_THING" } }];
|
yield return [typeof(SpeckleGraphQLException), new Map { { "code", "CUSTOM_THING" } }];
|
||||||
|
yield return [typeof(CannotCreateCommitException), new Map { { "code", "COMMIT_CREATE_ERROR" } }];
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class SpecklePathTests
|
|||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
pattern = @"\/Users\/.*\/\.config";
|
pattern = @"\/Users\/.*\/Library\/Application Support";
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ public class SpecklePathTests
|
|||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
pattern = @"\/Users\/.*\/\.config";
|
pattern = @"\/Users\/.*\/Library\/Application Support";
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
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]
|
[Fact]
|
||||||
public void CanShallowCopy()
|
public void CanShallowCopy()
|
||||||
{
|
{
|
||||||
var sample = new SampleObject();
|
var sample = new SampleObject { id = "sampleId" };
|
||||||
|
dynamic x = sample;
|
||||||
|
x.test = "test";
|
||||||
var copy = sample.ShallowCopy();
|
var copy = sample.ShallowCopy();
|
||||||
|
|
||||||
var selectedMembers = DynamicBaseMemberType.Dynamic | DynamicBaseMemberType.Instance;
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Speckle.Sdk.Caching;
|
||||||
using Speckle.Sdk.Common;
|
using Speckle.Sdk.Common;
|
||||||
using Speckle.Sdk.Serialisation.Utilities;
|
|
||||||
using Speckle.Sdk.Transports;
|
using Speckle.Sdk.Transports;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Tests.Unit.Transports;
|
namespace Speckle.Sdk.Tests.Unit.Transports;
|
||||||
@@ -13,7 +13,7 @@ public sealed class SQLiteTransport2Tests : TransportTests, IDisposable
|
|||||||
private SQLiteTransport2? _sqlite;
|
private SQLiteTransport2? _sqlite;
|
||||||
|
|
||||||
private static readonly string s_name = $"test-{Guid.NewGuid()}";
|
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()
|
public SQLiteTransport2Tests()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user