Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c186d98ea7 | |||
| 00a6619cbe | |||
| 49ef9917c4 | |||
| 94b0473157 | |||
| 8071990dd5 | |||
| 8c7dbc89aa | |||
| 676a3df153 | |||
| c75538e1c7 | |||
| 5d10b77ee4 | |||
| 82dca56fbd | |||
| 80d1df8eca | |||
| b5796245aa | |||
| 639c774f80 | |||
| 3bb5d1e73a | |||
| e01360ad03 | |||
| 2494b160e8 | |||
| 0aacc3fe89 | |||
| 3c0b9e8b1c | |||
| 6568781275 | |||
| 6740659af4 | |||
| 701013ad46 | |||
| fdc0842b03 | |||
| 23d5dd44bc |
@@ -6,10 +6,12 @@ on:
|
||||
docker-compose-file:
|
||||
required: true
|
||||
type: string
|
||||
use-github-container-registry:
|
||||
use-internal-image:
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
integration-test:
|
||||
env:
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
@@ -27,7 +29,7 @@ jobs:
|
||||
cache-dependency-path: "**/packages.lock.json"
|
||||
|
||||
- name: 🔐 Login to Github Container Registry
|
||||
if: ${{ inputs.use-github-container-registry }}
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
@@ -43,11 +45,18 @@ jobs:
|
||||
- name: 🏗️ Build
|
||||
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
|
||||
|
||||
- name: 🔨 Integration Tests
|
||||
run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
- name: 🔨 Integration Tests against Public Server
|
||||
if: ${{ !inputs.use-internal-image }}
|
||||
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Internal)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: 🔨 Integration Tests against Internal Server
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
@@ -39,7 +39,9 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -47,9 +49,13 @@ jobs:
|
||||
uses: "./.github/workflows/integration-test.yml"
|
||||
with:
|
||||
docker-compose-file: "docker-compose-internal.yml"
|
||||
use-github-container-registry: true
|
||||
|
||||
use-internal-image: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
integration-test-public:
|
||||
uses: "./.github/workflows/integration-test.yml"
|
||||
with:
|
||||
docker-compose-file: "docker-compose.yml"
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
@@ -48,7 +48,9 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dotnet 8.0.400
|
||||
@@ -10,6 +10,7 @@
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Build.Targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="docker-compose-internal.yml" />
|
||||
<File Path="docker-compose.yml" />
|
||||
<File Path="global.json" />
|
||||
<File Path="README.md" />
|
||||
@@ -17,6 +18,7 @@
|
||||
<File Path=".github\git-commit-instructions.md" />
|
||||
</Folder>
|
||||
<Folder Name="/config/workflows/">
|
||||
<File Path=".github/workflows/integration-test.yml" />
|
||||
<File Path=".github/workflows/pr.yml" />
|
||||
<File Path=".github/workflows/release.yml" />
|
||||
</Folder>
|
||||
|
||||
@@ -97,10 +97,7 @@ services:
|
||||
|
||||
STRATEGY_LOCAL: "true"
|
||||
|
||||
POSTGRES_URL: "postgres"
|
||||
POSTGRES_USER: "speckle"
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
POSTGRES_URL: 'postgres://speckle:speckle@postgres:5432/speckle'
|
||||
ENABLE_MP: "false"
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
@@ -63,7 +63,7 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
|
||||
);
|
||||
}
|
||||
|
||||
Base? rootObject = await operations
|
||||
Base rootObject = await operations
|
||||
.Receive2(
|
||||
SpeckleClient.ServerUrl,
|
||||
AutomationRunData.ProjectId,
|
||||
@@ -74,6 +74,10 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await SpeckleClient
|
||||
.Version.Received(new(version.id, AutomationRunData.ProjectId, "automate_function"), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine($"It took {Elapsed.TotalSeconds} seconds to receive the speckle version {versionId}");
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Objects.Geometry;
|
||||
|
||||
[SpeckleType("Objects.Geometry.SolidX")]
|
||||
public class SolidX : RawEncodedObject;
|
||||
@@ -20,4 +20,6 @@ public class RawEncoding : Base // note: at this stage, since we're using this f
|
||||
public static class RawEncodingFormats
|
||||
{
|
||||
public const string RHINO_3DM = "3dm";
|
||||
public const string ACAD_DWG = "dwg";
|
||||
public const string ACAD_SAT = "sat";
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Speckle.Sdk.Dependencies;
|
||||
|
||||
internal sealed class BroadcastChannel<T>
|
||||
{
|
||||
private readonly List<Channel<T>> _subscribers = [];
|
||||
|
||||
public ChannelReader<T> Subscribe()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<T>(new UnboundedChannelOptions() { SingleReader = true });
|
||||
_subscribers.Add(channel);
|
||||
return channel.Reader;
|
||||
}
|
||||
|
||||
public async Task WriteAsync(T item, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var sub in _subscribers)
|
||||
{
|
||||
await sub.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadingCompleted()
|
||||
{
|
||||
return _subscribers.All(x => x.Reader.Completion.IsCompleted);
|
||||
}
|
||||
|
||||
public void CompleteWriters()
|
||||
{
|
||||
foreach (var sub in _subscribers)
|
||||
{
|
||||
sub.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CompleteReaders()
|
||||
{
|
||||
await Task.WhenAll(_subscribers.Select(x => x.Reader.Completion)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,28 @@ namespace Speckle.Sdk.Serialisation.V2.Send;
|
||||
public sealed class Batch<T> : IMemoryOwner<T>
|
||||
where T : IHasByteSize
|
||||
{
|
||||
private static readonly Pool<List<T>> s_pool = Pools.CreateListPool<T>();
|
||||
public List<T> Items { get; } = s_pool.Get();
|
||||
public int BatchByteSize { get; private set; }
|
||||
private static readonly Pool<List<T>> _pool = Pools.CreateListPool<T>();
|
||||
#pragma warning disable IDE0032
|
||||
private readonly List<T> _items = _pool.Get();
|
||||
private int _batchByteSize;
|
||||
#pragma warning restore IDE0032
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
Items.Add(item);
|
||||
BatchByteSize += item.ByteSize;
|
||||
_items.Add(item);
|
||||
_batchByteSize += item.ByteSize;
|
||||
}
|
||||
|
||||
public void TrimExcess()
|
||||
{
|
||||
Items.TrimExcess();
|
||||
BatchByteSize = Items.Sum(x => x.ByteSize);
|
||||
_items.TrimExcess();
|
||||
_batchByteSize = _items.Sum(x => x.ByteSize);
|
||||
}
|
||||
|
||||
public void Dispose() => s_pool.Return(Items);
|
||||
public int BatchByteSize => _batchByteSize;
|
||||
public List<T> Items => _items;
|
||||
|
||||
public Memory<T> Memory => new(Items.ToArray());
|
||||
public void Dispose() => _pool.Return(_items);
|
||||
|
||||
public Memory<T> Memory => new(_items.ToArray());
|
||||
}
|
||||
|
||||
@@ -1,134 +1,74 @@
|
||||
using System.Buffers;
|
||||
using System.Threading.Channels;
|
||||
using Open.ChannelExtensions;
|
||||
using Speckle.Sdk.Serialisation.V2.Send;
|
||||
|
||||
namespace Speckle.Sdk.Dependencies.Serialization;
|
||||
|
||||
public abstract class ChannelSaver<TItem, TBlobItem>
|
||||
where TItem : IHasByteSize
|
||||
where TBlobItem : IHasByteSize, TItem
|
||||
public abstract class ChannelSaver<T>
|
||||
where T : IHasByteSize
|
||||
{
|
||||
private const int SEND_CAPACITY = 10000;
|
||||
private const int HTTP_SEND_CHUNK_SIZE = 25_000_000; //bytes
|
||||
private const int BLOB_SEND_CHUNK_SIZE = 10; //count
|
||||
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
|
||||
private const int MAX_PARALLELISM_HTTP = 4;
|
||||
private const int HTTP_CAPACITY = 500;
|
||||
private const int MAX_CACHE_WRITE_PARALLELISM = 1;
|
||||
private const int MAX_CACHE_BATCH = 1000;
|
||||
|
||||
private readonly BroadcastChannel<TItem> _broadcastChannel = new();
|
||||
private readonly Channel<T> _checkCacheChannel = Channel.CreateBounded<T>(
|
||||
new BoundedChannelOptions(SEND_CAPACITY)
|
||||
{
|
||||
AllowSynchronousContinuations = true,
|
||||
Capacity = SEND_CAPACITY,
|
||||
SingleWriter = false,
|
||||
SingleReader = false,
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
},
|
||||
_ => throw new NotImplementedException("Dropping items not supported.")
|
||||
);
|
||||
|
||||
public async Task Start(
|
||||
public Task Start(
|
||||
int? maxParallelism,
|
||||
int? httpBatchSize,
|
||||
int? blobSendCache,
|
||||
int? cacheBatchSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
maxParallelism ??= MAX_PARALLELISM_HTTP;
|
||||
httpBatchSize ??= HTTP_SEND_CHUNK_SIZE;
|
||||
blobSendCache ??= BLOB_SEND_CHUNK_SIZE;
|
||||
cacheBatchSize ??= MAX_CACHE_BATCH;
|
||||
await StartInternal(
|
||||
maxParallelism.Value,
|
||||
httpBatchSize.Value,
|
||||
blobSendCache.Value,
|
||||
cacheBatchSize.Value,
|
||||
) =>
|
||||
_checkCacheChannel
|
||||
.Reader.BatchByByteSize(httpBatchSize ?? HTTP_SEND_CHUNK_SIZE)
|
||||
.WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
.PipeAsync(
|
||||
maxParallelism ?? MAX_PARALLELISM_HTTP,
|
||||
async x => await SendToServer(x).ConfigureAwait(false),
|
||||
HTTP_CAPACITY,
|
||||
false,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task StartInternal(
|
||||
int maxParallelism,
|
||||
int httpBatchSize,
|
||||
int blobSendCache,
|
||||
int cacheBatchSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
Task serverSend = _broadcastChannel
|
||||
.Subscribe()
|
||||
.BatchByByteSize(httpBatchSize)
|
||||
.Join()
|
||||
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
|
||||
.WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
.ReadAllConcurrentlyAsync(
|
||||
maxParallelism,
|
||||
async x => await SendToServer(x).ConfigureAwait(false),
|
||||
cancellationToken
|
||||
.ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
|
||||
.ContinueWith(
|
||||
t =>
|
||||
{
|
||||
Exception? ex = t.Exception;
|
||||
if (ex is null && t.Status is TaskStatus.Canceled && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
ex = new OperationCanceledException();
|
||||
}
|
||||
|
||||
if (ex is not null)
|
||||
{
|
||||
RecordException(ex);
|
||||
}
|
||||
_checkCacheChannel.Writer.TryComplete(ex);
|
||||
},
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Current
|
||||
);
|
||||
|
||||
Task writeCache = _broadcastChannel
|
||||
.Subscribe()
|
||||
.Batch(cacheBatchSize)
|
||||
.ReadAll(SaveToCache, true, cancellationToken: cancellationToken)
|
||||
.AsTask();
|
||||
|
||||
Task blobsCache = _broadcastChannel
|
||||
.Subscribe()
|
||||
.OfType<TItem, TBlobItem>()
|
||||
.BatchByByteSize(blobSendCache)
|
||||
.ReadAllAsync(
|
||||
async x => await SendBlobToServer(x).ConfigureAwait(false),
|
||||
true,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
.AsTask();
|
||||
|
||||
return Task.WhenAll(serverSend, writeCache, blobsCache);
|
||||
|
||||
// return _broadcastChannel
|
||||
// .Subscribe()
|
||||
// .BatchByByteSize(httpBatchSize ?? HTTP_SEND_CHUNK_SIZE)
|
||||
// .WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
// .PipeAsync(
|
||||
// maxParallelism ?? MAX_PARALLELISM_HTTP,
|
||||
// async x => await SendToServer(x).ConfigureAwait(false),
|
||||
// HTTP_CAPACITY,
|
||||
// false,
|
||||
// cancellationToken
|
||||
// )
|
||||
// .Join()
|
||||
// .Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
|
||||
// .WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
// .ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
|
||||
// .ContinueWith(
|
||||
// t =>
|
||||
// {
|
||||
// Exception? ex = t.Exception;
|
||||
// if (ex is null && t.Status is TaskStatus.Canceled && !cancellationToken.IsCancellationRequested)
|
||||
// {
|
||||
// ex = new OperationCanceledException();
|
||||
// }
|
||||
//
|
||||
// if (ex is not null)
|
||||
// {
|
||||
// RecordException(ex);
|
||||
// }
|
||||
//
|
||||
// _checkCacheChannel.Writer.TryComplete(ex);
|
||||
// },
|
||||
// cancellationToken,
|
||||
// TaskContinuationOptions.ExecuteSynchronously,
|
||||
// TaskScheduler.Current
|
||||
// );
|
||||
}
|
||||
|
||||
private async ValueTask SendBlobToServer(IMemoryOwner<TBlobItem> batch)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendBlobToServerInternal((Batch<TBlobItem>)batch).ConfigureAwait(false);
|
||||
}
|
||||
#pragma warning disable CA1031
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031
|
||||
{
|
||||
RecordException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task SendBlobToServerInternal(Batch<TBlobItem> batch);
|
||||
|
||||
public async Task SaveAsync(TItem item, CancellationToken cancellationToken)
|
||||
public async Task SaveAsync(T item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Exception is not null)
|
||||
{
|
||||
@@ -136,34 +76,36 @@ public abstract class ChannelSaver<TItem, TBlobItem>
|
||||
}
|
||||
//can switch to check then try pattern when back pressure is needed or exceptions are too much
|
||||
//the trees don't need to respond to back pressure
|
||||
await _broadcastChannel.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
await _checkCacheChannel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendToServer(IMemoryOwner<TItem> batch)
|
||||
private async Task<IMemoryOwner<T>> SendToServer(IMemoryOwner<T> batch)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendToServerInternal((Batch<TItem>)batch).ConfigureAwait(false);
|
||||
await SendToServerInternal((Batch<T>)batch).ConfigureAwait(false);
|
||||
return batch;
|
||||
}
|
||||
#pragma warning disable CA1031
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031
|
||||
{
|
||||
RecordException(ex);
|
||||
return batch;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task SendToServerInternal(Batch<TItem> batch);
|
||||
protected abstract Task SendToServerInternal(Batch<T> batch);
|
||||
|
||||
public abstract void SaveToCache(List<TItem> item);
|
||||
public abstract void SaveToCache(List<T> item);
|
||||
|
||||
public void DoneTraversing() => _broadcastChannel.CompleteWriters();
|
||||
public void DoneTraversing() => _checkCacheChannel.Writer.TryComplete();
|
||||
|
||||
public async Task DoneSaving()
|
||||
{
|
||||
if (!_broadcastChannel.IsReadingCompleted())
|
||||
if (!_checkCacheChannel.Reader.Completion.IsCompleted)
|
||||
{
|
||||
await _broadcastChannel.CompleteReaders().ConfigureAwait(false);
|
||||
await _checkCacheChannel.Reader.Completion.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,5 +114,6 @@ public abstract class ChannelSaver<TItem, TBlobItem>
|
||||
private void RecordException(Exception ex)
|
||||
{
|
||||
Exception = ex;
|
||||
_checkCacheChannel.Writer.TryComplete(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
public WorkspaceResource Workspace { get; }
|
||||
public ServerResource Server { get; }
|
||||
public FileImportResource FileImport { get; }
|
||||
public ModelIngestionResource Ingestion { get; }
|
||||
|
||||
public Uri ServerUrl => new(Account.serverInfo.url);
|
||||
|
||||
@@ -71,6 +72,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
Workspace = new(this);
|
||||
Server = new(this);
|
||||
FileImport = new(this, blobApiFactory.Create(account));
|
||||
Ingestion = new(this);
|
||||
}
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
//This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
||||
/// <remarks>
|
||||
/// This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
||||
/// </remarks>
|
||||
public enum FileUploadConversionStatus
|
||||
{
|
||||
Queued = 0,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ModelIngestionStatus
|
||||
{
|
||||
cancelled,
|
||||
failed,
|
||||
processing,
|
||||
queued,
|
||||
success,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectCommentsUpdatedMessageType
|
||||
{
|
||||
ARCHIVED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectFileImportUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectModelIngestionUpdatedMessageType
|
||||
{
|
||||
cancellationRequested,
|
||||
created,
|
||||
deleted,
|
||||
updated,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectModelsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectPendingModelsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectUpdatedMessageType
|
||||
{
|
||||
DELETED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectVersionsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectVisibility
|
||||
{
|
||||
Private,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ResourceType
|
||||
{
|
||||
commit,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum UserProjectsUpdatedMessageType
|
||||
{
|
||||
ADDED,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
public record GenerateFileUploadUrlInput(string projectId, string fileName);
|
||||
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public record StartFileImportInput(string projectId, string modelId, string fileId, string etag);
|
||||
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public record FileImportResult(
|
||||
double durationSeconds,
|
||||
double downloadDurationSeconds,
|
||||
@@ -14,14 +16,23 @@ public record FileImportResult(
|
||||
|
||||
public abstract class FileImportInputBase
|
||||
{
|
||||
internal const string FILE_IMPORT_DEPRECATION_MESSAGE =
|
||||
"Part of the old API surface and will be removed in the future. Use the new ingestion API instead. Field will be deleted on June 1st, 2026";
|
||||
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
protected FileImportInputBase() { }
|
||||
|
||||
public required string projectId { get; init; }
|
||||
public required string jobId { get; init; }
|
||||
public required IReadOnlyCollection<string> warnings { get; init; }
|
||||
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public required FileImportResult result { get; init; }
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 //Mark members as static
|
||||
|
||||
[Obsolete(FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public sealed class FileImportSuccessInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "success";
|
||||
@@ -29,6 +40,7 @@ public sealed class FileImportSuccessInput() : FileImportInputBase()
|
||||
public string status => TYPE_STATUS;
|
||||
}
|
||||
|
||||
[Obsolete(FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public sealed class FileImportErrorInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "error";
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public record SourceDataInput(
|
||||
string sourceApplicationSlug,
|
||||
string sourceApplicationVersion,
|
||||
string? fileName,
|
||||
long? fileSizeBytes
|
||||
);
|
||||
|
||||
public record ModelIngestionCreateInput(
|
||||
string modelId,
|
||||
string projectId,
|
||||
string progressMessage,
|
||||
SourceDataInput sourceData
|
||||
);
|
||||
|
||||
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
||||
|
||||
public record ModelIngestionSuccessInput(
|
||||
string ingestionId,
|
||||
string projectId,
|
||||
string rootObjectId,
|
||||
string? versionMessage
|
||||
);
|
||||
|
||||
public record ModelIngestionFailedInput(
|
||||
string ingestionId,
|
||||
string projectId,
|
||||
string errorReason,
|
||||
string? errorStacktrace
|
||||
)
|
||||
{
|
||||
public static ModelIngestionFailedInput FromException(string ingestionId, string projectId, Exception ex)
|
||||
{
|
||||
return new ModelIngestionFailedInput(ingestionId, projectId, ex.Message, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public record ModelIngestionCancelledInput(string ingestionId, string projectId, string cancellationMessage);
|
||||
|
||||
public record ModelIngestionStartProcessingInput(
|
||||
string ingestionId,
|
||||
string projectId,
|
||||
string progressMessage,
|
||||
SourceDataInput sourceData
|
||||
);
|
||||
|
||||
public record ModelIngestionRequeueInput(string ingestionId, string projectId, string progressMessage);
|
||||
|
||||
public record ProjectModelIngestionSubscriptionInput(
|
||||
string projectId,
|
||||
ModelIngestionReference ingestionReference,
|
||||
[property: JsonIgnore] ProjectModelIngestionUpdatedMessageType messageType
|
||||
)
|
||||
{
|
||||
// The Newtonsoft serializer is setup to handle SCREAMING_CASE enums.
|
||||
// But the API requires the enum to look exactly like they are
|
||||
[JsonProperty(nameof(messageType))]
|
||||
public string serializedType => messageType.ToString();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <c>@oneOf</c> i.e. server expects <b>either</b> <paramref name="ingestionId"/> or <paramref name="modelId"/>, but not both.
|
||||
/// </remarks>
|
||||
/// <param name="ingestionId"></param>
|
||||
/// <param name="modelId"></param>
|
||||
public record ModelIngestionReference(string? ingestionId, string? modelId);
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public record UpdateVersionInput(string versionId, string projectId, string? message);
|
||||
|
||||
@@ -16,6 +18,10 @@ public record CreateVersionInput(
|
||||
IReadOnlyList<string>? parents = null
|
||||
);
|
||||
|
||||
/// <param name="versionId"></param>
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="sourceApplication">IMPORTANT: this is meant to be the slug of the application that has done the receiving, not to be confused with <see cref="Version.sourceApplication"/></param>
|
||||
/// <param name="message"></param>
|
||||
public record MarkReceivedVersionInput(
|
||||
string versionId,
|
||||
string projectId,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelIngestion
|
||||
{
|
||||
public required string id { get; init; }
|
||||
public required DateTime createdAt { get; init; }
|
||||
public required DateTime updatedAt { get; init; }
|
||||
public required string modelId { get; init; }
|
||||
public required bool cancellationRequested { get; init; }
|
||||
public required ModelIngestionStatusData statusData { get; init; }
|
||||
// public required LimitedUser user { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelIngestionStatusData
|
||||
{
|
||||
public required ModelIngestionStatus status { get; init; }
|
||||
public required string? progressMessage { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelPermissionChecks
|
||||
{
|
||||
public PermissionCheckResult canUpdate { get; init; }
|
||||
public PermissionCheckResult canDelete { get; init; }
|
||||
public PermissionCheckResult canCreateVersion { get; init; }
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public sealed class PendingStreamCollaborator
|
||||
public string projectName { get; init; }
|
||||
public string title { get; init; }
|
||||
public string role { get; init; }
|
||||
public LimitedUser invitedBy { get; init; }
|
||||
public LimitedUser? invitedBy { get; init; }
|
||||
public LimitedUser? user { get; init; }
|
||||
public string? token { get; init; }
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@ public sealed class ProjectPermissionChecks
|
||||
public PermissionCheckResult canCreateModel { get; init; }
|
||||
public PermissionCheckResult canDelete { get; init; }
|
||||
public PermissionCheckResult canLoad { get; init; }
|
||||
|
||||
[Obsolete("Use ModelPermissionChecks.CanCreateVersion instead", true)]
|
||||
public PermissionCheckResult canPublish { get; init; }
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
public sealed class UserProjectsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public UserProjectsUpdatedMessageType type { get; init; }
|
||||
public required UserProjectsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Project? project { get; init; }
|
||||
}
|
||||
@@ -17,10 +17,10 @@ public sealed class UserProjectsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectCommentsUpdatedMessageType type { get; init; }
|
||||
public required ProjectCommentsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Comment? comment { get; init; }
|
||||
}
|
||||
@@ -28,10 +28,10 @@ public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectFileImportUpdatedMessageType type { get; init; }
|
||||
public required ProjectFileImportUpdatedMessageType type { get; init; }
|
||||
|
||||
public FileUpload? upload { get; init; }
|
||||
}
|
||||
@@ -39,10 +39,10 @@ public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
||||
public sealed class ProjectModelsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectModelsUpdatedMessageType type { get; init; }
|
||||
public required ProjectModelsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Model? model { get; init; }
|
||||
}
|
||||
@@ -50,10 +50,10 @@ public sealed class ProjectModelsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectPendingModelsUpdatedMessageType type { get; init; }
|
||||
public required ProjectPendingModelsUpdatedMessageType type { get; init; }
|
||||
|
||||
public FileUpload? model { get; init; }
|
||||
}
|
||||
@@ -61,10 +61,10 @@ public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectUpdatedMessageType type { get; init; }
|
||||
public required ProjectUpdatedMessageType type { get; init; }
|
||||
|
||||
public Project? project { get; init; }
|
||||
}
|
||||
@@ -72,13 +72,22 @@ public sealed class ProjectUpdatedMessage : EventArgs
|
||||
public sealed class ProjectVersionsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectVersionsUpdatedMessageType type { get; init; }
|
||||
public required ProjectVersionsUpdatedMessageType type { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public string modelId { get; init; }
|
||||
public required string modelId { get; init; }
|
||||
|
||||
public Version? version { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ProjectModelIngestionUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public required ModelIngestion modelIngestion { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required ProjectModelIngestionUpdatedMessageType type { get; init; }
|
||||
}
|
||||
|
||||
@@ -397,11 +397,6 @@ public sealed class ActiveUserResource
|
||||
authorized
|
||||
message
|
||||
}
|
||||
canPublish {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ public sealed class FileImportResource : IDisposable
|
||||
/// <remarks>
|
||||
/// Only use this if you are writing a file importer, that is responsible for
|
||||
/// processing file import jobs.
|
||||
/// Only works on servers version >=2.25.8
|
||||
/// Only works on servers version >=2.25.8 but from 3.0.7 onwards has been deprecated and replaced by model ingestion api
|
||||
/// see <see cref="ModelIngestionResource.Complete"/>
|
||||
/// </remarks>
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public async Task<bool> FinishFileImportJob(FileImportInputBase input, CancellationToken cancellationToken)
|
||||
{
|
||||
//language=graphql
|
||||
@@ -57,7 +59,11 @@ public sealed class FileImportResource : IDisposable
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
/// <remarks>
|
||||
/// Only works on servers version >=2.25.8 but from 3.0.7 onwards has been deprecated and replaced by model ingestion api
|
||||
/// see <see cref="ModelIngestionResource.StartProcessing"/>
|
||||
/// </remarks>
|
||||
[Obsolete(FileImportInputBase.FILE_IMPORT_DEPRECATION_MESSAGE)]
|
||||
public async Task<FileImport> StartFileImportJob(
|
||||
StartFileImportInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
public sealed class ModelIngestionResource
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
|
||||
internal ModelIngestionResource(ISpeckleGraphQLClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new model ingestion
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The model ingestion created will have a <c>processing</c> state (not <c>queued</c>). This mutation is designed to be used
|
||||
/// by client/connectors that are immediately processing
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> Create(
|
||||
ModelIngestionCreateInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionCreate($input: ModelIngestionCreateInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: create(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="modelIngestionId"></param>
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> Get(
|
||||
string modelIngestionId,
|
||||
string projectId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query Query($projectId: String!, $modelIngestionId: ID!) {
|
||||
data:project(id: $projectId) {
|
||||
data:ingestion(id: $modelIngestionId) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelIngestionId } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<ModelIngestion>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> StartProcessing(
|
||||
ModelIngestionStartProcessingInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionStartProcessing($input: ModelIngestionStartProcessingInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: startProcessing(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> Requeue(
|
||||
ModelIngestionRequeueInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionStartProcessing($input: ModelIngestionRequeueInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: requeue(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> UpdateProgress(
|
||||
ModelIngestionUpdateInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionUpdateProgress(
|
||||
$input: ModelIngestionUpdateInput!
|
||||
) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: updateProgress(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request that the server completes the ingestion by creating a version
|
||||
/// If successful, the job will be in a terminal "successful" state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>The version id</returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<string> Complete(ModelIngestionSuccessInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionComplete($input: ModelIngestionSuccessInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: completeWithVersion(input: $input) {
|
||||
data:statusData {
|
||||
... on ModelIngestionSuccessStatus {
|
||||
data:versionId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<
|
||||
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<string>>>>>
|
||||
>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail the job with an error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> FailWithError(
|
||||
ModelIngestionFailedInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionFailWithError($input: ModelIngestionFailedInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: failWithError(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail the ingestion with a <c>canceled</c> status.
|
||||
/// This should only be done if the user has explicitly requested cancellation
|
||||
/// Other forms of cancellation use <see cref="FailWithError"/>.
|
||||
/// The ingestion should then enter a terminal "canceled" state.<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </summary>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> FailWithCancel(
|
||||
ModelIngestionCancelledInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionFailWithCancel($input: ModelIngestionCancelledInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: failWithCancel(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request that the <see cref="ModelIngestion"/> is canceled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note simply calling this mutation does not imediatly cancel, it doesn't even guarantee it will be canceled at all.
|
||||
/// It's up to the client to observe this cancellation request
|
||||
/// via <see cref="SubscriptionResource.CreateProjectModelIngestionCancellationRequestedSubscription"/>
|
||||
/// and report it as canceled via <see cref="ModelIngestionResource.FailWithCancel"/>
|
||||
/// See "cooperative cancellation pattern"<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> RequestCancellation(
|
||||
ModelIngestionCancelledInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionRequestCancellation($input: ModelIngestionRequestCancellationInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: requestCancellation (input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
}
|
||||
@@ -312,4 +312,88 @@ public sealed class ModelResource
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelPermissionChecks> GetPermissions(
|
||||
string projectId,
|
||||
string modelId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:permissions {
|
||||
canUpdate {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canDelete {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canCreateVersion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelPermissionChecks>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="SpeckleGraphQLBadInputException">server versions <3.0.11 do not have <c>canCreateIngestion</c> and will throw this exception</exception>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<PermissionCheckResult> CanCreateModelIngestion(
|
||||
string projectId,
|
||||
string modelId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:permissions {
|
||||
data:canCreateIngestion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<
|
||||
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<PermissionCheckResult>>>>
|
||||
>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
@@ -212,6 +213,60 @@ public sealed class SubscriptionResource : IDisposable
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionUpdatedSubscription(
|
||||
ProjectModelIngestionSubscriptionInput input
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
subscription IngestionUpdated($input: ProjectModelIngestionSubscriptionInput!) {
|
||||
data: projectModelIngestionUpdated(input: $input) {
|
||||
modelIngestion {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
Subscription<ProjectModelIngestionUpdatedMessage> subscription = new(_client, request);
|
||||
_subscriptions.Add(subscription);
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionCancellationRequestedSubscription(
|
||||
string ingestionId,
|
||||
string projectId
|
||||
)
|
||||
{
|
||||
return CreateProjectModelIngestionUpdatedSubscription(
|
||||
new ProjectModelIngestionSubscriptionInput(
|
||||
projectId,
|
||||
new(ingestionId, null),
|
||||
ProjectModelIngestionUpdatedMessageType.cancellationRequested
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
schema: https://app.speckle.systems/graphql
|
||||
schema: https://latest.speckle.systems/graphql
|
||||
documents: '**/*.graphql'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
#if NET6_0_OR_GREATER
|
||||
@@ -9,58 +8,47 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Speckle.Sdk.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for hashing data to a hex string
|
||||
/// </summary>
|
||||
public static class Sha256
|
||||
{
|
||||
public const string DEFAULT_FORMAT = "x2";
|
||||
public const int HASH_SIZE_CHARS = 64; // SHA256.HashSizeInBytes * sizeof(char)
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <param name="input">the value to hash</param>
|
||||
/// <param name="destination">Output hash; it must have <c>2 ≤ Length ≤ 64</c>, and must be a multiple of 2</param>
|
||||
/// <param name="formatUpperCase"><see langword="true"/> for upper case, false otherwise</param>
|
||||
public static void Hash(ReadOnlySpan<char> input, bool formatUpperCase, Span<char> destination)
|
||||
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
|
||||
/// <param name="length">Desired length of the returned string. Must be 2 ≤ Length ≤ 64, and must be a multiple of 2</param>
|
||||
/// <returns><inheritdoc cref="GetString(string, string?, int)"/></returns>
|
||||
[Pure]
|
||||
public static string GetString(
|
||||
ReadOnlySpan<char> input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
|
||||
int length = SHA256.HashSizeInBytes * sizeof(char)
|
||||
)
|
||||
{
|
||||
ReadOnlySpan<byte> inputBytes = MemoryMarshal.AsBytes(input);
|
||||
Hash(inputBytes, formatUpperCase, destination);
|
||||
}
|
||||
|
||||
public static void Hash(ReadOnlySpan<byte> input, bool formatUpperCase, Span<char> destination)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(input, hash);
|
||||
SHA256.HashData(inputBytes, hash);
|
||||
|
||||
FormatHash(hash, formatUpperCase, destination);
|
||||
}
|
||||
Span<char> output = stackalloc char[length];
|
||||
|
||||
public static void Hash(Stream source, bool formatUpperCase, Span<char> destination)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(source, hash);
|
||||
|
||||
FormatHash(hash, formatUpperCase, destination);
|
||||
}
|
||||
|
||||
private static void FormatHash(ReadOnlySpan<byte> input, bool formatUpperCase, Span<char> output)
|
||||
{
|
||||
for (int i = 0, j = 0; j < output.Length; i += sizeof(byte), j += sizeof(char))
|
||||
for (int i = 0, j = 0; j < length; i += sizeof(byte), j += sizeof(char))
|
||||
{
|
||||
input[i].TryFormat(output[j..], out _, formatUpperCase ? "X2" : "x2");
|
||||
hash[i].TryFormat(output[j..], out _, format);
|
||||
}
|
||||
|
||||
return new string(output);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <param name="input">the value to hash</param>
|
||||
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
|
||||
/// <param name="outputLengthChars">Desired length of the returned string</param>
|
||||
/// <param name="length">Desired length of the returned string</param>
|
||||
/// <returns>the hash string</returns>
|
||||
/// <exception cref="FormatException"><paramref name="format"/> is not a recognised numeric format</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><inheritdoc cref="StringBuilder.ToString(int, int)"/></exception>
|
||||
[Pure]
|
||||
public static string Hash(
|
||||
public static string GetString(
|
||||
string input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = DEFAULT_FORMAT,
|
||||
int outputLengthChars = HASH_SIZE_CHARS
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
|
||||
int length = 64
|
||||
)
|
||||
{
|
||||
var inputBytes = Encoding.Unicode.GetBytes(input);
|
||||
@@ -71,43 +59,12 @@ public static class Sha256
|
||||
byte[] hash = sha256.ComputeHash(inputBytes);
|
||||
#endif
|
||||
|
||||
StringBuilder sb = new(HASH_SIZE_CHARS);
|
||||
StringBuilder sb = new(64);
|
||||
foreach (byte b in hash)
|
||||
{
|
||||
sb.Append(b.ToString(format));
|
||||
}
|
||||
|
||||
return sb.ToString(0, outputLengthChars);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Hash(string, string?, int)"/>
|
||||
[Pure]
|
||||
public static string Hash(
|
||||
Stream input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = DEFAULT_FORMAT,
|
||||
int outputLengthChars = HASH_SIZE_CHARS
|
||||
)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
byte[] hash = SHA256.HashData(input);
|
||||
#else
|
||||
using var sha256 = SHA256.Create();
|
||||
byte[] hash = sha256.ComputeHash(input);
|
||||
#endif
|
||||
|
||||
return FormatHash(hash, format, outputLengthChars);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string FormatHash(byte[] hash, string? format, int outputLengthChars)
|
||||
{
|
||||
StringBuilder sb = new(HASH_SIZE_CHARS);
|
||||
foreach (byte b in hash)
|
||||
{
|
||||
sb.Append(b.ToString(format));
|
||||
}
|
||||
|
||||
return sb.ToString(0, outputLengthChars);
|
||||
return sb.ToString(0, length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,22 @@ internal static class TypeLoader
|
||||
return typeof(Base);
|
||||
}
|
||||
|
||||
//Don't use unless you're testing
|
||||
/// <summary>
|
||||
/// For testing purposes only
|
||||
/// </summary>
|
||||
internal static void ReInitialize(params Assembly[] assemblies)
|
||||
{
|
||||
lock (s_availableTypes)
|
||||
{
|
||||
Reset();
|
||||
Load(assemblies);
|
||||
s_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For testing purposes only
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
s_availableTypes = new();
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
|
||||
namespace Speckle.Sdk.Models;
|
||||
|
||||
[SpeckleType("Speckle.Core.Models.Blob")]
|
||||
public sealed class Blob : Base
|
||||
public class Blob : Base
|
||||
{
|
||||
[JsonIgnore]
|
||||
public static int LocalHashPrefixLength => 20;
|
||||
|
||||
private string _filePath;
|
||||
private string? _hash;
|
||||
private string _hash;
|
||||
private bool _isHashExpired = true;
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public Blob() { }
|
||||
|
||||
public Blob(string filePath)
|
||||
{
|
||||
this.filePath = filePath;
|
||||
this.originalPath = filePath;
|
||||
}
|
||||
|
||||
public required string filePath
|
||||
public string filePath
|
||||
{
|
||||
get => _filePath;
|
||||
set
|
||||
{
|
||||
originalPath ??= value;
|
||||
|
||||
_filePath = value;
|
||||
_isHashExpired = true;
|
||||
}
|
||||
}
|
||||
public required string originalPath { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public FileInfo FileInfo => new(filePath);
|
||||
public string originalPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For blobs, the id is the same as the file hash. Please note, when deserialising, the id will be set from the original hash generated on sending.
|
||||
@@ -46,9 +45,9 @@ public sealed class Blob : Base
|
||||
|
||||
public string? GetFileHash()
|
||||
{
|
||||
if ((_isHashExpired || _hash == null))
|
||||
if ((_isHashExpired || _hash == null) && filePath != null)
|
||||
{
|
||||
_hash = HashUtility.CalculateBlobHash(filePath);
|
||||
_hash = HashUtility.HashFile(filePath);
|
||||
}
|
||||
|
||||
return _hash;
|
||||
|
||||
@@ -1,39 +1,26 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Speckle.Sdk.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for calculating hash based Ids for Speckle core concepts
|
||||
/// </summary>
|
||||
public static class HashUtility
|
||||
{
|
||||
public const int HASH_LENGTH_CHARS = 32;
|
||||
|
||||
[Pure]
|
||||
public static Id ComputeObjectId(Json serialized)
|
||||
public enum HashingFunctions
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
Span<char> hash = stackalloc char[HASH_LENGTH_CHARS];
|
||||
Sha256.Hash(serialized.Value.AsSpan(), false, hash);
|
||||
return new Id(new string(hash));
|
||||
#else
|
||||
string hash = Sha256.Hash(serialized.Value, outputLengthChars: HashUtility.HASH_LENGTH_CHARS);
|
||||
return new Id(hash);
|
||||
#endif
|
||||
SHA256,
|
||||
MD5,
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string CalculateBlobHash(string filePath)
|
||||
public const int HASH_LENGTH = 32;
|
||||
|
||||
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
|
||||
public static string HashFile(string filePath, HashingFunctions func = HashingFunctions.SHA256)
|
||||
{
|
||||
using HashAlgorithm hashAlgorithm = func == HashingFunctions.MD5 ? MD5.Create() : SHA256.Create();
|
||||
|
||||
using var stream = File.OpenRead(filePath);
|
||||
#if NET6_0_OR_GREATER
|
||||
Span<char> hash = stackalloc char[HASH_LENGTH_CHARS];
|
||||
Sha256.Hash(stream, false, hash);
|
||||
return new(hash);
|
||||
#else
|
||||
return Sha256.Hash(stream, "x2", HASH_LENGTH_CHARS);
|
||||
#endif
|
||||
|
||||
var hash = hashAlgorithm.ComputeHash(stream);
|
||||
return BitConverter.ToString(hash, 0, HASH_LENGTH).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation;
|
||||
|
||||
public static class IdGenerator
|
||||
{
|
||||
[Pure]
|
||||
public static Id ComputeId(Json serialized)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
string hash = Sha256.GetString(serialized.Value.AsSpan(), length: HashUtility.HASH_LENGTH);
|
||||
#else
|
||||
string hash = Sha256.GetString(serialized.Value, length: HashUtility.HASH_LENGTH);
|
||||
#endif
|
||||
return new Id(hash);
|
||||
}
|
||||
}
|
||||
@@ -358,7 +358,7 @@ public class SpeckleObjectSerializer
|
||||
if (writer is SerializerIdWriter serializerIdWriter)
|
||||
{
|
||||
(var json, writer) = serializerIdWriter.FinishIdWriter();
|
||||
id = HashUtility.ComputeObjectId(json);
|
||||
id = IdGenerator.ComputeId(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Text;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation.V2.Send;
|
||||
|
||||
public record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>? Closures) : IHasByteSize
|
||||
public sealed record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>? Closures) : IHasByteSize
|
||||
{
|
||||
public virtual int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value);
|
||||
public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value);
|
||||
|
||||
public virtual bool Equals(BaseItem? other)
|
||||
public bool Equals(BaseItem? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
@@ -18,10 +17,3 @@ public record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>?
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
}
|
||||
|
||||
public sealed record BlobItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>? Closures, Blob Blob)
|
||||
: BaseItem(Id, Json, NeedsStorage, Closures)
|
||||
{
|
||||
public Blob Blob { get; } = Blob;
|
||||
public override int ByteSize { get; } = (int)Blob.FileInfo.Length;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Dependencies;
|
||||
using Speckle.Sdk.Dependencies.Serialization;
|
||||
using Speckle.Sdk.SQLite;
|
||||
@@ -10,13 +9,7 @@ namespace Speckle.Sdk.Serialisation.V2.Send;
|
||||
public interface IObjectSaver : IDisposable
|
||||
{
|
||||
Exception? Exception { get; set; }
|
||||
Task Start(
|
||||
int? maxParallelism,
|
||||
int? httpBatchSize,
|
||||
int? blobBatchSize,
|
||||
int? cacheBatchSize,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
Task Start(int? maxParallelism, int? httpBatchSize, int? cacheBatchSize, CancellationToken cancellationToken);
|
||||
void DoneTraversing();
|
||||
Task DoneSaving();
|
||||
Task SaveAsync(BaseItem item);
|
||||
@@ -26,11 +19,14 @@ public sealed class ObjectSaver(
|
||||
IProgress<ProgressArgs>? progress,
|
||||
ISqLiteJsonCacheManager sqLiteJsonCacheManager,
|
||||
IServerObjectManager serverObjectManager,
|
||||
IServerBlobManager? serverBlobManager,
|
||||
ILogger<ObjectSaver> logger,
|
||||
SerializeProcessOptions options,
|
||||
CancellationToken cancellationToken
|
||||
) : ChannelSaver<BaseItem, BlobItem>, IObjectSaver
|
||||
#pragma warning disable CS9107
|
||||
#pragma warning disable CA2254
|
||||
) : ChannelSaver<BaseItem>, IObjectSaver
|
||||
#pragma warning restore CA2254
|
||||
#pragma warning restore CS9107
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken
|
||||
@@ -44,24 +40,6 @@ public sealed class ObjectSaver(
|
||||
private long _objectsSerialized;
|
||||
private bool _disposed;
|
||||
|
||||
protected override async Task SendBlobToServerInternal(Batch<BlobItem> batch)
|
||||
{
|
||||
// Callers should either setup a blob manager, or not try and send blobs
|
||||
serverBlobManager.NotNull("No blob manager was setup to handle sending blobs");
|
||||
|
||||
var objectBatch = batch.Items.Distinct().Select(x => (x.Blob.id.NotNull(), x.Blob.filePath)).ToList();
|
||||
// var hasObjects = await serverBlobManager
|
||||
// .HasObjects(objectBatch.Select(x => x.Id.Value).Freeze(), _cancellationTokenSource.Token)
|
||||
// .ConfigureAwait(false);
|
||||
// objectBatch = batch.Items.Where(x => !hasObjects[x.Id.Value]).ToList();
|
||||
if (objectBatch.Count != 0)
|
||||
{
|
||||
// Interlocked.Add(ref _uploading, batch.Items.Count);
|
||||
// progress?.Report(new(ProgressEvent.UploadingObjects, _uploading, null));
|
||||
await serverBlobManager.UploadBlobs(objectBatch, progress, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SendToServerInternal(Batch<BaseItem> batch)
|
||||
{
|
||||
if (IsCancelled())
|
||||
|
||||
@@ -343,7 +343,7 @@ public sealed class ObjectSerializer : IObjectSerializer
|
||||
if (writer is SerializerIdWriter serializerIdWriter)
|
||||
{
|
||||
(var json, writer) = serializerIdWriter.FinishIdWriter();
|
||||
id = HashUtility.ComputeObjectId(json);
|
||||
id = IdGenerator.ComputeId(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -17,7 +17,6 @@ public record SerializeProcessOptions(
|
||||
{
|
||||
public int? MaxHttpSendBatchSize { get; set; }
|
||||
public int? MaxCacheBatchSize { get; set; }
|
||||
public int? MaxBlobBatchSize { get; set; }
|
||||
public int? MaxParallelism { get; set; }
|
||||
}
|
||||
|
||||
@@ -110,7 +109,6 @@ public sealed class SerializeProcess(
|
||||
var channelTask = objectSaver.Start(
|
||||
options.MaxParallelism,
|
||||
options.MaxHttpSendBatchSize,
|
||||
options.MaxBlobBatchSize,
|
||||
options.MaxCacheBatchSize,
|
||||
_processSource.Token
|
||||
);
|
||||
|
||||
@@ -13,36 +13,26 @@ public class SerializeProcessFactory(
|
||||
IObjectSerializerFactory objectSerializerFactory,
|
||||
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory,
|
||||
IServerObjectManagerFactory serverObjectManagerFactory,
|
||||
IServerBlobManagerFactory serverBlobManagerFactory,
|
||||
ILoggerFactory loggerFactory
|
||||
) : ISerializeProcessFactory
|
||||
{
|
||||
public ISerializeProcess CreateSerializeProcess(
|
||||
Uri url,
|
||||
string projectId,
|
||||
string streamId,
|
||||
string? authorizationToken,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken,
|
||||
SerializeProcessOptions? options = null
|
||||
)
|
||||
{
|
||||
var sqLiteJsonCacheManager = sqLiteJsonCacheManagerFactory.CreateFromStream(projectId);
|
||||
var serverObjectManager = serverObjectManagerFactory.Create(url, projectId, authorizationToken);
|
||||
var serverBlobManager = serverBlobManagerFactory.Create(url, projectId, authorizationToken);
|
||||
return CreateSerializeProcess(
|
||||
sqLiteJsonCacheManager,
|
||||
serverObjectManager,
|
||||
serverBlobManager,
|
||||
progress,
|
||||
cancellationToken,
|
||||
options
|
||||
);
|
||||
var sqLiteJsonCacheManager = sqLiteJsonCacheManagerFactory.CreateFromStream(streamId);
|
||||
var serverObjectManager = serverObjectManagerFactory.Create(url, streamId, authorizationToken);
|
||||
return CreateSerializeProcess(sqLiteJsonCacheManager, serverObjectManager, progress, cancellationToken, options);
|
||||
}
|
||||
|
||||
public ISerializeProcess CreateSerializeProcess(
|
||||
ISqLiteJsonCacheManager sqLiteJsonCacheManager,
|
||||
IServerObjectManager serverObjectManager,
|
||||
IServerBlobManager? serverBlobManager,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken,
|
||||
SerializeProcessOptions? options = null
|
||||
@@ -53,7 +43,6 @@ public class SerializeProcessFactory(
|
||||
progress,
|
||||
sqLiteJsonCacheManager,
|
||||
serverObjectManager,
|
||||
serverBlobManager,
|
||||
loggerFactory.CreateLogger<ObjectSaver>(),
|
||||
options ?? new SerializeProcessOptions(),
|
||||
cancellationToken
|
||||
@@ -79,7 +68,6 @@ public class SerializeProcessFactory(
|
||||
return CreateSerializeProcess(
|
||||
memoryJsonCacheManager,
|
||||
new MemoryServerObjectManager(objects),
|
||||
null!, //this would need a better solution
|
||||
progress,
|
||||
cancellationToken,
|
||||
options
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Helpers;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation.V2;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class ServerBlobManagerFactory(ISpeckleHttp speckleHttp) : IServerBlobManagerFactory
|
||||
{
|
||||
public IServerBlobManager Create(
|
||||
Uri serverUrl,
|
||||
string projectId,
|
||||
string? authorizationToken,
|
||||
TimeSpan? timeout = null
|
||||
)
|
||||
{
|
||||
var client = speckleHttp.CreateHttpClient(authorizationToken: authorizationToken);
|
||||
client.BaseAddress = serverUrl;
|
||||
return new ServerBlobManager(client, projectId);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Transports;
|
||||
using Speckle.Sdk.Transports.ServerUtils;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation.V2;
|
||||
|
||||
[GenerateAutoInterface(VisibilityModifier = "public")]
|
||||
internal sealed class ServerBlobManager(HttpClient authorizedClient, string projectId) : IServerBlobManager
|
||||
{
|
||||
public async Task UploadBlobs(
|
||||
IReadOnlyCollection<(string blobId, string filePath)> objects,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (objects.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var multipartFormDataContent = new MultipartFormDataContent();
|
||||
foreach (var (id, filePath) in objects)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var stream = File.OpenRead(filePath);
|
||||
StreamContent fsc = new(stream);
|
||||
|
||||
multipartFormDataContent.Add(fsc, $"hash:{id}", fileName);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
using var message = new HttpRequestMessage();
|
||||
message.RequestUri = new Uri($"/api/stream/{projectId}/blob", UriKind.Relative);
|
||||
message.Method = HttpMethod.Post;
|
||||
message.Content = new ProgressContent(multipartFormDataContent, progress);
|
||||
|
||||
using var response = await authorizedClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ public class CancellationTests
|
||||
new DummySqLiteSendManager(),
|
||||
new CancellationServerObjectManager(cancellationSource),
|
||||
null,
|
||||
null,
|
||||
cancellationSource.Token,
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
);
|
||||
@@ -80,7 +79,6 @@ public class CancellationTests
|
||||
new DummySqLiteSendManager(),
|
||||
new CancellationServerObjectManager(cancellationSource),
|
||||
null,
|
||||
null,
|
||||
cancellationSource.Token,
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
);
|
||||
|
||||
@@ -40,7 +40,6 @@ public class DataObjectTests
|
||||
new MemoryJsonCacheManager(json),
|
||||
new DummyServerObjectManager(),
|
||||
null,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true)
|
||||
);
|
||||
|
||||
@@ -37,7 +37,6 @@ public class ExceptionTests
|
||||
new MemoryJsonCacheManager(objects),
|
||||
new ExceptionServerObjectManager(),
|
||||
null,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, false, true)
|
||||
);
|
||||
@@ -56,7 +55,6 @@ public class ExceptionTests
|
||||
new ExceptionSendCacheManager(),
|
||||
new MemoryServerObjectManager(new()),
|
||||
null,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, false, true)
|
||||
);
|
||||
@@ -94,7 +92,6 @@ public class ExceptionTests
|
||||
new ExceptionSendCacheManager(exceptionsAfter: 10),
|
||||
new MemoryServerObjectManager(new()),
|
||||
null,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, false, true)
|
||||
{
|
||||
|
||||
@@ -146,7 +146,7 @@ public class SerializationTests
|
||||
jObject.Remove("id");
|
||||
jObject.Remove("__closure");
|
||||
var jsonWithoutId = jObject.ToString(Formatting.None);
|
||||
var newId = HashUtility.ComputeObjectId(new Json(jsonWithoutId));
|
||||
var newId = IdGenerator.ComputeId(new Json(jsonWithoutId));
|
||||
id.Should().Be(newId.Value);
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ public class SerializationTests
|
||||
idToBase.Count.Should().Be(count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Theory(Skip = "Takes too long")]
|
||||
[InlineData(1)]
|
||||
[InlineData(4)]
|
||||
public async Task Roundtrip_Test_New(int concurrency)
|
||||
@@ -227,7 +227,6 @@ public class SerializationTests
|
||||
SqLiteJsonCacheManager.FromMemory(1),
|
||||
new MemoryServerObjectManager(newIdToJson),
|
||||
null,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, false, true) { MaxCacheBatchSize = 1, MaxParallelism = concurrency }
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ public class BlobApiExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
await writer.WriteLineAsync(PAYLOAD);
|
||||
}
|
||||
string id = HashUtility.CalculateBlobHash(filePath);
|
||||
string id = HashUtility.HashFile(filePath);
|
||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await _sut.UploadBlobs("non-existent-project", [(id, filePath)], null, CancellationToken.None)
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ public class BlobApiTests : IAsyncLifetime
|
||||
{
|
||||
await writer.WriteLineAsync(PAYLOAD);
|
||||
}
|
||||
string id = HashUtility.CalculateBlobHash(filePath);
|
||||
string id = HashUtility.HashFile(filePath);
|
||||
|
||||
//act
|
||||
var preDiff = await _blobApi.HasBlobs(_project.id, [id], CancellationToken.None);
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
|
||||
[Trait("Server", "Internal")]
|
||||
public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
|
||||
{
|
||||
private IClient _testUser;
|
||||
private ModelIngestionResource Sut => _testUser.Ingestion;
|
||||
private Project _project;
|
||||
private Model _model;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateIngestionNonExistentProject()
|
||||
{
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
"Doesn't exist...",
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
{
|
||||
_ = await Sut.Create(createInput);
|
||||
});
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLStreamNotFoundException>(item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateNonExistentNonExistent()
|
||||
{
|
||||
var updateInput = new ModelIngestionUpdateInput("Doesn't exist", _project.id, "Can't be", 0.5);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
{
|
||||
_ = await Sut.UpdateProgress(updateInput);
|
||||
});
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelNonExistentIngestion()
|
||||
{
|
||||
var input = new ModelIngestionCancelledInput(
|
||||
"Non-existent-ingestion",
|
||||
_project.id,
|
||||
cancellationMessage: "This was cancelled for testing purposes"
|
||||
);
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
{
|
||||
_ = await Sut.FailWithCancel(input);
|
||||
});
|
||||
Assert.Single(ex.InnerExceptions);
|
||||
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLException>(item));
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Transports;
|
||||
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
|
||||
[Trait("Server", "Internal")]
|
||||
public sealed class ModelIngestionResourceTests : IAsyncLifetime
|
||||
{
|
||||
private IClient _testUser;
|
||||
private ModelIngestionResource Sut => _testUser.Ingestion;
|
||||
private Project _project;
|
||||
private Model _model;
|
||||
private IOperations _operations;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndError()
|
||||
{
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
ModelIngestion ingest = await Sut.Create(createInput);
|
||||
|
||||
var errorInput = new ModelIngestionFailedInput(ingest.id, _project.id, "A bad thing happened", "Over hear!");
|
||||
var res = await Sut.FailWithError(errorInput);
|
||||
Assert.Equal(ingest.id, res.id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndUpdate()
|
||||
{
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
ModelIngestion ingest = await Sut.Create(createInput);
|
||||
|
||||
await Update(null, "None");
|
||||
await Update(0.1, "0.1");
|
||||
await Update(0.5, "Whoa-oh! We're half way there!");
|
||||
await Update(1, "Finished");
|
||||
await Update(0.2, "Back to processing again");
|
||||
|
||||
async Task Update(double? progress, string message)
|
||||
{
|
||||
var updateInput = new ModelIngestionUpdateInput(ingest.id, _project.id, message, progress);
|
||||
var res = await Sut.UpdateProgress(updateInput);
|
||||
|
||||
Assert.Equal(message, res.statusData.progressMessage);
|
||||
Assert.False(res.cancellationRequested);
|
||||
Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndCancel()
|
||||
{
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
ModelIngestion ingest = await Sut.Create(createInput);
|
||||
|
||||
var input = new ModelIngestionCancelledInput(
|
||||
ingest.id,
|
||||
_project.id,
|
||||
cancellationMessage: "This was cancelled for testing purposes"
|
||||
);
|
||||
var res = await Sut.FailWithCancel(input);
|
||||
Assert.Equal(ingest.id, res.id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndComplete()
|
||||
{
|
||||
ModelIngestionCreateInput createInput = new(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
ModelIngestion ingest = await Sut.Create(createInput);
|
||||
|
||||
Base myObject = Fixtures.GenerateNestedObject();
|
||||
var sendResult = await _operations.Send2(
|
||||
_testUser.ServerUrl,
|
||||
_project.id,
|
||||
_testUser.Account.token,
|
||||
myObject,
|
||||
new Progress<ProgressArgs>(x =>
|
||||
{
|
||||
var updateInput = new ModelIngestionUpdateInput(
|
||||
ingest.id,
|
||||
_project.id,
|
||||
$"{x.Count} / {x.Total}",
|
||||
x.Total == null ? null : x.Count / x.Total
|
||||
);
|
||||
_ = Sut.UpdateProgress(updateInput).Result;
|
||||
}),
|
||||
CancellationToken.None,
|
||||
new(true, true)
|
||||
);
|
||||
|
||||
ModelIngestionSuccessInput finish = new(ingest.id, _project.id, sendResult.RootId, "yay!");
|
||||
string versionId = await Sut.Complete(finish);
|
||||
Version version = await _testUser.Version.Get(versionId, _project.id);
|
||||
Assert.Equal(version.id, versionId);
|
||||
Assert.Equal(sendResult.RootId, version.referencedObject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGet()
|
||||
{
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
ModelIngestion ingest = await Sut.Create(createInput);
|
||||
|
||||
ModelIngestion res = await Sut.Get(ingest.id, _project.id);
|
||||
Assert.Equal(ingest.id, res.id);
|
||||
Assert.Equal(ingest.statusData.status, res.statusData.status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestRequeue()
|
||||
{
|
||||
//Not sure if is desirable that ingestions created by the modelIngestionMutations.create mutation can be re-queued
|
||||
//But the server allows it, so we test it
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
var ingestion = await Sut.Create(createInput);
|
||||
var res = await Sut.Requeue(new(ingestion.id, _project.id, "we'll try and requeue this ingestion"));
|
||||
|
||||
Assert.Equal(ingestion.id, res.id);
|
||||
Assert.Equal(ModelIngestionStatus.queued, res.statusData.status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestStartProcessing()
|
||||
{
|
||||
//Not sure if is desirable that StartProcessing can be used by ingestions created by the modelIngestionMutations.create mutation
|
||||
//But the server allows it, so we test it
|
||||
var createInput = new ModelIngestionCreateInput(
|
||||
_model.id,
|
||||
_project.id,
|
||||
"Starting processing",
|
||||
new(".NET test runner", "0.0.0", null, null)
|
||||
);
|
||||
var ingestion = await Sut.Create(createInput);
|
||||
var res = await Sut.StartProcessing(
|
||||
new(ingestion.id, _project.id, "", new SourceDataInput("what", "happens", "now", 0))
|
||||
);
|
||||
|
||||
Assert.Equal(ingestion.id, res.id);
|
||||
Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentAssertions;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
@@ -17,7 +18,7 @@ public class ModelResourceTests : IAsyncLifetime
|
||||
{
|
||||
// Runs instead of [SetUp] in NUnit
|
||||
_testUser = await Fixtures.SeedUserWithClient();
|
||||
_project = await _testUser.Project.Create(new("Test project", "", null));
|
||||
_project = await _testUser.Project.Create(new("Test project", "", ProjectVisibility.Public));
|
||||
_model = await _testUser.Model.Create(new("Test Model", "", _project.id));
|
||||
}
|
||||
|
||||
@@ -123,4 +124,40 @@ public class ModelResourceTests : IAsyncLifetime
|
||||
var delEx = await FluentActions.Invoking(() => Sut.Delete(input)).Should().ThrowAsync<AggregateException>();
|
||||
getEx.WithInnerExceptionExactly<SpeckleGraphQLException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestUserHasModelPermissions()
|
||||
{
|
||||
var ownerResult = await Sut.GetPermissions(_project.id, _model.id);
|
||||
ownerResult.canUpdate.authorized.Should().Be(true);
|
||||
ownerResult.canCreateVersion.authorized.Should().Be(true);
|
||||
ownerResult.canDelete.authorized.Should().Be(true);
|
||||
|
||||
// Test with another user
|
||||
var guest = await Fixtures.SeedUserWithClient();
|
||||
|
||||
var guestResult = await guest.Model.GetPermissions(_project.id, _model.id);
|
||||
guestResult.canUpdate.authorized.Should().Be(false);
|
||||
guestResult.canCreateVersion.authorized.Should().Be(false);
|
||||
guestResult.canDelete.authorized.Should().Be(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Server", "Internal")]
|
||||
public async Task TestCanCreateModelIngestion_InternalServer()
|
||||
{
|
||||
var ownerResult = await Sut.CanCreateModelIngestion(_project.id, _model.id);
|
||||
ownerResult.authorized.Should().Be(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Server", "Public")]
|
||||
public async Task TestCanCreateModelIngestion_PublicServer_Throws()
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
await Sut.CanCreateModelIngestion(_project.id, _model.id)
|
||||
);
|
||||
ex.InnerExceptions.Should().HaveCount(1);
|
||||
ex.InnerExceptions.Should().AllBeOfType<SpeckleGraphQLInvalidQueryException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +103,30 @@ public class ProjectResourceTests
|
||||
[Fact]
|
||||
public async Task TestUserHasProjectPermissions()
|
||||
{
|
||||
var res = await Sut.GetPermissions(_testProject.id);
|
||||
var privateProject = await _testUser.Project.Create(
|
||||
new ProjectCreateInput("asdfasdf", "desc", ProjectVisibility.Private)
|
||||
);
|
||||
|
||||
var resp = await Sut.GetPermissions(privateProject.id);
|
||||
resp.canCreateModel.authorized.Should().Be(true);
|
||||
resp.canDelete.authorized.Should().Be(true);
|
||||
resp.canLoad.authorized.Should().Be(true);
|
||||
|
||||
var publicProject = await _testUser.Project.Create(
|
||||
new ProjectCreateInput("asdfasdf", "desc", ProjectVisibility.Public)
|
||||
);
|
||||
|
||||
var res = await Sut.GetPermissions(publicProject.id);
|
||||
res.canCreateModel.authorized.Should().Be(true);
|
||||
res.canDelete.authorized.Should().Be(true);
|
||||
res.canLoad.authorized.Should().Be(true);
|
||||
|
||||
// Test with another user
|
||||
var guest = await Fixtures.SeedUserWithClient();
|
||||
|
||||
var guestResult = await guest.Project.GetPermissions(publicProject.id);
|
||||
guestResult.canCreateModel.authorized.Should().Be(false);
|
||||
guestResult.canDelete.authorized.Should().Be(false);
|
||||
guestResult.canLoad.authorized.Should().Be(false);
|
||||
}
|
||||
}
|
||||
|
||||
+78
-8
@@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
@@ -11,11 +11,11 @@ namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
|
||||
public class SubscriptionResourceTests : IAsyncLifetime
|
||||
{
|
||||
#if DEBUG
|
||||
private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
|
||||
private const int WAIT_PERIOD = 4000; // WSL is slow AF, so for local runs, we're being extra generous
|
||||
#else
|
||||
private const int WAIT_PERIOD = 400; // For CI runs, a much smaller wait time is acceptable
|
||||
private const int WAIT_PERIOD = 500; // For CI runs, a much smaller wait time is acceptable
|
||||
#endif
|
||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 400;
|
||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 500;
|
||||
private IClient _testUser;
|
||||
private Project _testProject;
|
||||
private Model _testModel;
|
||||
@@ -80,15 +80,15 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
||||
public async Task ProjectUpdated_SubscriptionIsCalled()
|
||||
{
|
||||
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
|
||||
using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
|
||||
using Subscription<ProjectUpdatedMessage> sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
|
||||
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||
|
||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||
|
||||
var input = new ProjectUpdateInput(_testProject.id, "This is my new name");
|
||||
var created = await _testUser.Project.Update(input);
|
||||
ProjectUpdateInput input = new(_testProject.id, "This is my new name");
|
||||
Project created = await _testUser.Project.Update(input);
|
||||
|
||||
var subscriptionMessage = await tcs.Task;
|
||||
ProjectUpdatedMessage subscriptionMessage = await tcs.Task;
|
||||
|
||||
subscriptionMessage.Should().NotBeNull();
|
||||
subscriptionMessage.id.Should().Be(created.id);
|
||||
@@ -135,4 +135,74 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
||||
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
|
||||
subscriptionMessage.comment.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||
public async Task ProjectModelIngestionCancellationRequested_SubscriptionIsCalled()
|
||||
{
|
||||
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||
);
|
||||
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||
|
||||
using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.id);
|
||||
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||
|
||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||
|
||||
await _testUser.Ingestion.RequestCancellation(new(ingestion.id, _testProject.id, "please cancel"));
|
||||
|
||||
var subscriptionMessage = await tcs.Task;
|
||||
|
||||
subscriptionMessage.Should().NotBeNull();
|
||||
subscriptionMessage.type.Should().Be(ProjectModelIngestionUpdatedMessageType.cancellationRequested);
|
||||
subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||
public async Task ProjectModelIngestionUpdate_UpdateSubscriptionIs()
|
||||
{
|
||||
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||
);
|
||||
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||
|
||||
using var sub = Sut.CreateProjectModelIngestionUpdatedSubscription(
|
||||
new(
|
||||
_testProject.id,
|
||||
new ModelIngestionReference(ingestion.id, null),
|
||||
ProjectModelIngestionUpdatedMessageType.updated
|
||||
)
|
||||
);
|
||||
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||
|
||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||
|
||||
await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "Here's an update", 0.314));
|
||||
|
||||
var subscriptionMessage = await tcs.Task;
|
||||
|
||||
subscriptionMessage.Should().NotBeNull();
|
||||
subscriptionMessage.type.Should().Be(ProjectModelIngestionUpdatedMessageType.updated);
|
||||
subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
|
||||
public async Task ProjectModelIngestionUpdate_CancelSubscriptionIsNotCalled()
|
||||
{
|
||||
ModelIngestion ingestion = await _testUser.Ingestion.Create(
|
||||
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
|
||||
);
|
||||
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
|
||||
|
||||
using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.id);
|
||||
sub.Listeners += (_, message) => tcs.SetResult(message);
|
||||
|
||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
|
||||
|
||||
await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "this shouldn't cancel", null));
|
||||
|
||||
await Task.Delay(WAIT_PERIOD); // Give time to subscription to maybe fire
|
||||
|
||||
tcs.Task.IsCompleted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
[assembly: AssemblyTrait("Category", "Integration")]
|
||||
|
||||
#if DEBUG
|
||||
[assembly: CollectionBehavior(MaxParallelThreads = 8)]
|
||||
|
||||
#endif
|
||||
|
||||
namespace Speckle.Sdk.Tests.Integration;
|
||||
|
||||
public static class Fixtures
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
@@ -16,8 +14,7 @@ public class MemoryTransportTests : IDisposable
|
||||
public MemoryTransportTests()
|
||||
{
|
||||
CleanData();
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
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;
|
||||
|
||||
@@ -19,8 +16,6 @@ public sealed class SendReceiveTests : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
ClearCache();
|
||||
|
||||
@@ -19,14 +19,12 @@ public class CryptSha256Hash
|
||||
[Benchmark]
|
||||
public string Sha256()
|
||||
{
|
||||
return Speckle.Sdk.Common.Sha256.Hash(testData);
|
||||
return Speckle.Sdk.Common.Sha256.GetString(testData);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public string Sha256_Span()
|
||||
{
|
||||
Span<char> resultLowerSpan = stackalloc char[Speckle.Sdk.Common.Sha256.HASH_SIZE_CHARS];
|
||||
Speckle.Sdk.Common.Sha256.Hash(testData.AsSpan(), false, resultLowerSpan);
|
||||
return new string(resultLowerSpan);
|
||||
return Speckle.Sdk.Common.Sha256.GetString(testData.AsSpan());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class Closures
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public Closures()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(TableLegFixture).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public sealed partial class OperationsReceiveTests : IDisposable
|
||||
{
|
||||
private static readonly Base[] s_testObjects;
|
||||
@@ -44,8 +45,7 @@ public sealed partial class OperationsReceiveTests : IDisposable
|
||||
|
||||
private static void Reset()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> TestCases()
|
||||
|
||||
@@ -7,14 +7,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SendObjectReferences
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SendObjectReferences()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public sealed class SendReceiveLocal : IDisposable
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SendReceiveLocal()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ using Point = Speckle.Sdk.Tests.Unit.Host.Point;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Api.Operations;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class ObjectSerialization
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public ObjectSerialization()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
[assembly: CollectionBehavior()]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Sdk.Tests.Unit;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresTypeLoaderCollection), DisableParallelization = true)]
|
||||
public class RequiresTypeLoaderCollection;
|
||||
|
||||
[CollectionDefinition(nameof(RequiresSqLiteAccountDb), DisableParallelization = true)]
|
||||
public class RequiresSqLiteAccountDb;
|
||||
@@ -9,6 +9,7 @@ using Speckle.Sdk.Testing;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public sealed class AccountManagerTests : MoqTest
|
||||
{
|
||||
private class TestAccountFactory : IAccountFactory
|
||||
|
||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public class AccountServerMigrationTests : IDisposable
|
||||
{
|
||||
private readonly List<Account> _accountsToCleanUp = [];
|
||||
|
||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Credentials;
|
||||
|
||||
[Collection(nameof(RequiresSqLiteAccountDb))]
|
||||
public class CredentialInfrastructure : IDisposable
|
||||
{
|
||||
private readonly IAccountManager _accountManager;
|
||||
|
||||
@@ -5,12 +5,12 @@ using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class BaseTests
|
||||
{
|
||||
public BaseTests()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -5,12 +5,12 @@ using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class DynamicBaseTests
|
||||
{
|
||||
public DynamicBaseTests()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -6,12 +6,12 @@ using Speckle.Sdk.Models.Extensions;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models.Extensions;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class BaseExtensionsTests
|
||||
{
|
||||
public BaseExtensionsTests()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(TestBase).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(TestBase).Assembly);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -5,6 +5,7 @@ using Speckle.Sdk.Models.Extensions;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models.Extensions;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class DisplayValueTests
|
||||
{
|
||||
private const string PAYLOAD = "This is my payload";
|
||||
@@ -17,8 +18,7 @@ public class DisplayValueTests
|
||||
|
||||
private static void Reset()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -6,12 +6,12 @@ using Speckle.Sdk.Models.GraphTraversal;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models.GraphTraversal;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class GraphTraversalTests
|
||||
{
|
||||
public GraphTraversalTests()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(TraversalMock).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(TraversalMock).Assembly);
|
||||
}
|
||||
|
||||
private static IEnumerable<TraversalContext> Traverse(Base testCase, params ITraversalRule[] rules)
|
||||
|
||||
@@ -6,13 +6,12 @@ using Speckle.Sdk.Tests.Unit.Host;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models;
|
||||
|
||||
// Removed [TestFixture] and [TestOf] annotations as they are NUnit specific
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class Hashing
|
||||
{
|
||||
public Hashing()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(DiningTable).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(DiningTable).Assembly);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Checks that hashing (as represented by object IDs) actually works.")]
|
||||
|
||||
@@ -5,13 +5,13 @@ using Speckle.Sdk.Tests.Unit.Models.TestModels;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Models
|
||||
{
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SpeckleTypeTests
|
||||
{
|
||||
public SpeckleTypeTests()
|
||||
{
|
||||
// Setup logic during test class initialization
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(Foo).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(Foo).Assembly);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -69,8 +69,8 @@ public sealed class HashUtilityTests
|
||||
[MemberData(nameof(SmallTestCasesSha256))]
|
||||
public void Sha256(string input, string expected, string _, int length)
|
||||
{
|
||||
var resultLower = Speckle.Sdk.Common.Sha256.Hash(input, "x2", length);
|
||||
var resultUpper = Speckle.Sdk.Common.Sha256.Hash(input, "X2", length);
|
||||
var resultLower = Speckle.Sdk.Common.Sha256.GetString(input, "x2", length);
|
||||
var resultUpper = Speckle.Sdk.Common.Sha256.GetString(input, "X2", length);
|
||||
|
||||
resultLower.Should().Be(new string(expected.ToLower()[..length]));
|
||||
|
||||
@@ -86,22 +86,19 @@ public sealed class HashUtilityTests
|
||||
int length //Span version of the function must have multiple of 2
|
||||
)
|
||||
{
|
||||
Span<char> resultLowerSpan = stackalloc char[length];
|
||||
Speckle.Sdk.Common.Sha256.Hash(input.AsSpan(), false, resultLowerSpan);
|
||||
Span<char> resultUpperSpan = stackalloc char[length];
|
||||
Speckle.Sdk.Common.Sha256.Hash(input.AsSpan(), true, resultUpperSpan);
|
||||
var resultLowerSpan = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan(), "x2", length);
|
||||
var resultUpperSpan = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan(), "X2", length);
|
||||
|
||||
new string(resultLowerSpan).Should().Be(new string(expected.ToLower()[..length]));
|
||||
resultLowerSpan.Should().Be(new string(expected.ToLower()[..length]));
|
||||
|
||||
new string(resultUpperSpan).Should().Be(new string(expected.ToUpper()[..length]));
|
||||
resultUpperSpan.Should().Be(new string(expected.ToUpper()[..length]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(LargeTestCases))]
|
||||
public void Sha256_Span_LargeDataTests(string input, string expected)
|
||||
public void Sha256_LargeDataTests(string input, string expected)
|
||||
{
|
||||
Span<char> output = stackalloc char[Speckle.Sdk.Common.Sha256.HASH_SIZE_CHARS];
|
||||
Speckle.Sdk.Common.Sha256.Hash(input.AsSpan(), false, output);
|
||||
new string(output).Should().Be(expected);
|
||||
var computedHash = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan());
|
||||
computedHash.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Serialisation;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class ChunkingTests
|
||||
{
|
||||
public static IEnumerable<object[]> TestCases()
|
||||
{
|
||||
// Initialize type loader
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(IgnoreTest).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(IgnoreTest).Assembly);
|
||||
|
||||
// Return test data as a collection of objects for xUnit
|
||||
yield return [CreateDynamicTestCase(10, 100), 10];
|
||||
|
||||
@@ -12,12 +12,12 @@ namespace Speckle.Sdk.Tests.Unit.Serialisation;
|
||||
/// Tests that the <see cref="JsonIgnoreAttribute"/> leads to properties being ignored both from the final JSON output,
|
||||
/// But also from the id calculation
|
||||
/// </summary>
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public sealed class JsonIgnoreRespected
|
||||
{
|
||||
public JsonIgnoreRespected()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(IgnoreTest).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(IgnoreTest).Assembly);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> IgnoredTestCases()
|
||||
|
||||
@@ -5,13 +5,13 @@ using Speckle.Sdk.Serialisation.Deprecated;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Serialisation
|
||||
{
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class TypeLoaderTests
|
||||
{
|
||||
// Constructor replaces the [SetUp] functionality in NUnit
|
||||
public TypeLoaderTests()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(MySpeckleBase).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(MySpeckleBase).Assembly);
|
||||
}
|
||||
|
||||
[Fact] // Replaces [Test]
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Speckle.Sdk.Tests.Unit.Serialisation;
|
||||
/// This doesn't guarantee things work this way for SpecklePy
|
||||
/// Nor does it encompass other tricks (like deserialize callback, or computed json ignored properties)
|
||||
/// </summary>
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SerializerBreakingChanges : PrimitiveTestFixture
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
@@ -21,8 +22,7 @@ public class SerializerBreakingChanges : PrimitiveTestFixture
|
||||
// xUnit does not support a Setup method; instead, you can use the constructor for initialization.
|
||||
public SerializerBreakingChanges()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(Base).Assembly, typeof(Point).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ using Matrix4x4 = Speckle.DoubleNumerics.Matrix4x4;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Serialisation;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SerializerNonBreakingChanges : PrimitiveTestFixture
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SerializerNonBreakingChanges()
|
||||
{
|
||||
TypeLoader.Reset();
|
||||
TypeLoader.Initialize(typeof(StringValueMock).Assembly);
|
||||
TypeLoader.ReInitialize(typeof(StringValueMock).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Tests.Unit.Host;
|
||||
|
||||
namespace Speckle.Sdk.Tests.Unit.Serialisation;
|
||||
|
||||
[Collection(nameof(RequiresTypeLoaderCollection))]
|
||||
public class SimpleRoundTripTests
|
||||
{
|
||||
private readonly IOperations _operations;
|
||||
|
||||
public SimpleRoundTripTests()
|
||||
{
|
||||
TypeLoader.ReInitialize(typeof(DiningTable).Assembly);
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
_operations = serviceProvider.GetRequiredService<IOperations>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user