Compare commits

..

2 Commits

Author SHA1 Message Date
Adam Hathcock 637997bd18 Report progress before saving SQLite
.NET Build and Publish / build (push) Has been cancelled
2025-08-26 10:31:43 +01:00
Adam Hathcock 6c89748fd0 Report increment rather than total position 2025-08-26 10:04:14 +01:00
78 changed files with 556 additions and 1201 deletions
-53
View File
@@ -1,53 +0,0 @@
name: Integration Test
on:
workflow_call:
inputs:
docker-compose-file:
required: true
type: string
use-github-container-registry:
default: false
type: boolean
jobs:
integration-test:
env:
Solution: "Speckle.Sdk.sln"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- name: 🔐 Login to Github Container Registry
if: ${{ inputs.use-github-container-registry }}
uses: docker/login-action@v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ github.token }}
- name: ⚙️ Spin up Server
run: docker compose -f ${{ inputs.docker-compose-file }} up --wait
- name: 📦 Restore
run: dotnet restore ${{ env.Solution }} --locked-mode
- 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: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
+37 -46
View File
@@ -1,55 +1,46 @@
name: PR Test
name: .NET CI Build
on:
pull_request:
jobs:
build:
env:
Solution: "Speckle.Sdk.sln"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Checkout
uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- name: 📦 Tool Restore
run: dotnet tool restore
- name: 📄 Format
run: dotnet csharpier check .
- name: 📦 Restore
run: dotnet restore ${{ env.Solution }} --locked-mode
- name: 🏗️ Build
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
- name: 🔨 Unit Tests
run: dotnet test ${{ env.Solution }} --configuration Release --filter "Category!=Integration" --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
- name: 🎁 Pack
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
integration-test-internal:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose-internal.yml"
use-github-container-registry: true
integration-test-public:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose.yml"
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x.x
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- id: set-version
name: Set version to output
run: |
SEMVER="3.0.99.${{ github.run_number }}"
FILE_VERSION=$(echo "$SEMVER" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
echo $SEMVER
echo $FILE_VERSION
- name: 🔫 Build All
run: ./build.sh
env:
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
+11 -19
View File
@@ -2,27 +2,25 @@ name: .NET Build and Publish
on:
push:
tags: ["3.*.*"]
tags: ["3.*"]
jobs:
build:
runs-on: ubuntu-latest
environment:
name: "nuget.org"
permissions:
id-token: write # enable GitHub OIDC token issuance for this job
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- id: set-version
name: Set version to output
run: |
@@ -39,24 +37,18 @@ jobs:
echo $SEMVER
echo $FILE_VERSION
- name: 🔫 Build and Pack
run: ./build.sh pack
env:
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Push to nuget.org
run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{steps.login.outputs.NUGET_API_KEY}}
run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{secrets.CONNECTORS_NUGET_TOKEN }} --skip-duplicate
+2 -2
View File
@@ -1,8 +1,8 @@
<Project>
<PropertyGroup Condition="'$(IsTestProject)' == 'true' or '$(TestProjectAnalyserRules)' == 'true' ">
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<NoWarn>
<!-- Things we need to test -->
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065;
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
IDE0044;IDE0130;CA1508;
<!-- Analysers that provide no tangeable value to a test project -->
CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831;
+2 -4
View File
@@ -65,12 +65,10 @@ Docs are a bit patchy [https://docs.speckle.systems/developers/looking-for-devel
### Tests
There are several test projects. It is a requirement that all tests pass for PRs to be merged.
The Integration test projects require a local server to be running.
You must have docker installed. Then you can run `docker compose up` from the root of the repo to start the required containers.
In CI, they will be run against both the public and private versions of the server.
It is important that we remain compatible with both server versions.
You must have docker installed. Then you can run `docker compose up --wait` from the root of the repo to start the required containers.
## Contributing
Before embarking on submitting a patch, please make sure you read:
-2
View File
@@ -27,7 +27,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{DA2AED
CodeMetricsConfig.txt = CodeMetricsConfig.txt
Directory.Build.Targets = Directory.Build.Targets
.config\dotnet-tools.json = .config\dotnet-tools.json
docker-compose-internal.yml = docker-compose-internal.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{58D37DA9-F948-48CA-9A73-F5BBBD533DBF}"
@@ -42,7 +41,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
ProjectSection(SolutionItems) = preProject
.github\workflows\pr.yml = .github\workflows\pr.yml
.github\workflows\release.yml = .github\workflows\release.yml
.github\workflows\integration-test.yml = .github\workflows\integration-test.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Tests.Performance", "tests\Speckle.Sdk.Tests.Performance\Speckle.Sdk.Tests.Performance.csproj", "{870E3396-E6F7-43AE-B120-E651FA4F46BD}"
-118
View File
@@ -1,118 +0,0 @@
name: "speckle-server"
services:
####
# Speckle Server dependencies
#######
postgres:
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- ./.volumes/postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
interval: 5s
timeout: 5s
retries: 30
redis:
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
restart: always
volumes:
- ./.volumes/redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
timeout: 5s
retries: 30
minio:
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
command: server /data --console-address ":9001"
restart: always
volumes:
- ./.volumes/minio-data:/data
ports:
- '127.0.0.1:9000:9000'
- '127.0.0.1:9001:9001'
healthcheck:
test:
[
"CMD-SHELL",
"curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html",
]
interval: 5s
timeout: 30s
retries: 30
start_period: 10s
speckle-server:
image: ghcr.io/specklesystems/speckle-server:latest
restart: always
healthcheck:
test:
- CMD
- /nodejs/bin/node
- -e
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }"
interval: 10s
timeout: 10s
retries: 3
start_period: 90s
ports:
- "0.0.0.0:3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
environment:
# TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://127.0.0.1:8080"
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
FRONTEND_ORIGIN: "http://127.0.0.1:8081"
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
MAX_PROJECT_MODELS_PER_PAGE: 500
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
networks:
default:
name: speckle-server
volumes:
postgres-data:
redis-data:
minio-data:
+10 -8
View File
@@ -12,7 +12,7 @@ services:
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- ./.volumes/postgres-data:/var/lib/postgresql/data/
- postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
@@ -24,7 +24,7 @@ services:
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
restart: always
volumes:
- ./.volumes/redis-data:/data
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
@@ -36,7 +36,7 @@ services:
command: server /data --console-address ":9001"
restart: always
volumes:
- ./.volumes/minio-data:/data
- minio-data:/data
ports:
- '127.0.0.1:9000:9000'
- '127.0.0.1:9001:9001'
@@ -55,7 +55,7 @@ services:
image: speckle/speckle-server:latest
restart: always
healthcheck:
test:
test:
- CMD
- /nodejs/bin/node
- -e
@@ -81,9 +81,9 @@ services:
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
S3_PUBLIC_ENDPOINT: 'http://127.0.0.1:9000'
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
@@ -96,17 +96,19 @@ services:
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
DEBUG: "speckle:*"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
networks:
default:
+9 -2
View File
@@ -2,7 +2,6 @@ using Speckle.Objects.Geometry;
using Speckle.Objects.Other;
using Speckle.Objects.Primitive;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Data;
namespace Speckle.Objects;
@@ -111,7 +110,15 @@ public interface IDisplayValue<out T> : ISpeckleObject
#region Data objects
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>, ISpeckleObject
/// <summary>
/// Specifies properties on objects to be used for data-based workflows
/// </summary>
public interface IProperties : ISpeckleObject
{
Dictionary<string, object?> properties { get; }
}
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>
{
/// <summary>
/// The name of the object, primarily used to decorate the object for consumption in frontend and other apps
-32
View File
@@ -1,32 +0,0 @@
using Speckle.Objects.Geometry;
using Speckle.Sdk.Models;
namespace Speckle.Objects.Other;
/// <summary>
/// Camera class to represent a perspective camera for a 3D view.
/// </summary>
/// <remarks>Assumes a Z-up, right-handed convention for orientation vectors</remarks>
[SpeckleType("Objects.Other.Camera")]
public class Camera : Base
{
/// <summary>
/// The name of the view that is created by this camera
/// </summary>
public required string name { get; set; }
/// <summary>
/// The location of the camera
/// </summary>
public required Point position { get; set; }
/// <summary>
/// The unit up vector of the camera
/// </summary>
public required Vector up { get; set; }
/// <summary>
/// The unit forward vector of the camera
/// </summary>
public required Vector forward { get; set; }
}
+1 -1
View File
@@ -35,6 +35,6 @@ public class RenderMaterial : Base
public Color emissiveColor
{
get => Color.FromArgb(emissive);
set => emissive = value.ToArgb();
set => diffuse = value.ToArgb();
}
}
@@ -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);
}
}
+1 -6
View File
@@ -23,15 +23,10 @@ public class SpeckleGraphQLException : SpeckleException
}
/// <summary>
/// Represents a "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception.
/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception.
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden
/// https://github.com/specklesystems/speckle-server/blob/v2.23.18/packages/server/modules/shared/errors/index.ts#L34
/// </summary>
/// <remarks>
/// Server is a bit inconsistent with these error codes, hence there's 4 different codes that mean "auth no work"
/// Apollo no longer considers "FORBIDDEN" or "UNAUTHENTICATED" as built in error codes, so everything is custom anyway.
/// </remarks>
public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
{
public SpeckleGraphQLForbiddenException() { }
@@ -28,8 +28,7 @@ internal static class GraphQLErrorHandler
var ex = code switch
{
"GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message),
"FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" =>
new SpeckleGraphQLForbiddenException(message),
"FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message),
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
@@ -14,7 +14,7 @@ public sealed class Comment
public string rawText { get; init; }
public ResourceCollection<Comment> replies { get; init; }
public CommentReplyAuthorCollection replyAuthors { get; init; }
public List<ResourceIdentifier> resources { get; init; } //todo: add resourceIds/baseResourceIds
public List<ResourceIdentifier> resources { get; init; }
public string? screenshot { get; init; }
public DateTime updatedAt { get; init; }
public DateTime? viewedAt { get; init; }
@@ -19,9 +19,6 @@ public sealed class ServerInfo
[Obsolete("Don't use")]
public bool frontend2 { get; set; } = true;
/// <summary>
/// The URL that should be used to talk with the server
/// </summary>
/// <remarks>
/// This field is not returned from the GQL API,
/// it should be populated after construction.
@@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Serialisation;
using Speckle.Sdk.Serialisation.V2.Receive;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Api;
@@ -16,7 +15,6 @@ public partial class Operations
/// <exception cref="ArgumentException">No transports were specified</exception>
/// <exception cref="ArgumentNullException">The <paramref name="objectId"/> was <see langword="null"/></exception>
/// <exception cref="SpeckleException">Serialization or Send operation was unsuccessful</exception>
/// <exception cref="HttpRequestException">HTTP layer errors</exception>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> requested cancellation</exception>
public async Task<Base> Receive2(
Uri url,
@@ -24,8 +22,7 @@ public partial class Operations
string objectId,
string? authorizationToken,
IProgress<ProgressArgs>? onProgressAction,
CancellationToken cancellationToken,
DeserializeProcessOptions? options = null
CancellationToken cancellationToken
)
{
using var receiveActivity = activityFactory.Start("Operations.Receive");
@@ -39,8 +36,7 @@ public partial class Operations
streamId,
authorizationToken,
onProgressAction,
cancellationToken,
options
cancellationToken
);
try
{
@@ -48,11 +44,6 @@ public partial class Operations
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
return result;
}
catch (OperationCanceledException)
{
//this is handled by the caller
throw;
}
catch (Exception ex)
{
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
@@ -25,8 +25,7 @@ public partial class Operations
string? authorizationToken,
Base value,
IProgress<ProgressArgs>? onProgressAction,
CancellationToken cancellationToken,
SerializeProcessOptions? options = null
CancellationToken cancellationToken
)
{
using var receiveActivity = activityFactory.Start("Operations.Send");
@@ -39,8 +38,7 @@ public partial class Operations
streamId,
authorizationToken,
onProgressAction,
cancellationToken,
options
cancellationToken
);
try
{
+22 -65
View File
@@ -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 &#x2264; Length &#x2264; 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 &#x2264; Length &#x2264; 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);
}
}
+2 -10
View File
@@ -59,20 +59,15 @@ public class Account : IEquatable<Account>
#region public methods
/// <remarks>The logic is aligned with <c>distinct_id</c> mixpanel property</remarks>
/// <exception cref="ArgumentNullException">Thrown if <see name="userInfo.email"/> was <see langword="null"/></exception>
public string GetHashedEmail()
{
string email = userInfo.email.NotNull();
string email = userInfo?.email ?? "unknown";
return "@" + Md5.GetString(email).ToUpperInvariant();
}
/// <remarks>The logic is aligned with <c>server</c> mixpanel property</remarks>
/// <exception cref="ArgumentNullException">Thrown if <see name="serverInfo.url"/> was <see langword="null"/></exception>
public string GetHashedServer()
{
string url = serverInfo.url.NotNull();
string url = serverInfo?.url ?? AccountManager.DEFAULT_SERVER_URL;
return Md5.GetString(CleanURL(url)).ToUpperInvariant();
}
@@ -102,8 +97,6 @@ public class Account : IEquatable<Account>
#endregion
internal const string LOCAL_IDENTIFIER_DEPRECATION_MESSAGE = "Local identifiers no longer nesseary";
/// <summary>
/// Retrieves the local identifier for the current user.
/// </summary>
@@ -128,6 +121,5 @@ public class Account : IEquatable<Account>
/// https://speckle.xyz?id=123
/// </code>
/// </example>
[Obsolete(LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
internal Uri GetLocalIdentifier() => new($"{serverInfo.url}?id={userInfo.id}");
}
@@ -419,7 +419,6 @@ public sealed class AccountManager(
/// <remarks>
/// <inheritdoc cref="Account.GetLocalIdentifier"/>
/// </remarks>
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
public Uri? GetLocalIdentifierForAccount(Account account)
{
var identifier = account.GetLocalIdentifier();
@@ -441,7 +440,6 @@ public sealed class AccountManager(
/// </summary>
/// <param name="localIdentifier">The local identifier of the account.</param>
/// <returns>The account that matches the local identifier, or null if no match is found.</returns>
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
public Account? GetAccountForLocalIdentifier(Uri localIdentifier)
{
var searchResult = GetAccounts()
+18 -12
View File
@@ -9,14 +9,12 @@ public static class SpecklePathProvider
{
private const string APPLICATION_NAME = "Speckle";
private const string LOG_FOLDER_NAME = "Logs";
private const string BLOB_FOLDER_NAME = "Blobs";
private const string ACCOUNTS_FOLDER_NAME = "Accounts";
public const string USER_DATA_PATH_ENV_VAR = "SPECKLE_USERDATA_PATH";
private static string? Path => Environment.GetEnvironmentVariable(USER_DATA_PATH_ENV_VAR);
private static string UserDataPathEnvVar => "SPECKLE_USERDATA_PATH";
private static string? Path => Environment.GetEnvironmentVariable(UserDataPathEnvVar);
/// <summary>
/// Get the installation path.
@@ -45,7 +43,7 @@ public static class SpecklePathProvider
/// <see cref="Environment.SpecialFolder.ApplicationData"/> path usually maps to
/// <ul>
/// <li>win: <c>%appdata%/</c></li>
/// <li>MacOS: <c>~/Library/Application Support</c></li>
/// <li>MacOS: <c>~/.config/</c></li>
/// <li>Linux: <c>~/.config/</c></li>
/// </ul>
/// </remarks>
@@ -59,18 +57,29 @@ public static class SpecklePathProvider
return pathOverride;
}
// on desktop linux and macos we use the appdata.
// but we might not have write access to the disk
// so the catch falls back to the user profile
try
{
return Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData,
// It's not a given that the folder is already there on all OS-es, so we'll create it
// if the folder doesn't exist, we get back an empty string on OSX,
// which in turn, breaks other stuff down the line.
// passing in the Create option ensures that this directory exists,
// which is not a given on all OS-es.
Environment.SpecialFolderOption.Create
);
}
catch (PlatformNotSupportedException)
catch (SystemException ex) when (ex is PlatformNotSupportedException or ArgumentException)
{
// We might not have write access to the disk to create the folder,
// so we'll fall back to the user profile
//Adding this log just so we confidently know which Exception type to catch here.
// TODO: Must re-add log call when (and if) this get's made as a service
//SpeckleLog.Logger.Warning(ex, "Falling back to user profile path");
// on server linux, there might not be a user setup, things can run under root
// in that case, the appdata variable is most probably not set up
// we fall back to the value of the home folder
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
}
@@ -87,7 +96,4 @@ public static class SpecklePathProvider
Directory.CreateDirectory(path);
return path;
}
public static string LogFolderPath(string applicationAndVersion) =>
EnsureFolderExists(UserSpeckleFolderPath, LOG_FOLDER_NAME, applicationAndVersion);
}
+11 -12
View File
@@ -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,21 +0,0 @@
using Speckle.Sdk.Models.Data;
namespace Speckle.Sdk.Models.Collections;
/// <summary>
/// Root collection that represents the top-level commit object.
/// Extends Collection to include model-wide properties that apply to the entire model.
/// </summary>
[SpeckleType("Speckle.Core.Models.Collections.RootCollection")]
public class RootCollection : Collection, IProperties
{
public RootCollection() { }
public RootCollection(string name)
: base(name) { }
/// <summary>
/// Model-wide properties that apply to the entire model.
/// </summary>
public Dictionary<string, object?> properties { get; set; } = new();
}
@@ -1,10 +0,0 @@
namespace Speckle.Sdk.Models.Data;
/// <summary>
/// Specifies properties on objects to be used for data-based workflows.
/// Can be applied to both objects and collections.
/// </summary>
public interface IProperties
{
Dictionary<string, object?> properties { get; }
}
@@ -109,9 +109,9 @@ public abstract class GraphTraversal<T>
break;
case IList list:
{
for (int i = list.Count - 1; i >= 0; i--)
foreach (object? obj in list)
{
TraverseMemberToStack(stack, list[i], memberName, parent);
TraverseMemberToStack(stack, obj, memberName, parent);
}
break;
+14 -27
View File
@@ -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
{
@@ -9,7 +9,6 @@ public class MemoryServerObjectManager(ConcurrentDictionary<string, string> obje
{
public virtual async IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
[EnumeratorCancellation] CancellationToken cancellationToken
)
@@ -10,7 +10,7 @@ using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.V2.Receive;
public record DeserializeProcessOptions(
bool SkipCache = false, //TODO: This appears to be bugged when set to `true`, `LoadId` depends on sqlite
bool SkipCache = false,
bool ThrowOnMissingReferences = true,
bool SkipInvalidConverts = false,
int? MaxParallelism = null,
@@ -19,7 +19,9 @@ public sealed class ObjectLoader(
IProgress<ProgressArgs>? progress,
ILogger<ObjectLoader> logger,
CancellationToken cancellationToken
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
) : ChannelLoader<BaseItem>(cancellationToken), IObjectLoader
#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
private int? _allChildrenCount;
private long _checkCache;
@@ -27,7 +29,6 @@ public sealed class ObjectLoader(
private long _downloaded;
private long _totalToDownload;
private DeserializeProcessOptions _options = new();
private readonly CancellationToken _cancellationToken = cancellationToken;
[AutoInterfaceIgnore]
public void Dispose() => sqLiteJsonCacheManager.Dispose();
@@ -45,7 +46,7 @@ public sealed class ObjectLoader(
{
//assume everything exists as the root is there.
var allChildren = ClosureParser
.GetClosuresSorted(rootJson, _cancellationToken)
.GetClosuresSorted(rootJson, cancellationToken)
.Select(x => new Id(x.Item1))
.ToList();
//this probably yields away from the Main thread to let host apps update progress
@@ -58,11 +59,11 @@ public sealed class ObjectLoader(
if (!options.SkipServer)
{
rootJson = await serverObjectManager
.DownloadSingleObject(rootId, progress, _cancellationToken)
.DownloadSingleObject(rootId, progress, cancellationToken)
.NotNull()
.ConfigureAwait(false);
IReadOnlyCollection<Id> allChildrenIds = ClosureParser
.GetClosures(rootJson, _cancellationToken)
.GetClosures(rootJson, cancellationToken)
.OrderByDescending(x => x.Item2)
.Select(x => new Id(x.Item1))
.Where(x => !x.Value.StartsWith("blob", StringComparison.Ordinal))
@@ -110,13 +111,12 @@ public sealed class ObjectLoader(
await foreach (
var (id, json) in serverObjectManager.DownloadObjects(
ids.Select(x => x.NotNull()).ToList(),
null, //TODO: Implement attribute masking in a safe way that will not poison SQLite DB.
progress,
_cancellationToken
cancellationToken
)
)
{
_cancellationToken.ThrowIfCancellationRequested();
cancellationToken.ThrowIfCancellationRequested();
Interlocked.Increment(ref _downloaded);
progress?.Report(new(ProgressEvent.DownloadObjects, _downloaded, _totalToDownload));
toCache.Add(new(new(id), new(json), true, null));
@@ -138,7 +138,7 @@ public sealed class ObjectLoader(
{
if (!_options.SkipCache)
{
_cancellationToken.ThrowIfCancellationRequested();
cancellationToken.ThrowIfCancellationRequested();
sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value)));
Interlocked.Exchange(ref _cached, _cached + batch.Count);
progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _allChildrenCount));
@@ -168,7 +168,7 @@ public sealed class ObjectLoader(
private void ThrowIfFailed()
{
//always check for cancellation first
_cancellationToken.ThrowIfCancellationRequested();
cancellationToken.ThrowIfCancellationRequested();
if (Exception is not null)
{
throw new SpeckleException($"Error while loading: {Exception.Message}", Exception);
@@ -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;
}
@@ -21,7 +21,12 @@ public class BaseSerializer(
public IReadOnlyDictionary<Id, ObjectReference> ObjectReferences => _objectReferences;
//leave this sync
public IEnumerable<BaseItem> Serialise(Base obj, bool skipCacheRead, CancellationToken cancellationToken)
public IEnumerable<BaseItem> Serialise(
Base obj,
IReadOnlyDictionary<Id, NodeInfo> childInfo,
bool skipCacheRead,
CancellationToken cancellationToken
)
{
if (!skipCacheRead && obj.id != null)
{
@@ -33,7 +38,7 @@ public class BaseSerializer(
}
}
using var serializer2 = objectSerializerFactory.Create(cancellationToken);
using var serializer2 = objectSerializerFactory.Create(childInfo, cancellationToken);
var items = _pool.Get();
try
{
@@ -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
CancellationToken cancellationToken,
#pragma warning disable CS9107
#pragma warning disable CA2254
SerializeProcessOptions? options = null
) : 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())
@@ -26,6 +26,8 @@ public sealed class ObjectSerializer : IObjectSerializer
{
private HashSet<object> _parentObjects = new();
private readonly IReadOnlyDictionary<Id, NodeInfo> _childCache;
private readonly IBasePropertyGatherer _propertyGatherer;
private readonly CancellationToken _cancellationToken;
@@ -50,6 +52,7 @@ public sealed class ObjectSerializer : IObjectSerializer
/// <param name="cancellationToken"></param>
public ObjectSerializer(
IBasePropertyGatherer propertyGatherer,
IReadOnlyDictionary<Id, NodeInfo> childCache,
Pool<List<(Id, Json, Closures)>> chunksPool,
Pool<List<DataChunk>> chunks2Pool,
Pool<List<object?>> chunks3Pool,
@@ -57,6 +60,7 @@ public sealed class ObjectSerializer : IObjectSerializer
)
{
_propertyGatherer = propertyGatherer;
_childCache = childCache;
_chunksPool = chunksPool;
_chunks2Pool = chunks2Pool;
_chunks3Pool = chunks3Pool;
@@ -295,14 +299,28 @@ public sealed class ObjectSerializer : IObjectSerializer
private (Id, Json)? SerializeDetachedBase(Base baseObj, Closures closures)
{
Closures childClosures = [];
var sb = Pools.StringBuilders.Get();
using var writer = new StringWriter(sb);
using var jsonWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer);
var id = SerializeBaseWithClosures(baseObj, jsonWriter, childClosures, true);
var json = new Json(writer.ToString());
Pools.StringBuilders.Return(sb);
closures.IncrementClosures(childClosures);
Closures childClosures;
Id id;
Json json;
//avoid multiple serialization to get closures
if (baseObj.id != null && _childCache.TryGetValue(new(baseObj.id), out var info))
{
id = new Id(baseObj.id);
childClosures = info.GetClosures(_cancellationToken);
json = info.Json;
closures.IncrementClosures(childClosures);
}
else
{
childClosures = [];
var sb = Pools.StringBuilders.Get();
using var writer = new StringWriter(sb);
using var jsonWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer);
id = SerializeBaseWithClosures(baseObj, jsonWriter, childClosures, true);
closures.IncrementClosures(childClosures);
json = new Json(writer.ToString());
Pools.StringBuilders.Return(sb);
}
var json2 = ReferenceGenerator.CreateReference(id);
closures.MergeClosure(id);
// add to obj refs to return
@@ -343,7 +361,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
{
@@ -12,6 +12,6 @@ public class ObjectSerializerFactory(IBasePropertyGatherer propertyGatherer) : I
private readonly Pool<List<DataChunk>> _chunk2Pool = Pools.CreateListPool<DataChunk>();
private readonly Pool<List<object?>> _chunk3Pool = Pools.CreateListPool<object?>();
public IObjectSerializer Create(CancellationToken cancellationToken) =>
new ObjectSerializer(propertyGatherer, _chunkPool, _chunk2Pool, _chunk3Pool, cancellationToken);
public IObjectSerializer Create(IReadOnlyDictionary<Id, NodeInfo> baseCache, CancellationToken cancellationToken) =>
new ObjectSerializer(propertyGatherer, baseCache, _chunkPool, _chunk2Pool, _chunk3Pool, cancellationToken);
}
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
@@ -17,7 +18,6 @@ public record SerializeProcessOptions(
{
public int? MaxHttpSendBatchSize { get; set; }
public int? MaxCacheBatchSize { get; set; }
public int? MaxBlobBatchSize { get; set; }
public int? MaxParallelism { get; set; }
}
@@ -37,8 +37,8 @@ public sealed class SerializeProcess(
IBaseChildFinder baseChildFinder,
IBaseSerializer baseSerializer,
ILoggerFactory loggerFactory,
SerializeProcessOptions options,
CancellationToken cancellationToken
CancellationToken cancellationToken,
SerializeProcessOptions? options = null
) : ISerializeProcess
{
private static readonly Dictionary<Id, NodeInfo> EMPTY_CLOSURES = new();
@@ -64,8 +64,13 @@ public sealed class SerializeProcess(
ThreadPriority.BelowNormal,
Environment.ProcessorCount * 2
);
private readonly SerializeProcessOptions _options = options ?? new();
private readonly Pool<Dictionary<Id, NodeInfo>> _currentClosurePool = Pools.CreateDictionaryPool<Id, NodeInfo>();
private readonly Pool<ConcurrentDictionary<Id, NodeInfo>> _childClosurePool = Pools.CreateConcurrentDictionaryPool<
Id,
NodeInfo
>();
private readonly Pool<List<Task<Dictionary<Id, NodeInfo>>>> _taskResultPool = Pools.CreateListPool<
Task<Dictionary<Id, NodeInfo>>
@@ -108,14 +113,13 @@ public sealed class SerializeProcess(
try
{
var channelTask = objectSaver.Start(
options.MaxParallelism,
options.MaxHttpSendBatchSize,
options.MaxBlobBatchSize,
options.MaxCacheBatchSize,
options?.MaxParallelism,
options?.MaxHttpSendBatchSize,
options?.MaxCacheBatchSize,
_processSource.Token
);
var findTotalObjectsTask = Task.CompletedTask;
if (!options.SkipFindTotalObjects)
if (!_options.SkipFindTotalObjects)
{
ThrowIfFailed();
findTotalObjectsTask = Task.Factory.StartNew(
@@ -221,6 +225,7 @@ public sealed class SerializeProcess(
return EMPTY_CLOSURES;
}
var childClosures = _childClosurePool.Get();
foreach (var childClosure in taskClosures)
{
if (IsCancelled())
@@ -229,6 +234,7 @@ public sealed class SerializeProcess(
}
foreach (var kvp in childClosure)
{
childClosures[kvp.Key] = kvp.Value;
if (IsCancelled())
{
return EMPTY_CLOSURES;
@@ -243,7 +249,7 @@ public sealed class SerializeProcess(
return EMPTY_CLOSURES;
}
var items = baseSerializer.Serialise(obj, options.SkipCacheRead, _processSource.Token);
var items = baseSerializer.Serialise(obj, childClosures, _options.SkipCacheRead, _processSource.Token);
if (IsCancelled())
{
@@ -251,27 +257,33 @@ public sealed class SerializeProcess(
}
var currentClosures = _currentClosurePool.Get();
Interlocked.Increment(ref _objectCount);
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
foreach (var item in items)
try
{
if (IsCancelled())
Interlocked.Increment(ref _objectCount);
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
foreach (var item in items)
{
return EMPTY_CLOSURES;
}
if (IsCancelled())
{
return EMPTY_CLOSURES;
}
if (item.NeedsStorage)
{
Interlocked.Increment(ref _objectsSerialized);
await objectSaver.SaveAsync(item).ConfigureAwait(false);
}
if (item.NeedsStorage)
{
Interlocked.Increment(ref _objectsSerialized);
await objectSaver.SaveAsync(item).ConfigureAwait(false);
}
if (!currentClosures.ContainsKey(item.Id))
{
currentClosures.Add(item.Id, new NodeInfo(item.Json, item.Closures));
if (!currentClosures.ContainsKey(item.Id))
{
currentClosures.Add(item.Id, new NodeInfo(item.Json, item.Closures));
}
}
}
finally
{
_childClosurePool.Return(childClosures);
}
return currentClosures;
}
@@ -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,16 +43,14 @@ public class SerializeProcessFactory(
progress,
sqLiteJsonCacheManager,
serverObjectManager,
serverBlobManager,
loggerFactory.CreateLogger<ObjectSaver>(),
options ?? new SerializeProcessOptions(),
cancellationToken
),
baseChildFinder,
new BaseSerializer(sqLiteJsonCacheManager, objectSerializerFactory),
loggerFactory,
options ?? new SerializeProcessOptions(),
cancellationToken
cancellationToken,
options
);
public ISerializeProcess CreateSerializeProcess(
@@ -79,7 +67,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();
}
}
@@ -51,7 +51,6 @@ public class ServerObjectManager : IServerObjectManager
public async IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
[EnumeratorCancellation] CancellationToken cancellationToken
)
@@ -60,14 +59,10 @@ public class ServerObjectManager : IServerObjectManager
cancellationToken.ThrowIfCancellationRequested();
using var childrenHttpMessage = new HttpRequestMessage();
childrenHttpMessage.RequestUri = new Uri($"/api/v2/projects/{_streamId}/object-stream/", UriKind.Relative);
childrenHttpMessage.RequestUri = new Uri($"/api/getobjects/{_streamId}", UriKind.Relative);
childrenHttpMessage.Method = HttpMethod.Post;
Dictionary<string, object> postParameters = new() { { "objectIds", objectIds } };
if (!string.IsNullOrWhiteSpace(attributeMask))
{
postParameters.Add("attributeMask", attributeMask.NotNull());
}
Dictionary<string, string> postParameters = new() { { "objects", JsonConvert.SerializeObject(objectIds) } };
string serializedPayload = JsonConvert.SerializeObject(postParameters);
childrenHttpMessage.Content = new StringContent(serializedPayload, Encoding.UTF8, "application/json");
childrenHttpMessage.Headers.Add("Accept", "text/plain");
@@ -3,13 +3,6 @@
<TargetFramework>net8.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="altcover" />
<PackageReference Include="AwesomeAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
<ProjectReference Include="..\..\src\Speckle.Automate.Sdk\Speckle.Automate.Sdk.csproj" />
@@ -25,9 +25,7 @@ public sealed class AutomationContextTest : IAsyncLifetime
public async Task InitializeAsync()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddAutomateSdk();
var serviceProvider = serviceCollection.BuildServiceProvider();
var serviceProvider = TestServiceSetup.GetServiceProvider();
_account = await Fixtures.SeedUser().ConfigureAwait(false);
_client = serviceProvider.GetRequiredService<IClientFactory>().Create(_account);
_runner = serviceProvider.GetRequiredService<IAutomationRunner>();
@@ -44,7 +42,7 @@ public sealed class AutomationContextTest : IAsyncLifetime
private async Task<AutomationRunData> AutomationRunData(Base testObject)
{
Project project = await _client.Project.Create(new("Automate function e2e test", null, ProjectVisibility.Public));
const string BRANCH_NAME = "Trigger";
const string BRANCH_NAME = "main";
var model = await _client.Model.Create(new(BRANCH_NAME, null, project.id));
string modelId = model.id;
@@ -2,28 +2,6 @@
"version": 2,
"dependencies": {
"net8.0": {
"altcover": {
"type": "Direct",
"requested": "[9.0.1, )",
"resolved": "9.0.1",
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
},
"AwesomeAssertions": {
"type": "Direct",
"requested": "[8.1.0, )",
"resolved": "8.1.0",
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.13.0, )",
"resolved": "17.13.0",
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
"dependencies": {
"Microsoft.CodeCoverage": "17.13.0",
"Microsoft.TestPlatform.TestHost": "17.13.0"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
@@ -46,18 +24,6 @@
"resolved": "0.9.6",
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
},
"xunit.assert": {
"type": "Direct",
"requested": "[2.9.3, )",
"resolved": "2.9.3",
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
},
"xunit.runner.visualstudio": {
"type": "Direct",
"requested": "[3.0.2, )",
"resolved": "3.0.2",
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
},
"Argon": {
"type": "Transitive",
"resolved": "0.28.0",
@@ -398,6 +364,18 @@
"xunit.runner.visualstudio": "[3.0.2, )"
}
},
"altcover": {
"type": "CentralTransitive",
"requested": "[9.0.1, )",
"resolved": "9.0.1",
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
},
"AwesomeAssertions": {
"type": "CentralTransitive",
"requested": "[8.1.0, )",
"resolved": "8.1.0",
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
},
"GraphQL.Client": {
"type": "CentralTransitive",
"requested": "[6.0.0, )",
@@ -446,6 +424,16 @@
"Microsoft.Extensions.Options": "2.2.0"
}
},
"Microsoft.NET.Test.Sdk": {
"type": "CentralTransitive",
"requested": "[17.13.0, )",
"resolved": "17.13.0",
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
"dependencies": {
"Microsoft.CodeCoverage": "17.13.0",
"Microsoft.TestPlatform.TestHost": "17.13.0"
}
},
"Moq": {
"type": "CentralTransitive",
"requested": "[4.20.72, )",
@@ -527,6 +515,18 @@
"xunit.assert": "2.9.3",
"xunit.core": "[2.9.3]"
}
},
"xunit.assert": {
"type": "CentralTransitive",
"requested": "[2.9.3, )",
"resolved": "2.9.3",
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
},
"xunit.runner.visualstudio": {
"type": "CentralTransitive",
"requested": "[3.0.2, )",
"resolved": "3.0.2",
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
}
}
}
@@ -18,7 +18,10 @@ public sealed class ObjectsSerializationTest
private static IReadOnlyList<(Id, Json, Dictionary<Id, int>)> Serialize(Base data)
{
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
new Dictionary<Id, NodeInfo>(),
default
);
return serializer.Serialize(data).ToList();
}
@@ -4,7 +4,6 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
@@ -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,9 +40,8 @@ public class DataObjectTests
new MemoryJsonCacheManager(json),
new DummyServerObjectManager(),
null,
null,
default,
new SerializeProcessOptions(false, false, true, true)
new SerializeProcessOptions(true, true, false, true)
);
await serializeProcess.Serialize(x);
await VerifyJson(json.Single().Value.Value).UseParameters(type);
@@ -41,7 +41,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true)
new SerializeProcessOptions(false, false, true, true)
);
await serializeProcess.Serialize(@base);
@@ -123,7 +123,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -150,7 +150,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -172,7 +172,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -239,7 +239,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true)
new SerializeProcessOptions(false, false, true, true)
);
var results = await serializeProcess.Serialize(@base);
@@ -272,7 +272,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(true, true, false, true)
new SerializeProcessOptions(false, false, true, true)
);
var results = await serializeProcess.Serialize(@base);
await VerifyJsonDictionary(objects);
@@ -338,7 +338,6 @@ public class DummyServerObjectManager : IServerObjectManager
{
public IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
@@ -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)
{
@@ -116,14 +113,14 @@ public class ExceptionTests
new ExceptionServerObjectManager(),
null,
new NullLogger<ObjectLoader>(),
CancellationToken.None
default
);
await using var process = new DeserializeProcess(
o,
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
CancellationToken.None,
default,
new(SkipCache: true, MaxParallelism: 1, SkipServer: true)
);
@@ -147,7 +144,7 @@ public class ExceptionTests
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
CancellationToken.None,
default,
new(true, MaxParallelism: 1)
);
@@ -172,7 +169,7 @@ public class ExceptionTests
null,
new BaseDeserializer(new ObjectDeserializerFactory()),
new NullLoggerFactory(),
CancellationToken.None,
default,
new(MaxParallelism: 1)
);
@@ -197,14 +194,16 @@ public class ExceptionTests
[SpeckleType("Objects.Geometry.BadBase")]
public class BadBase : Base
{
#pragma warning disable CA1065
public string BadProp => throw new NotImplementedException();
#pragma warning restore CA1065
}
[Fact]
public void Test_SpeckleSerializerException()
{
var factory = new ObjectSerializerFactory(new BasePropertyGatherer());
var serializer = factory.Create(default);
var serializer = factory.Create(new Dictionary<Id, NodeInfo>(), default);
Assert.Throws<SpeckleSerializeException>(() =>
{
var _ = serializer.Serialize(new BadBase()).ToList();
@@ -3,6 +3,7 @@ using Speckle.Objects.Primitive;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;
using Speckle.Sdk.Serialisation;
using Speckle.Sdk.Serialisation.V2.Send;
namespace Speckle.Sdk.Serialization.Tests;
@@ -19,7 +20,10 @@ public class ExternalIdTests
public async Task ExternalIdTest_Detached()
{
var p = new Polyline() { units = "cm", value = [1, 2] };
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
new Dictionary<Id, NodeInfo>(),
default
);
var objects = serializer.Serialize(p).ToDictionary(x => x.Item1, x => x.Item2);
await VerifyJsonDictionary(objects);
@@ -41,7 +45,10 @@ public class ExternalIdTests
knots = [],
weights = [],
};
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
new Dictionary<Id, NodeInfo>(),
default
);
var objects = serializer.Serialize(curve).ToDictionary(x => x.Item1, x => x.Item2);
await VerifyJsonDictionary(objects);
@@ -64,7 +71,10 @@ public class ExternalIdTests
weights = [],
};
var polycurve = new Polycurve() { segments = [curve], units = "cm" };
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
new Dictionary<Id, NodeInfo>(),
default
);
var objects = serializer.Serialize(polycurve).ToDictionary(x => x.Item1, x => x.Item2);
await VerifyJsonDictionary(objects);
@@ -89,7 +99,10 @@ public class ExternalIdTests
var polycurve = new Polycurve() { segments = [curve], units = "cm" };
var @base = new Base();
@base.SetDetachedProp("profile", polycurve);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
new Dictionary<Id, NodeInfo>(),
default
);
var objects = serializer.Serialize(@base).ToDictionary(x => x.Item1, x => x.Item2);
await VerifyJsonDictionary(objects);
}
@@ -8,7 +8,6 @@ public class ExceptionServerObjectManager : IServerObjectManager
{
public IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
@@ -146,11 +146,11 @@ 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);
}
[Theory(Skip = "Takes too long")]
[Theory]
[InlineData("RevitObject.json.gz", "3416d3fe01c9196115514c4a2f41617b", 7818)]
public async Task Roundtrip_Test_Old(string fileName, string _, int count)
{
@@ -186,6 +186,8 @@ public class SerializationTests
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public async Task Roundtrip_Test_New(int concurrency)
{
@@ -203,7 +205,7 @@ public class SerializationTests
new DummyReceiveServerObjectManager(closures),
null,
new NullLogger<ObjectLoader>(),
CancellationToken.None
default
)
)
{
@@ -227,7 +229,6 @@ public class SerializationTests
SqLiteJsonCacheManager.FromMemory(1),
new MemoryServerObjectManager(newIdToJson),
null,
null,
default,
new SerializeProcessOptions(false, false, false, true) { MaxCacheBatchSize = 1, MaxParallelism = concurrency }
)
@@ -31,17 +31,14 @@ public class ServerObjectManagerTests : MoqTest
var jObject = new JObject { { "id", id }, { "value", true } };
var jObject2 = new JObject { { "id", id2 }, { "value", true } };
var mockHttp = new MockHttpMessageHandler();
Dictionary<string, object> postParameters = new()
Dictionary<string, string> postParameters = new()
{
{
"objectIds",
new List<string> { id, id2 }
},
{ "objects", JsonConvert.SerializeObject(new List<string> { id, id2 }) },
};
string serializedPayload = JsonConvert.SerializeObject(postParameters);
mockHttp
.When(HttpMethod.Post, $"http://localhost/api/v2/projects/{streamId}/object-stream/")
.When(HttpMethod.Post, $"http://localhost/api/getobjects/{streamId}")
.WithContent(serializedPayload)
.Respond(
"application/json",
@@ -62,7 +59,7 @@ public class ServerObjectManagerTests : MoqTest
token,
new(timeout: TimeSpan.FromSeconds(timeout))
);
var results = serverObjectManager.DownloadObjects(new List<string> { id, id2 }, null, null, ct);
var results = serverObjectManager.DownloadObjects(new List<string> { id, id2 }, null, ct);
var objects = new JObject();
await foreach (var (x, json) in results)
{
@@ -10,7 +10,6 @@ public class DummyReceiveServerObjectManager(IReadOnlyDictionary<string, string>
{
public async IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
[EnumeratorCancellation] CancellationToken cancellationToken
)
@@ -9,7 +9,6 @@ public class DummySendServerObjectManager(ConcurrentDictionary<string, string> s
{
public IAsyncEnumerable<(string, string)> DownloadObjects(
IReadOnlyCollection<string> objectIds,
string? attributeMask,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken
) => throw new NotImplementedException();
+1 -13
View File
@@ -8,19 +8,7 @@ public abstract class MoqTest : IDisposable
{
protected MoqTest() => Repository = new(MockBehavior.Strict);
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)
{
Repository.VerifyAll();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose() => Repository.VerifyAll();
protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict);
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
@@ -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);
@@ -93,7 +93,7 @@ public class ProjectResourceExceptionalTests : IAsyncLifetime
new(_testProject.id, "My new name", ProjectVisibility.Public, "NonExistentWorkspace")
)
);
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLForbiddenException>();
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLException>();
}
[Theory]
@@ -10,12 +10,7 @@ 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
#else
private const int WAIT_PERIOD = 400; // For CI runs, a much smaller wait time is acceptable
#endif
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 400;
private const int WAIT_PERIOD = 300;
private IClient _testUser;
private Project _testProject;
private Model _testModel;
@@ -37,101 +32,105 @@ public class SubscriptionResourceTests : IAsyncLifetime
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
}
[Fact(Timeout = TIMEOUT)]
[Fact]
public async Task UserProjectsUpdated_SubscriptionIsCalled()
{
TaskCompletionSource<UserProjectsUpdatedMessage> tcs = new();
UserProjectsUpdatedMessage? subscriptionMessage = null;
using var sub = Sut.CreateUserProjectsUpdatedSubscription();
sub.Listeners += (_, message) => tcs.SetResult(message);
sub.Listeners += (_, message) => subscriptionMessage = message;
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await _testUser.Project.Create(new(null, null, null));
var subscriptionMessage = await tcs.Task;
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(UserProjectsUpdatedMessageType.ADDED);
subscriptionMessage.project.Should().NotBeNull();
}
[Fact(Timeout = TIMEOUT)]
[Fact]
public async Task ProjectModelsUpdated_SubscriptionIsCalled()
{
TaskCompletionSource<ProjectModelsUpdatedMessage> tcs = new();
ProjectModelsUpdatedMessage? subscriptionMessage = null;
using var sub = Sut.CreateProjectModelsUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => tcs.SetResult(message);
sub.Listeners += (_, message) => subscriptionMessage = message;
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
CreateModelInput input = new("my model", "myDescription", _testProject.id);
var created = await _testUser.Model.Create(input);
var subscriptionMessage = await tcs.Task;
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectModelsUpdatedMessageType.CREATED);
subscriptionMessage.model.Should().NotBeNull();
}
[Fact(Timeout = TIMEOUT)]
[Fact]
public async Task ProjectUpdated_SubscriptionIsCalled()
{
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
ProjectUpdatedMessage? subscriptionMessage = null;
using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => tcs.SetResult(message);
sub.Listeners += (_, message) => subscriptionMessage = 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);
var subscriptionMessage = await tcs.Task;
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectUpdatedMessageType.UPDATED);
subscriptionMessage.project.Should().NotBeNull();
}
[Fact(Timeout = TIMEOUT)]
[Fact]
public async Task ProjectVersionsUpdated_SubscriptionIsCalled()
{
TaskCompletionSource<ProjectVersionsUpdatedMessage> tcs = new();
ProjectVersionsUpdatedMessage? subscriptionMessage = null;
using var sub = Sut.CreateProjectVersionsUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => tcs.SetResult(message);
sub.Listeners += (_, message) => subscriptionMessage = message;
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
var subscriptionMessage = await tcs.Task;
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectVersionsUpdatedMessageType.CREATED);
subscriptionMessage.version.Should().NotBeNull();
}
[Fact(Skip = CommentResourceTests.SERVER_SKIP_MESSAGE, Timeout = TIMEOUT)]
[Fact(Skip = CommentResourceTests.SERVER_SKIP_MESSAGE)]
public async Task ProjectCommentsUpdated_SubscriptionIsCalled()
{
string resourceIdString = $"{_testProject.id},{_testModel.id},{_testVersion}";
ProjectCommentsUpdatedMessage? subscriptionMessage = null;
TaskCompletionSource<ProjectCommentsUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectCommentsUpdatedSubscription(new(_testProject.id, resourceIdString));
sub.Listeners += (_, message) => tcs.SetResult(message);
sub.Listeners += (_, message) => subscriptionMessage = message;
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await Fixtures.CreateComment(_testUser, _testProject.id, _testModel.id, _testVersion.id);
var subscriptionMessage = await tcs.Task;
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
subscriptionMessage.comment.Should().NotBeNull();
}
@@ -0,0 +1,85 @@
using FluentAssertions;
using GraphQL.Client.Http;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Credentials;
namespace Speckle.Sdk.Tests.Integration.Credentials;
public class UserServerInfoTests : IAsyncLifetime
{
private Account _acc;
public Task DisposeAsync() => Task.CompletedTask;
public async Task InitializeAsync()
{
_acc = await Fixtures.SeedUser();
}
[Fact]
public async Task IsFrontEnd2True()
{
ServerInfo result = await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetServerInfo(new("https://app.speckle.systems/"));
result.Should().NotBeNull();
result.frontend2.Should().BeTrue();
}
[Fact]
public async Task GetServerInfo_ExpectFail_NoServer()
{
Uri serverUrl = new("http://invalidserver.local");
await FluentActions
.Invoking(async () =>
await Fixtures.ServiceProvider.GetRequiredService<IAccountManager>().GetServerInfo(serverUrl)
)
.Should()
.ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task GetUserInfo()
{
Uri serverUrl = new(_acc.serverInfo.url);
UserInfo result = await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetUserInfo(_acc.token, serverUrl);
result.id.Should().Be(_acc.userInfo.id);
result.name.Should().Be(_acc.userInfo.name);
result.email.Should().Be(_acc.userInfo.email);
result.company.Should().Be(_acc.userInfo.company);
result.avatar.Should().Be(_acc.userInfo.avatar);
}
[Fact]
public async Task GetUserInfo_ExpectFail_NoServer()
{
Uri serverUrl = new("http://invalidserver.local");
await FluentActions
.Invoking(async () =>
await Fixtures.ServiceProvider.GetRequiredService<IAccountManager>().GetUserInfo("", serverUrl)
)
.Should()
.ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task GetUserInfo_ExpectFail_NoUser()
{
Uri serverUrl = new(_acc.serverInfo.url);
await FluentActions
.Invoking(async () =>
await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetUserInfo("Bearer 08913c3c1e7ac65d779d1e1f11b942a44ad9672ca9", serverUrl)
)
.Should()
.ThrowAsync<GraphQLHttpRequestException>();
}
}
@@ -15,8 +15,6 @@ using Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
using Speckle.Sdk.Transports;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
[assembly: AssemblyTrait("Category", "Integration")]
namespace Speckle.Sdk.Tests.Integration;
public static class Fixtures
@@ -1,6 +0,0 @@
{
"Data": {},
"Message": "Response status code does not indicate success: 404 (Not Found).",
"StatusCode": "NotFound",
"Type": "HttpRequestException"
}
@@ -1,6 +0,0 @@
{
"Data": {},
"Message": "Response status code does not indicate success: 404 (Not Found).",
"StatusCode": "NotFound",
"Type": "HttpRequestException"
}
@@ -1,4 +0,0 @@
{
"ConvertedReferences": {},
"RootId": "5313a8f61e1fa7abe9bf716ddfc767bd"
}
@@ -1,18 +0,0 @@
{
"Data": {},
"InnerException": {
"$type": "SpeckleSerializeException",
"Data": {},
"InnerException": {
"$type": "ArgumentException",
"Data": {},
"Message": "Unsupported value in serialization: System.Text.StringBuilder",
"ParamName": "obj",
"Type": "ArgumentException"
},
"Message": "Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base",
"Type": "SpeckleSerializeException"
},
"Message": "Error while sending: Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base",
"Type": "SpeckleException"
}
@@ -1,190 +0,0 @@
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Tests.Integration;
public sealed class SendReceiveTests : IAsyncLifetime
{
private Project _project;
private IClient _client;
private IOperations _operations;
private const string NON_EXISTENT_OBJECT_ID = "0a480dfb7aa774f19a82bee9d6320abd";
private const string NON_EXISTENT_PROJECT_ID = "8cdc651d13";
public async Task InitializeAsync()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
var serviceProvider = TestServiceSetup.GetServiceProvider();
_operations = serviceProvider.GetRequiredService<IOperations>();
ClearCache();
_client = await Fixtures.SeedUserWithClient();
_project = await _client.Project.Create(new("Blobber", "Flobber", ProjectVisibility.Private));
}
[Fact]
public async Task SendAndReceive()
{
var myObject = Fixtures.GenerateNestedObject();
string expectedId = myObject.GetId(true);
//SEND
var fistSend = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
myObject,
null,
CancellationToken.None
);
Assert.Equal(expectedId, fistSend.RootId);
await Verify(fistSend);
//RECEIVE
var received = await _operations.Receive2(
_client.ServerUrl,
_project.id,
fistSend.RootId,
_client.Account.token,
null,
CancellationToken.None
);
Assert.Equal(expectedId, received.id);
//SEND AGAIN!
var secondSend = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
received,
null,
CancellationToken.None
);
Assert.Equal(expectedId, secondSend.RootId);
//RECEIVE AGAIN, but using cache
ClearCache();
var secondReceive = await _operations.Receive2(
_client.ServerUrl,
_project.id,
fistSend.RootId,
_client.Account.token,
null,
CancellationToken.None
);
Assert.Equal(expectedId, secondReceive.id);
}
private void ClearCache() { }
[Fact]
public async Task ReceiveNonExistentObjectThrows()
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
CancellationToken.None,
new(true)
);
});
await Verify(ex);
}
[Fact]
public async Task ReceiveNonExistentProjectThrows()
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
NON_EXISTENT_PROJECT_ID,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
CancellationToken.None,
new(true)
);
});
await Verify(ex);
}
[Fact]
public async Task SendInvalidData()
{
var myObject = Fixtures.GenerateNestedObject();
myObject["invalidProp"] = new StringBuilder(); //Serializer does not support serializing this type
var ex = await Assert.ThrowsAsync<SpeckleException>(async () =>
{
_ = await _operations.Send2(
_client.ServerUrl,
_project.id,
_client.Account.token,
myObject,
null,
CancellationToken.None,
new(SkipCacheRead: true, SkipCacheWrite: true)
);
});
await Verify(ex);
}
[Fact]
public async Task ReceiveNonAuthThrows()
{
using IClient unauthed = Fixtures.Unauthed;
await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
_ = await _operations.Receive2(
unauthed.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
unauthed.Account.token,
null,
CancellationToken.None,
new(true)
);
});
}
[Fact]
public async Task ReceiveCancellation()
{
using CancellationTokenSource ct = new();
await ct.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
{
_ = await _operations.Receive2(
_client.ServerUrl,
_project.id,
NON_EXISTENT_OBJECT_ID,
_client.Account.token,
null,
ct.Token,
new(true)
);
});
}
public Task DisposeAsync()
{
_client?.Dispose();
return Task.CompletedTask;
}
}
@@ -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());
}
}
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
@@ -11,7 +11,6 @@ public class GraphQLErrorHandlerTests
{
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } }];
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } }];
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHORIZED_ACCESS_ERROR" } }];
yield return [typeof(SpeckleGraphQLInternalErrorException), new Map { { "code", "INTERNAL_SERVER_ERROR" } }];
yield return [typeof(SpeckleGraphQLStreamNotFoundException), new Map { { "code", "STREAM_NOT_FOUND" } }];
yield return [typeof(SpeckleGraphQLBadInputException), new Map { { "code", "BAD_USER_INPUT" } }];
@@ -9,7 +9,7 @@ using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Tests.Unit.Credentials;
public sealed class AccountManagerTests : MoqTest
public class AccountManagerTests : MoqTest
{
private class TestAccountFactory : IAccountFactory
{
@@ -36,9 +36,7 @@ public sealed class AccountManagerTests : MoqTest
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountStorage;
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountAddLockStorage;
#pragma warning disable CA2213
private readonly AccountManager _accountManager;
#pragma warning restore CA2213
public AccountManagerTests()
{
@@ -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);
}
}
@@ -31,7 +31,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest
baseChildFinderMock.Object,
baseSerializerMock.Object,
loggerFactoryMock.Object,
new(),
cts.Token
);
var ex = new Exception("Test error");
@@ -68,7 +67,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest
baseChildFinderMock.Object,
baseSerializerMock.Object,
loggerFactoryMock.Object,
new(),
cts.Token
);
var ex = new OperationCanceledException();
@@ -100,7 +98,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest
baseChildFinderMock.Object,
baseSerializerMock.Object,
loggerFactoryMock.Object,
new(),
cts.Token
);
var ex = new AggregateException(new OperationCanceledException());