Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c517dead03 | |||
| 2b61ab7d2e | |||
| 4b319499c3 | |||
| d4055c6ff1 | |||
| af0fc9f669 | |||
| edbc884d74 | |||
| 025d7f70ba | |||
| 70acc06f37 | |||
| a2c99a537a | |||
| 906ff9c3ff | |||
| 515d45528d | |||
| abf86eda03 |
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🔐 Login to Github Container Registry
|
- name: 🔐 Login to Github Container Registry
|
||||||
if: ${{ inputs.use-internal-image }}
|
if: ${{ inputs.use-internal-image }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: "ghcr.io"
|
registry: "ghcr.io"
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
name: PR Test
|
name: PR Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main" # Need to run for codecov to compare against the BASE
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -46,13 +46,6 @@ jobs:
|
|||||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov with GitHub Action
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
fail_ci_if_error: true
|
|
||||||
files: tests/**/coverage.xml
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
- name: NuGet login (OIDC → temp API key)
|
- name: NuGet login (OIDC → temp API key)
|
||||||
uses: NuGet/login@v1
|
uses: NuGet/login@v1
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public interface ISdkActivity : IDisposable
|
|||||||
void SetTag(string key, object? value);
|
void SetTag(string key, object? value);
|
||||||
void RecordException(Exception e);
|
void RecordException(Exception e);
|
||||||
string TraceId { get; }
|
string TraceId { get; }
|
||||||
|
string SpanId { get; }
|
||||||
void SetStatus(SdkActivityStatusCode code);
|
void SetStatus(SdkActivityStatusCode code);
|
||||||
|
|
||||||
void InjectHeaders(Action<string, string> header);
|
void InjectHeaders(Action<string, string> header);
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using Speckle.Connectors.Logging;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Logging;
|
namespace Speckle.Sdk.Logging;
|
||||||
|
|
||||||
public interface ISdkActivityFactory : IDisposable
|
public interface ISdkActivityFactory : IDisposable
|
||||||
{
|
{
|
||||||
ISdkActivity? Start(string? name = default, [CallerMemberName] string source = "");
|
ISdkActivity? Start(
|
||||||
|
string? name = null,
|
||||||
|
SdkActivityKind kind = SdkActivityKind.Internal,
|
||||||
|
[CallerMemberName] string source = ""
|
||||||
|
);
|
||||||
|
|
||||||
|
ISdkActivity? StartRemote(
|
||||||
|
string traceContext,
|
||||||
|
SdkActivityKind kind,
|
||||||
|
string? name = null,
|
||||||
|
[CallerMemberName] string source = ""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Speckle.Connectors.Logging;
|
||||||
|
|
||||||
|
public enum SdkActivityKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default value.
|
||||||
|
/// Indicates that the Activity represents an internal operation within an application, as opposed to an operations with remote parents or children.
|
||||||
|
/// </summary>
|
||||||
|
Internal = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server activity represents request incoming from external component.
|
||||||
|
/// </summary>
|
||||||
|
Server = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client activity represents outgoing request to the external component.
|
||||||
|
/// </summary>
|
||||||
|
Client = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Producer activity represents output provided to external components.
|
||||||
|
/// </summary>
|
||||||
|
Producer = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consumer activity represents output received from an external component.
|
||||||
|
/// </summary>
|
||||||
|
Consumer = 4,
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ public record ModelIngestionCreateInput(
|
|||||||
string modelId,
|
string modelId,
|
||||||
string projectId,
|
string projectId,
|
||||||
string progressMessage,
|
string progressMessage,
|
||||||
SourceDataInput sourceData
|
SourceDataInput sourceData,
|
||||||
|
int? maxIdleTimeoutSeconds = null
|
||||||
);
|
);
|
||||||
|
|
||||||
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Speckle.Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
|
||||||
public class LimitedWorkspace
|
public class LimitedWorkspace
|
||||||
@@ -6,8 +8,12 @@ public class LimitedWorkspace
|
|||||||
public string name { get; init; }
|
public string name { get; init; }
|
||||||
public string? role { get; init; }
|
public string? role { get; init; }
|
||||||
public string slug { get; init; }
|
public string slug { get; init; }
|
||||||
public string? logo { get; init; }
|
public string? logoUri { get; init; }
|
||||||
public string? description { get; init; }
|
public string? description { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Obsolete($"Deprecated, use {nameof(logoUri)} instead", true)]
|
||||||
|
public string? logo { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Workspace : LimitedWorkspace
|
public class Workspace : LimitedWorkspace
|
||||||
@@ -16,9 +22,13 @@ public class Workspace : LimitedWorkspace
|
|||||||
public DateTime updatedAt { get; init; }
|
public DateTime updatedAt { get; init; }
|
||||||
public bool readOnly { get; init; }
|
public bool readOnly { get; init; }
|
||||||
public WorkspacePermissionChecks permissions { get; init; }
|
public WorkspacePermissionChecks permissions { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Obsolete("Workspaces no longer have creation state, is always created true", true)]
|
||||||
public WorkspaceCreationState? creationState { get; init; }
|
public WorkspaceCreationState? creationState { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete("Workspaces no longer have creation state, is always created true")]
|
||||||
public sealed class WorkspaceCreationState
|
public sealed class WorkspaceCreationState
|
||||||
{
|
{
|
||||||
public bool completed { get; init; }
|
public bool completed { get; init; }
|
||||||
|
|||||||
@@ -264,15 +264,11 @@ public sealed class ActiveUserResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
logoUrl
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
readOnly
|
readOnly
|
||||||
description
|
description
|
||||||
creationState
|
|
||||||
{
|
|
||||||
completed
|
|
||||||
}
|
|
||||||
permissions {
|
permissions {
|
||||||
canCreateProject {
|
canCreateProject {
|
||||||
authorized
|
authorized
|
||||||
@@ -317,7 +313,7 @@ public sealed class ActiveUserResource
|
|||||||
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
||||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||||
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
private async Task<LimitedWorkspace?> GetActiveWorkspace_Legacy(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
//language=graphql
|
//language=graphql
|
||||||
const string QUERY = """
|
const string QUERY = """
|
||||||
@@ -328,7 +324,6 @@ public sealed class ActiveUserResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
|
||||||
description
|
description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +344,47 @@ public sealed class ActiveUserResource
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
//language=graphql
|
||||||
|
const string QUERY = """
|
||||||
|
query ActiveUser {
|
||||||
|
data:activeUser {
|
||||||
|
data:activeWorkspace {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
role
|
||||||
|
slug
|
||||||
|
logoUrl
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var request = new GraphQLRequest { Query = QUERY };
|
||||||
|
|
||||||
|
NullableResponse<NullableResponse<LimitedWorkspace?>?> response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await _client
|
||||||
|
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<LimitedWorkspace?>?>>(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (SpeckleGraphQLInvalidQueryException)
|
||||||
|
{
|
||||||
|
//v2.x.x servers do not have a logoUrl property
|
||||||
|
return await GetActiveWorkspace_Legacy(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data is null)
|
||||||
|
{
|
||||||
|
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
/// <param name="limit">Max number of projects to fetch</param>
|
/// <param name="limit">Max number of projects to fetch</param>
|
||||||
/// <param name="cursor">Optional cursor for pagination</param>
|
/// <param name="cursor">Optional cursor for pagination</param>
|
||||||
/// <param name="filter">Optional filter</param>
|
/// <param name="filter">Optional filter</param>
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ public sealed class OtherUserResource
|
|||||||
/// <param name="query">String to search for. Must be at least 3 characters</param>
|
/// <param name="query">String to search for. Must be at least 3 characters</param>
|
||||||
/// <param name="limit">Max number of users to fetch</param>
|
/// <param name="limit">Max number of users to fetch</param>
|
||||||
/// <param name="cursor">Optional cursor for pagination</param>
|
/// <param name="cursor">Optional cursor for pagination</param>
|
||||||
/// <param name="archived"></param>
|
|
||||||
/// <param name="emailOnly"></param>
|
/// <param name="emailOnly"></param>
|
||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -61,26 +60,25 @@ public sealed class OtherUserResource
|
|||||||
string query,
|
string query,
|
||||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||||
string? cursor = null,
|
string? cursor = null,
|
||||||
bool archived = false,
|
|
||||||
bool emailOnly = false,
|
bool emailOnly = false,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//language=graphql
|
//language=graphql
|
||||||
const string QUERY = """
|
const string QUERY = """
|
||||||
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) {
|
query Users($input: UsersRetrievalInput!) {
|
||||||
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) {
|
data:users(input: $input) {
|
||||||
cursor
|
cursor
|
||||||
items {
|
items {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
company
|
company
|
||||||
avatar
|
avatar
|
||||||
verified
|
verified
|
||||||
role
|
role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -89,11 +87,13 @@ public sealed class OtherUserResource
|
|||||||
Query = QUERY,
|
Query = QUERY,
|
||||||
Variables = new
|
Variables = new
|
||||||
{
|
{
|
||||||
query,
|
input = new
|
||||||
limit,
|
{
|
||||||
cursor,
|
query,
|
||||||
archived,
|
limit,
|
||||||
emailOnly,
|
emailOnly,
|
||||||
|
cursor,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public sealed class SubscriptionResource : IDisposable
|
|||||||
/// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary>
|
/// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary>
|
||||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||||
|
[Obsolete("Comments are now issues, and we've not update SDKs with the new subs")]
|
||||||
public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription(
|
public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription(
|
||||||
ViewerUpdateTrackingTarget target
|
ViewerUpdateTrackingTarget target
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ public sealed class WorkspaceResource
|
|||||||
name
|
name
|
||||||
role
|
role
|
||||||
slug
|
slug
|
||||||
logo
|
logoUrl
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
readOnly
|
readOnly
|
||||||
description
|
description
|
||||||
creationState
|
|
||||||
{
|
|
||||||
completed
|
|
||||||
}
|
|
||||||
permissions {
|
permissions {
|
||||||
canCreateProject {
|
canCreateProject {
|
||||||
authorized
|
authorized
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
namespace Speckle.Sdk.Logging;
|
using Speckle.Connectors.Logging;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Logging;
|
||||||
|
|
||||||
public sealed class NullActivityFactory : ISdkActivityFactory
|
public sealed class NullActivityFactory : ISdkActivityFactory
|
||||||
{
|
{
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|
||||||
public ISdkActivity? Start(string? name = default, string source = "") => null;
|
public ISdkActivity? Start(string? name, SdkActivityKind kind, string source) => null;
|
||||||
|
|
||||||
|
public ISdkActivity? StartRemote(string traceContext, SdkActivityKind kind, string? name, string source) => null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,32 +7,33 @@ namespace Speckle.Sdk.Models;
|
|||||||
public enum DynamicBaseMemberType
|
public enum DynamicBaseMemberType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed members of the DynamicBase object
|
/// The typed members of the <see cref="DynamicBase"/> object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Instance = 1,
|
Instance = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The dynamically added members of the DynamicBase object
|
/// The dynamically added members of the <see cref="DynamicBase"/> object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Dynamic = 2,
|
Dynamic = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed members flagged with ObsoleteAttribute attribute.
|
/// The typed members flagged with <see cref="ObsoleteAttribute"/> attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Obsolete = 4,
|
Obsolete = 4,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed methods flagged with TODO:
|
/// Old feature supported in v2 for grasshopper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Feature no longer supported")]
|
||||||
SchemaComputed = 16,
|
SchemaComputed = 16,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the typed members, including ones with ObsoleteAttribute attributes.
|
/// All the typed members, including ones with <see cref="ObsoleteAttribute"/> attributes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
InstanceAll = Instance + Obsolete,
|
InstanceAll = Instance + Obsolete,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the members, including dynamic and instance members flagged with ObsoleteAttribute attributes
|
/// All the members, including dynamic and instance members flagged with <see cref="ObsoleteAttribute"/> attributes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
All = InstanceAll + Dynamic,
|
All = InstanceAll + Dynamic,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,20 @@ public partial interface IIngestionProgressManager : IProgress<CardProgress>;
|
|||||||
/// An <see langword="IProgress{IngestionProgressEventArgs}"/> implementation for the entire client side Ingestion progress update reporting
|
/// An <see langword="IProgress{IngestionProgressEventArgs}"/> implementation for the entire client side Ingestion progress update reporting
|
||||||
/// Will throttles ingestion progress messages and reports their progress
|
/// Will throttles ingestion progress messages and reports their progress
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Normally we would pick quite a coarse updateInterval to try and spamming the server (1-5s)
|
||||||
|
/// </remarks>
|
||||||
[GenerateAutoInterface]
|
[GenerateAutoInterface]
|
||||||
public sealed class IngestionProgressManager(
|
public sealed class IngestionProgressManager(
|
||||||
ILogger<IngestionProgressManager> logger,
|
ILogger<IngestionProgressManager> logger,
|
||||||
IClient speckleClient,
|
IClient speckleClient,
|
||||||
ModelIngestion ingestion,
|
ModelIngestion ingestion,
|
||||||
string projectId,
|
|
||||||
TimeSpan updateInterval,
|
TimeSpan updateInterval,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
) : IIngestionProgressManager
|
) : IIngestionProgressManager
|
||||||
{
|
{
|
||||||
/// <remarks>
|
public Task? LastUpdate { get; private set; }
|
||||||
/// Normally we would pick quite a coarse throttle window to try and avoid over pressure (1-5s)
|
|
||||||
/// </remarks>
|
|
||||||
private Task? _lastUpdate;
|
|
||||||
private long _lastUpdatedAt;
|
private long _lastUpdatedAt;
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
|
||||||
@@ -48,15 +48,15 @@ public sealed class IngestionProgressManager(
|
|||||||
|
|
||||||
trimmedMessage = value.Status.TrimEnd('.');
|
trimmedMessage = value.Status.TrimEnd('.');
|
||||||
|
|
||||||
_lastUpdate = speckleClient
|
LastUpdate = speckleClient
|
||||||
.Ingestion.UpdateProgress(
|
.Ingestion.UpdateProgress(
|
||||||
new ModelIngestionUpdateInput(ingestion.id, projectId, trimmedMessage, value.Progress),
|
new ModelIngestionUpdateInput(ingestion.id, ingestion.projectId, trimmedMessage, value.Progress),
|
||||||
cancellationToken
|
cancellationToken
|
||||||
)
|
)
|
||||||
.ContinueWith(
|
.ContinueWith(
|
||||||
HandleFaultedContinuation,
|
Continuation,
|
||||||
CancellationToken.None,
|
CancellationToken.None,
|
||||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
|
TaskContinuationOptions.ExecuteSynchronously,
|
||||||
TaskScheduler.Default
|
TaskScheduler.Default
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ public sealed class IngestionProgressManager(
|
|||||||
/// <returns><see langword="true"/> if the update should be ignored, otherwise <see langword="false"/></returns>
|
/// <returns><see langword="true"/> if the update should be ignored, otherwise <see langword="false"/></returns>
|
||||||
private bool ShouldIgnoreProgressUpdate()
|
private bool ShouldIgnoreProgressUpdate()
|
||||||
{
|
{
|
||||||
if (_lastUpdate is not null && !_lastUpdate.IsCompleted)
|
if (LastUpdate is not null && !LastUpdate.IsCompleted)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ public sealed class IngestionProgressManager(
|
|||||||
return msSinceLastUpdate < updateInterval;
|
return msSinceLastUpdate < updateInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleFaultedContinuation(Task updateTask)
|
private void Continuation(Task updateTask)
|
||||||
{
|
{
|
||||||
// The progress report failed... could be many reasons.
|
// The progress report failed... could be many reasons.
|
||||||
// For now, we're not letting this fail the Ingestion in any way
|
// For now, we're not letting this fail the Ingestion in any way
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ public sealed class IngestionProgressManagerFactory(ILogger<IngestionProgressMan
|
|||||||
public IIngestionProgressManager CreateInstance(
|
public IIngestionProgressManager CreateInstance(
|
||||||
IClient speckleClient,
|
IClient speckleClient,
|
||||||
ModelIngestion ingestion,
|
ModelIngestion ingestion,
|
||||||
string projectId,
|
|
||||||
TimeSpan updateInterval,
|
TimeSpan updateInterval,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return new IngestionProgressManager(logger, speckleClient, ingestion, projectId, updateInterval, cancellationToken);
|
return new IngestionProgressManager(logger, speckleClient, ingestion, updateInterval, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ public sealed class RenderedStreamProgress(IProgress<CardProgress> progress) : I
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string[] s_suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
private static readonly string[] s_suffixes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
|
||||||
private static (string suffix, double scaleFactor) GetFileSizeRendering(long value)
|
internal static (string suffix, double scaleFactor) GetFileSizeRendering(long value)
|
||||||
{
|
{
|
||||||
if (value <= 0)
|
if (value <= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Speckle.InterfaceGenerator;
|
using Speckle.InterfaceGenerator;
|
||||||
using Speckle.Sdk.Dependencies;
|
using Speckle.Sdk.Dependencies;
|
||||||
using Speckle.Sdk.Helpers;
|
using Speckle.Sdk.Helpers;
|
||||||
|
using Speckle.Sdk.Logging;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Pipelines.Send;
|
namespace Speckle.Sdk.Pipelines.Send;
|
||||||
|
|
||||||
[GenerateAutoInterface]
|
[GenerateAutoInterface]
|
||||||
public sealed class DiskStoreFactory(ILogger<DiskStore> logger) : IDiskStoreFactory
|
public sealed class DiskStoreFactory(ILogger<DiskStore> logger, ISdkActivityFactory activityFactory) : IDiskStoreFactory
|
||||||
{
|
{
|
||||||
public DiskStore CreateInstance(CancellationToken cancellationToken) => new(logger, cancellationToken);
|
public DiskStore CreateInstance(CancellationToken cancellationToken) =>
|
||||||
|
new(logger, activityFactory, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DiskStore
|
public sealed class DiskStore
|
||||||
@@ -17,11 +19,17 @@ public sealed class DiskStore
|
|||||||
private readonly RepackedChannel<UploadItem> _channel;
|
private readonly RepackedChannel<UploadItem> _channel;
|
||||||
private readonly Task<DisposableFile> _writeToDiskTask;
|
private readonly Task<DisposableFile> _writeToDiskTask;
|
||||||
private readonly ILogger<DiskStore> _logger;
|
private readonly ILogger<DiskStore> _logger;
|
||||||
|
private readonly ISdkActivityFactory _activityFactory;
|
||||||
private readonly CancellationToken _cancellationToken;
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
|
||||||
internal DiskStore(ILogger<DiskStore> logger, CancellationToken cancellationToken)
|
internal DiskStore(
|
||||||
|
ILogger<DiskStore> logger,
|
||||||
|
ISdkActivityFactory activityFactory,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_activityFactory = activityFactory;
|
||||||
_cancellationToken = cancellationToken;
|
_cancellationToken = cancellationToken;
|
||||||
|
|
||||||
_channel = new RepackedChannel<UploadItem>(1000, true, false);
|
_channel = new RepackedChannel<UploadItem>(1000, true, false);
|
||||||
@@ -33,6 +41,7 @@ public sealed class DiskStore
|
|||||||
|
|
||||||
public async Task<DisposableFile> CompleteAsync()
|
public async Task<DisposableFile> CompleteAsync()
|
||||||
{
|
{
|
||||||
|
using var a = _activityFactory.Start("Waiting for DiskStore to complete");
|
||||||
_channel.CompleteWriter();
|
_channel.CompleteWriter();
|
||||||
return await _writeToDiskTask.ConfigureAwait(false);
|
return await _writeToDiskTask.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,11 +70,5 @@ public sealed class SendPipeline : IDisposable
|
|||||||
await _uploader.Send(fileStreamUpload).ConfigureAwait(false);
|
await _uploader.Send(fileStreamUpload).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> WaitForUploadAndServerProcessing()
|
|
||||||
{
|
|
||||||
// TODO: in some way, wait for the server to process the upload and return the actual new version id
|
|
||||||
return await Task.FromResult("todo").ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() => _uploader.Dispose();
|
public void Dispose() => _uploader.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Speckle.InterfaceGenerator;
|
using Speckle.InterfaceGenerator;
|
||||||
using Speckle.Newtonsoft.Json;
|
using Speckle.Newtonsoft.Json;
|
||||||
using Speckle.Sdk.Credentials;
|
using Speckle.Sdk.Credentials;
|
||||||
using Speckle.Sdk.Helpers;
|
using Speckle.Sdk.Helpers;
|
||||||
|
using Speckle.Sdk.Logging;
|
||||||
using Speckle.Sdk.Pipelines.Progress;
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
|
|
||||||
namespace Speckle.Sdk.Pipelines.Send;
|
namespace Speckle.Sdk.Pipelines.Send;
|
||||||
|
|
||||||
[GenerateAutoInterface]
|
[GenerateAutoInterface]
|
||||||
public sealed class UploaderFactory(ISpeckleHttp httpClientFactory, ILogger<Uploader> logger) : IUploaderFactory
|
public sealed class UploaderFactory(ISpeckleHttp httpClientFactory, ISdkActivityFactory activityFactory)
|
||||||
|
: IUploaderFactory
|
||||||
{
|
{
|
||||||
public Uploader CreateInstance(
|
public Uploader CreateInstance(
|
||||||
string projectId,
|
string projectId,
|
||||||
@@ -18,7 +19,7 @@ public sealed class UploaderFactory(ISpeckleHttp httpClientFactory, ILogger<Uplo
|
|||||||
Account account,
|
Account account,
|
||||||
IProgress<StreamProgressArgs> progress,
|
IProgress<StreamProgressArgs> progress,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
) => new(projectId, ingestionId, logger, httpClientFactory, account, progress, cancellationToken);
|
) => new(projectId, ingestionId, activityFactory, httpClientFactory, account, progress, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class Uploader : IDisposable
|
public sealed class Uploader : IDisposable
|
||||||
@@ -28,13 +29,13 @@ public sealed class Uploader : IDisposable
|
|||||||
private readonly CancellationToken _cancellationToken;
|
private readonly CancellationToken _cancellationToken;
|
||||||
private readonly HttpClient _speckleClient;
|
private readonly HttpClient _speckleClient;
|
||||||
private readonly HttpClient _s3Client;
|
private readonly HttpClient _s3Client;
|
||||||
private readonly ILogger<Uploader> _logger;
|
private readonly ISdkActivityFactory _activity;
|
||||||
private readonly IProgress<StreamProgressArgs> _progress;
|
private readonly IProgress<StreamProgressArgs> _progress;
|
||||||
|
|
||||||
internal Uploader(
|
internal Uploader(
|
||||||
string projectId,
|
string projectId,
|
||||||
string ingestionId,
|
string ingestionId,
|
||||||
ILogger<Uploader> logger,
|
ISdkActivityFactory activity,
|
||||||
ISpeckleHttp httpClientFactory,
|
ISpeckleHttp httpClientFactory,
|
||||||
Account speckleAccount,
|
Account speckleAccount,
|
||||||
IProgress<StreamProgressArgs> progress,
|
IProgress<StreamProgressArgs> progress,
|
||||||
@@ -43,7 +44,7 @@ public sealed class Uploader : IDisposable
|
|||||||
{
|
{
|
||||||
_projectId = projectId;
|
_projectId = projectId;
|
||||||
_ingestionId = ingestionId;
|
_ingestionId = ingestionId;
|
||||||
_logger = logger;
|
_activity = activity;
|
||||||
_cancellationToken = cancellationToken;
|
_cancellationToken = cancellationToken;
|
||||||
_progress = progress;
|
_progress = progress;
|
||||||
_speckleClient = httpClientFactory.CreateHttpClient(authorizationToken: speckleAccount.token);
|
_speckleClient = httpClientFactory.CreateHttpClient(authorizationToken: speckleAccount.token);
|
||||||
@@ -62,6 +63,8 @@ public sealed class Uploader : IDisposable
|
|||||||
|
|
||||||
private async Task<PresignedUploadResponse> GetPresignedUrl()
|
private async Task<PresignedUploadResponse> GetPresignedUrl()
|
||||||
{
|
{
|
||||||
|
using var a = _activity.Start("Get Presigned Url");
|
||||||
|
|
||||||
var signUri = new Uri($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/sign", UriKind.Relative);
|
var signUri = new Uri($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/sign", UriKind.Relative);
|
||||||
|
|
||||||
using var signResponse = await _speckleClient.PostAsync(signUri, null, _cancellationToken).ConfigureAwait(false);
|
using var signResponse = await _speckleClient.PostAsync(signUri, null, _cancellationToken).ConfigureAwait(false);
|
||||||
@@ -80,7 +83,7 @@ public sealed class Uploader : IDisposable
|
|||||||
|
|
||||||
private async Task<string> UploadToS3(Stream fileStream, PresignedUploadResponse presignedUploadResponse)
|
private async Task<string> UploadToS3(Stream fileStream, PresignedUploadResponse presignedUploadResponse)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Uploading file to pre-signed url");
|
using var a = _activity.Start("Uploading file to pre-signed url");
|
||||||
|
|
||||||
Stream progressStream = new ProgressStream(fileStream, _progress);
|
Stream progressStream = new ProgressStream(fileStream, _progress);
|
||||||
|
|
||||||
@@ -107,6 +110,8 @@ public sealed class Uploader : IDisposable
|
|||||||
|
|
||||||
private async Task TriggerProcessing(TriggerUploadRequest request)
|
private async Task TriggerProcessing(TriggerUploadRequest request)
|
||||||
{
|
{
|
||||||
|
using var a = _activity.Start("Triggering Processing");
|
||||||
|
|
||||||
Uri processUri = new($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/process", UriKind.Relative);
|
Uri processUri = new($"projects/{_projectId}/modelingestion/{_ingestionId}/uploads/process", UriKind.Relative);
|
||||||
string requestBody = JsonConvert.SerializeObject(request);
|
string requestBody = JsonConvert.SerializeObject(request);
|
||||||
using var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
|
using var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
var activityFactory = Create<ISdkActivityFactory>();
|
||||||
activityFactory.Setup(x => x.Start(null, "DownloadObjects")).Returns((ISdkActivity?)null);
|
activityFactory.Setup(x => x.Start(null, default, "DownloadObjects")).Returns((ISdkActivity?)null);
|
||||||
|
|
||||||
var serverObjectManager = new ServerObjectManager(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -91,7 +91,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
var activityFactory = Create<ISdkActivityFactory>();
|
||||||
activityFactory.Setup(x => x.Start(null, "DownloadSingleObject")).Returns((ISdkActivity?)null);
|
activityFactory.Setup(x => x.Start(null, default, "DownloadSingleObject")).Returns((ISdkActivity?)null);
|
||||||
|
|
||||||
var serverObjectManager = new ServerObjectManager(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -132,7 +132,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
var activityFactory = Create<ISdkActivityFactory>();
|
||||||
activityFactory.Setup(x => x.Start(null, "HasObjects")).Returns((ISdkActivity?)null);
|
activityFactory.Setup(x => x.Start(null, default, "HasObjects")).Returns((ISdkActivity?)null);
|
||||||
|
|
||||||
var serverObjectManager = new ServerObjectManager(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
@@ -171,7 +171,7 @@ public class ServerObjectManagerTests : MoqTest
|
|||||||
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
|
||||||
|
|
||||||
var activityFactory = Create<ISdkActivityFactory>();
|
var activityFactory = Create<ISdkActivityFactory>();
|
||||||
activityFactory.Setup(x => x.Start(null, "UploadObjects")).Returns((ISdkActivity?)null);
|
activityFactory.Setup(x => x.Start(null, default, "UploadObjects")).Returns((ISdkActivity?)null);
|
||||||
|
|
||||||
var serverObjectManager = new ServerObjectManager(
|
var serverObjectManager = new ServerObjectManager(
|
||||||
http.Object,
|
http.Object,
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ public class SubscriptionResourceTests : IAsyncLifetime
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
|
private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
|
||||||
#else
|
#else
|
||||||
private const int WAIT_PERIOD = 400; // For CI runs, a much smaller wait time is acceptable
|
private const int WAIT_PERIOD = 600; // For CI runs, a much smaller wait time is acceptable
|
||||||
#endif
|
#endif
|
||||||
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 600;
|
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 600;
|
||||||
private IClient _testUser;
|
private IClient _testUser;
|
||||||
|
|||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Type": "AggregateException",
|
|
||||||
"InnerException": {
|
|
||||||
"Data": {},
|
|
||||||
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
|
|
||||||
"Type": "SpeckleGraphQLForbiddenException"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Type": "AggregateException",
|
|
||||||
"InnerException": {
|
|
||||||
"Data": {},
|
|
||||||
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
|
|
||||||
"Type": "SpeckleGraphQLForbiddenException"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,17 +21,19 @@ public class WorkspaceResourceTests
|
|||||||
return testUser;
|
return testUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact, Trait("Server", "Internal")]
|
||||||
public async Task TestGetWorkspace()
|
public async Task TestGetWorkspace()
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
|
||||||
await Verify(ex);
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestGetProjects()
|
public async Task TestGetProjects()
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
|
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
|
||||||
await Verify(ex);
|
Assert.Single(ex.InnerExceptions);
|
||||||
|
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Speckle.Sdk.Api;
|
||||||
|
using Speckle.Sdk.Api.GraphQL.Models;
|
||||||
|
using Speckle.Sdk.Common;
|
||||||
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Integration.Pipelines.Progress;
|
||||||
|
|
||||||
|
[Trait("Server", "Internal")]
|
||||||
|
public class IngestionProgressManagerTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private IIngestionProgressManagerFactory _factory;
|
||||||
|
private IClient _client;
|
||||||
|
private Project _project;
|
||||||
|
private Model _model;
|
||||||
|
private ModelIngestion _ingestion;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||||
|
_factory = serviceProvider.GetRequiredService<IIngestionProgressManagerFactory>();
|
||||||
|
|
||||||
|
_client = await Fixtures.SeedUserWithClient();
|
||||||
|
_project = await _client.Project.Create(new("test", null, default));
|
||||||
|
_model = await _client.Model.Create(new("test", null, _project.id));
|
||||||
|
_ingestion = await _client.Ingestion.Create(
|
||||||
|
new(_model.id, _project.id, "Testing ingestion", new("integrationTests", "0.0.0", null, null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TestProgress_NoThrottle()
|
||||||
|
{
|
||||||
|
var sut = _factory.CreateInstance(_client, _ingestion, TimeSpan.Zero, CancellationToken.None);
|
||||||
|
const string FIRST_MESSAGE = "This is a test 123";
|
||||||
|
const string SECOND_MESSAGE = "This is another test 321";
|
||||||
|
|
||||||
|
// first message (should go through)
|
||||||
|
sut.Report(new CardProgress(FIRST_MESSAGE, 0.123123123d));
|
||||||
|
await sut.LastUpdate.NotNull();
|
||||||
|
var res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(FIRST_MESSAGE, res.statusData.progressMessage);
|
||||||
|
|
||||||
|
// second message (should also go through)
|
||||||
|
sut.Report(new CardProgress(SECOND_MESSAGE, 0.321321321d));
|
||||||
|
await sut.LastUpdate.NotNull();
|
||||||
|
res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(SECOND_MESSAGE, res.statusData.progressMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TestProgress_WithThrottle()
|
||||||
|
{
|
||||||
|
var sut = _factory.CreateInstance(_client, _ingestion, TimeSpan.FromMilliseconds(500), CancellationToken.None);
|
||||||
|
const string EXPECTED_MESSAGE = "First message should go through 123";
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(600));
|
||||||
|
|
||||||
|
// first message (should go through)
|
||||||
|
sut.Report(new CardProgress(EXPECTED_MESSAGE, 0.123123123d));
|
||||||
|
// second message (should be dropped)
|
||||||
|
sut.Report(new CardProgress("Second message, should be dropped", 0.321321321d));
|
||||||
|
await sut.LastUpdate.NotNull();
|
||||||
|
var res = await _client.Ingestion.Get(_ingestion.id, _project.id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(EXPECTED_MESSAGE, res.statusData.progressMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,4 +16,7 @@
|
|||||||
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||||
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Pipelines\Send\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Moq;
|
||||||
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Unit.Pipelines.Progress;
|
||||||
|
|
||||||
|
public class AggregateProgressTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Report_InvokesReportOnAllInnerProgresses()
|
||||||
|
{
|
||||||
|
var mock1 = new Mock<IProgress<int>>();
|
||||||
|
var mock2 = new Mock<IProgress<int>>();
|
||||||
|
const int TEST_VALUE = 42;
|
||||||
|
var target = new AggregateProgress<int>(mock1.Object, mock2.Object);
|
||||||
|
|
||||||
|
target.Report(TEST_VALUE);
|
||||||
|
|
||||||
|
mock1.Verify(x => x.Report(TEST_VALUE), Times.Once);
|
||||||
|
mock2.Verify(x => x.Report(TEST_VALUE), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moq;
|
||||||
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Unit.Pipelines.Progress;
|
||||||
|
|
||||||
|
[SuppressMessage(
|
||||||
|
"Performance",
|
||||||
|
"CA1835:Prefer the \'Memory\'-based overloads for \'ReadAsync\' and \'WriteAsync\'",
|
||||||
|
Justification = "Need to test it"
|
||||||
|
)]
|
||||||
|
public class ProgressStreamTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Mock<Stream> _innerStreamMock;
|
||||||
|
private readonly Mock<IProgress<StreamProgressArgs>> _progressMock;
|
||||||
|
private readonly ProgressStream _sut;
|
||||||
|
|
||||||
|
public ProgressStreamTests()
|
||||||
|
{
|
||||||
|
// Setup the mocks
|
||||||
|
_innerStreamMock = new Mock<Stream>();
|
||||||
|
_innerStreamMock.Setup(s => s.Length).Returns(1024L);
|
||||||
|
|
||||||
|
_progressMock = new Mock<IProgress<StreamProgressArgs>>();
|
||||||
|
|
||||||
|
// Inject mocks into the System Under Test
|
||||||
|
_sut = new ProgressStream(_innerStreamMock.Object, _progressMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAsync_Should_CallInnerStreamAndReportProgress()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var buffer = new byte[10];
|
||||||
|
_innerStreamMock
|
||||||
|
.Setup(s => s.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None))
|
||||||
|
.Returns(Task.FromResult(5));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ReadAsync(buffer, 0, buffer.Length);
|
||||||
|
|
||||||
|
// Assert - Inner Stream Read was called
|
||||||
|
_innerStreamMock.Verify(s => s.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None), Times.Once);
|
||||||
|
|
||||||
|
// Assert - Progress Report was called with the correct byte count
|
||||||
|
_progressMock.Verify(p => p.Report(It.IsAny<StreamProgressArgs>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAsync_Should_CallInnerStreamAndReportProgress()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var buffer = new byte[10];
|
||||||
|
_innerStreamMock
|
||||||
|
.Setup(s => s.WriteAsync(buffer, 0, buffer.Length, CancellationToken.None))
|
||||||
|
.Returns(Task.FromResult(5));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.WriteAsync(buffer, 0, buffer.Length);
|
||||||
|
|
||||||
|
// Assert - Inner Stream Write was called
|
||||||
|
_innerStreamMock.Verify(s => s.WriteAsync(buffer, 0, buffer.Length, CancellationToken.None), Times.Once);
|
||||||
|
|
||||||
|
// Assert - Progress Report was called with the correct byte count
|
||||||
|
_progressMock.Verify(p => p.Report(It.IsAny<StreamProgressArgs>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_sut.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Speckle.Sdk.Pipelines.Progress;
|
||||||
|
|
||||||
|
namespace Speckle.Sdk.Tests.Unit.Pipelines.Progress;
|
||||||
|
|
||||||
|
public class RenderedStreamProgressTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, "B", 1.0)]
|
||||||
|
[InlineData(1024, "B", 1.0)]
|
||||||
|
[InlineData(1024 + 1, "KB", 1.0 / 1024)]
|
||||||
|
[InlineData(1024 * 1024, "KB", 1.0 / 1024)]
|
||||||
|
[InlineData(1024 * 1024 + 1, "MB", 1.0 / (1024 * 1024))]
|
||||||
|
[InlineData(1024 * 1024 * 1024, "MB", 1.0 / (1024 * 1024))]
|
||||||
|
[InlineData(1024 * 1024 * 1024 + 1, "GB", 1.0 / (1024 * 1024 * 1024))]
|
||||||
|
[InlineData(1024L * 1024L * 1024L * 1024L, "GB", 1.0 / (1024L * 1024L * 1024L))]
|
||||||
|
public void GetFileSizeRendering_WithPositiveValue_ReturnsCorrectSuffix(
|
||||||
|
long value,
|
||||||
|
string expectedSuffix,
|
||||||
|
double expectedScaleFactor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var result = RenderedStreamProgress.GetFileSizeRendering(value);
|
||||||
|
|
||||||
|
Assert.Equal(expectedSuffix, result.suffix);
|
||||||
|
Assert.Equal(expectedScaleFactor, result.scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(-1)]
|
||||||
|
[InlineData(-1000)]
|
||||||
|
public void GetFileSizeRendering_WithNonPositiveValue_ReturnsBytesSuffix(long value)
|
||||||
|
{
|
||||||
|
var result = RenderedStreamProgress.GetFileSizeRendering(value);
|
||||||
|
|
||||||
|
Assert.Equal("B", result.suffix);
|
||||||
|
Assert.Equal(1d, result.scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(long.MaxValue)]
|
||||||
|
public void GetFileSizeRendering_WithVeryLargeValue_ThrowsArgumentOutOfRangeException(long value)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => RenderedStreamProgress.GetFileSizeRendering(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user