Compare commits

...

7 Commits

Author SHA1 Message Date
Claire Kuang c3f944dcf1 Delete ViewProxy.cs (#415)
.NET Build and Publish / build (push) Has been cancelled
2025-10-28 13:52:19 +00:00
Jedd Morgan 8890f8cb36 Merge pull request #416 from specklesystems/jrm/attribute-mask-revert
feat(sdk)!: Unexpose attribute mask
2025-10-28 13:29:30 +00:00
Jedd Morgan a0eae88479 Merge branch 'dev' into jrm/attribute-mask-revert 2025-10-28 13:20:52 +00:00
Jedd Morgan 8785e49f73 Add send/receive integration tests (#412) 2025-10-28 14:12:12 +01:00
Jedd Morgan 6e35d6af6d Unexpose attribute mask 2025-10-28 10:33:47 +00:00
Claire Kuang c2735f0a32 chore(objects): update view proxy and camera classes (#408)
* Adds new camera and view proxy classes

* Update ViewProxy.cs

* removes viewproxy and orthographic prop on camera class
2025-10-24 11:46:30 +01:00
Claire Kuang 0b01091209 feat(objects): Adds new camera and view proxy classes (#407)
* Adds new camera and view proxy classes

* Update ViewProxy.cs
2025-10-22 10:53:38 +01:00
13 changed files with 275 additions and 26 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<NoWarn>
<!-- Things we need to test -->
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065;
IDE0044;IDE0130;CA1508;
<!-- Analysers that provide no tangeable value to a test project -->
CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831;
+32
View File
@@ -0,0 +1,32 @@
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
namespace Speckle.Objects.Other;
/// <summary>
/// Camera class to represent a perspective camera for a 3D view.
/// </summary>
/// <remarks>Assumes a Z-up, right-handed convention for orientation vectors</remarks>
[SpeckleType("Objects.Other.Camera")]
public class Camera : Base
{
/// <summary>
/// The name of the view that is created by this camera
/// </summary>
public required string name { get; set; }
/// <summary>
/// The location of the camera
/// </summary>
public required Point position { get; set; }
/// <summary>
/// The unit up vector of the camera
/// </summary>
public required Vector up { get; set; }
/// <summary>
/// The unit forward vector of the camera
/// </summary>
public required Vector forward { get; set; }
}
@@ -14,7 +14,7 @@ public sealed class Comment
public string rawText { get; init; }
public ResourceCollection<Comment> replies { get; init; }
public CommentReplyAuthorCollection replyAuthors { get; init; }
public List<ResourceIdentifier> resources { get; init; }
public List<ResourceIdentifier> resources { get; init; } //todo: add resourceIds/baseResourceIds
public string? screenshot { get; init; }
public DateTime updatedAt { get; init; }
public DateTime? viewedAt { get; init; }
@@ -16,6 +16,7 @@ public partial class Operations
/// <exception cref="ArgumentException">No transports were specified</exception>
/// <exception cref="ArgumentNullException">The <paramref name="objectId"/> was <see langword="null"/></exception>
/// <exception cref="SpeckleException">Serialization or Send operation was unsuccessful</exception>
/// <exception cref="HttpRequestException">HTTP layer errors</exception>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> requested cancellation</exception>
public async Task<Base> Receive2(
Uri url,
@@ -10,12 +10,11 @@ using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.V2.Receive;
public record DeserializeProcessOptions(
bool SkipCache = false,
bool SkipCache = false, //TODO: This appears to be bugged when set to `true`, `LoadId` depends on sqlite
bool ThrowOnMissingReferences = true,
bool SkipInvalidConverts = false,
int? MaxParallelism = null,
bool SkipServer = false,
string? AttributeMask = null
bool SkipServer = false
);
public partial interface IDeserializeProcess : IAsyncDisposable;
@@ -45,7 +44,6 @@ public sealed class DeserializeProcess(
new ObjectLoader(
sqLiteJsonCacheManager,
serverObjectManager,
options?.AttributeMask,
progress,
loggerFactory.CreateLogger<ObjectLoader>(),
cancellationToken
@@ -16,13 +16,10 @@ public partial interface IObjectLoader : IDisposable;
public sealed class ObjectLoader(
ISqLiteJsonCacheManager sqLiteJsonCacheManager,
IServerObjectManager serverObjectManager,
string? attributeMask,
IProgress<ProgressArgs>? progress,
ILogger<ObjectLoader> logger,
CancellationToken cancellationToken
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
) : ChannelLoader<BaseItem>(cancellationToken), IObjectLoader
#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
private int? _allChildrenCount;
private long _checkCache;
@@ -30,6 +27,7 @@ public sealed class ObjectLoader(
private long _downloaded;
private long _totalToDownload;
private DeserializeProcessOptions _options = new();
private readonly CancellationToken _cancellationToken = cancellationToken;
[AutoInterfaceIgnore]
public void Dispose() => sqLiteJsonCacheManager.Dispose();
@@ -47,7 +45,7 @@ public sealed class ObjectLoader(
{
//assume everything exists as the root is there.
var allChildren = ClosureParser
.GetClosuresSorted(rootJson, cancellationToken)
.GetClosuresSorted(rootJson, _cancellationToken)
.Select(x => new Id(x.Item1))
.ToList();
//this probably yields away from the Main thread to let host apps update progress
@@ -60,11 +58,11 @@ public sealed class ObjectLoader(
if (!options.SkipServer)
{
rootJson = await serverObjectManager
.DownloadSingleObject(rootId, progress, cancellationToken)
.DownloadSingleObject(rootId, progress, _cancellationToken)
.NotNull()
.ConfigureAwait(false);
IReadOnlyCollection<Id> allChildrenIds = ClosureParser
.GetClosures(rootJson, cancellationToken)
.GetClosures(rootJson, _cancellationToken)
.OrderByDescending(x => x.Item2)
.Select(x => new Id(x.Item1))
.Where(x => !x.Value.StartsWith("blob", StringComparison.Ordinal))
@@ -112,13 +110,13 @@ public sealed class ObjectLoader(
await foreach (
var (id, json) in serverObjectManager.DownloadObjects(
ids.Select(x => x.NotNull()).ToList(),
attributeMask,
null, //TODO: Implement attribute masking in a safe way that will not poison SQLite DB.
progress,
cancellationToken
_cancellationToken
)
)
{
cancellationToken.ThrowIfCancellationRequested();
_cancellationToken.ThrowIfCancellationRequested();
Interlocked.Increment(ref _downloaded);
progress?.Report(new(ProgressEvent.DownloadObjects, _downloaded, _totalToDownload));
toCache.Add(new(new(id), new(json), true, null));
@@ -140,7 +138,7 @@ public sealed class ObjectLoader(
{
if (!_options.SkipCache)
{
cancellationToken.ThrowIfCancellationRequested();
_cancellationToken.ThrowIfCancellationRequested();
sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value)));
Interlocked.Exchange(ref _cached, _cached + batch.Count);
progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _allChildrenCount));
@@ -170,7 +168,7 @@ public sealed class ObjectLoader(
private void ThrowIfFailed()
{
//always check for cancellation first
cancellationToken.ThrowIfCancellationRequested();
_cancellationToken.ThrowIfCancellationRequested();
if (Exception is not null)
{
throw new SpeckleException($"Error while loading: {Exception.Message}", Exception);
@@ -112,16 +112,15 @@ public class ExceptionTests
new DummySqLiteReceiveManager(new Dictionary<string, string>()),
new ExceptionServerObjectManager(),
null,
null,
new NullLogger<ObjectLoader>(),
default
CancellationToken.None
);
await using var process = new DeserializeProcess(
o,
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
default,
CancellationToken.None,
new(SkipCache: true, MaxParallelism: 1, SkipServer: true)
);
@@ -145,7 +144,7 @@ public class ExceptionTests
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
default,
CancellationToken.None,
new(true, MaxParallelism: 1)
);
@@ -170,7 +169,7 @@ public class ExceptionTests
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
default,
CancellationToken.None,
new(MaxParallelism: 1)
);
@@ -195,9 +194,7 @@ public class ExceptionTests
[SpeckleType("Objects.Geometry.BadBase")]
public class BadBase : Base
{
#pragma warning disable CA1065
public string BadProp => throw new NotImplementedException();
#pragma warning restore CA1065
}
[Fact]
@@ -202,9 +202,8 @@ public class SerializationTests
new DummySqLiteReceiveManager(closures),
new DummyReceiveServerObjectManager(closures),
null,
null,
new NullLogger<ObjectLoader>(),
default
CancellationToken.None
)
)
{
@@ -0,0 +1,6 @@
{
"Data": {},
"Message": "Response status code does not indicate success: 404 (Not Found).",
"StatusCode": "NotFound",
"Type": "HttpRequestException"
}
@@ -0,0 +1,6 @@
{
"Data": {},
"Message": "Response status code does not indicate success: 404 (Not Found).",
"StatusCode": "NotFound",
"Type": "HttpRequestException"
}
@@ -0,0 +1,4 @@
{
"ConvertedReferences": {},
"RootId": "5313a8f61e1fa7abe9bf716ddfc767bd"
}
@@ -0,0 +1,18 @@
{
"Data": {},
"InnerException": {
"$type": "SpeckleSerializeException",
"Data": {},
"InnerException": {
"$type": "ArgumentException",
"Data": {},
"Message": "Unsupported value in serialization: System.Text.StringBuilder",
"ParamName": "obj",
"Type": "ArgumentException"
},
"Message": "Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base",
"Type": "SpeckleSerializeException"
},
"Message": "Error while sending: Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base",
"Type": "SpeckleException"
}
@@ -0,0 +1,190 @@
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Tests.Integration;
public sealed class SendReceiveTests : IAsyncLifetime
{
private Project _project;
private IClient _client;
private IOperations _operations;
private const string NON_EXISTENT_OBJECT_ID = "0a480dfb7aa774f19a82bee9d6320abd";
private const string NON_EXISTENT_PROJECT_ID = "8cdc651d13";
public async Task InitializeAsync()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
var serviceProvider = TestServiceSetup.GetServiceProvider();
_operations = serviceProvider.GetRequiredService<IOperations>();
ClearCache();
_client = await Fixtures.SeedUserWithClient();
_project = await _client.Project.Create(new("Blobber", "Flobber", ProjectVisibility.Private));
}
[Fact]
public async Task SendAndReceive()
{
var myObject = Fixtures.GenerateNestedObject();
string expectedId = myObject.GetId(true);
//SEND
var fistSend = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
myObject,
null,
CancellationToken.None
);
Assert.Equal(expectedId, fistSend.RootId);
await Verify(fistSend);
//RECEIVE
var received = await _operations.Receive2(
_client.ServerUrl,
_project.id,
fistSend.RootId,
_client.Account.token,
null,
CancellationToken.None
);
Assert.Equal(expectedId, received.id);
//SEND AGAIN!
var secondSend = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
received,
null,
CancellationToken.None
);
Assert.Equal(expectedId, secondSend.RootId);
//RECEIVE AGAIN, but using cache
ClearCache();
var secondReceive = await _operations.Receive2(
_client.ServerUrl,
_project.id,
fistSend.RootId,
_client.Account.token,
null,
CancellationToken.None
);
Assert.Equal(expectedId, secondReceive.id);
}
private void ClearCache() { }
[Fact]
public async Task ReceiveNonExistentObjectThrows()
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
CancellationToken.None,
new(true)
);
});
await Verify(ex);
}
[Fact]
public async Task ReceiveNonExistentProjectThrows()
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
NON_EXISTENT_PROJECT_ID,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
CancellationToken.None,
new(true)
);
});
await Verify(ex);
}
[Fact]
public async Task SendInvalidData()
{
var myObject = Fixtures.GenerateNestedObject();
myObject["invalidProp"] = new StringBuilder(); //Serializer does not support serializing this type
var ex = await Assert.ThrowsAsync<SpeckleException>(async () =>
{
_ = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
myObject,
null,
CancellationToken.None,
new(SkipCacheRead: true, SkipCacheWrite: true)
);
});
await Verify(ex);
}
[Fact]
public async Task ReceiveNonAuthThrows()
{
using IClient unauthed = Fixtures.Unauthed;
await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
unauthed.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
unauthed.Account.token,
null,
CancellationToken.None,
new(true)
);
});
}
[Fact]
public async Task ReceiveCancellation()
{
using CancellationTokenSource ct = new();
await ct.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
ct.Token,
new(true)
);
});
}
public Task DisposeAsync()
{
_client?.Dispose();
return Task.CompletedTask;
}
}