diff --git a/src/Options/TransferOptions.cs b/src/Options/TransferOptions.cs index c1c1d419e..a8fbe9fdb 100644 --- a/src/Options/TransferOptions.cs +++ b/src/Options/TransferOptions.cs @@ -20,6 +20,7 @@ namespace Soulseek using System; using System.Threading; using System.Threading.Tasks; + using Soulseek.Diagnostics; /// /// Options for transfer operations. @@ -47,6 +48,9 @@ public class TransferOptions /// The delegate, accepting the number of bytes attempted, granted, and transferred for each chunk, used to report /// transfer statistics. /// + /// + /// The delegate to invoke when a diagnostic message for the transfer is captured. + /// /// /// The maximum linger time, in milliseconds, that a connection will attempt to cleanly close following a transfer. /// @@ -66,6 +70,7 @@ public TransferOptions( Func slotAwaiter = null, Action slotReleased = null, Action reporter = null, + Action<(DateTime timestamp, DiagnosticLevel Level, string Message, Exception exception)> diagnostic = null, int maximumLingerTime = 3000, bool seekInputStreamAutomatically = true, bool disposeInputStreamOnCompletion = true, @@ -78,12 +83,18 @@ public TransferOptions( SlotAwaiter = slotAwaiter ?? defaultSlotAwaiter; SlotReleased = slotReleased; Reporter = reporter; + Diagnostic = diagnostic; StateChanged = stateChanged; ProgressUpdated = progressUpdated; MaximumLingerTime = maximumLingerTime; } + /// + /// Gets the delegate to invoke when a diagnostic message for the transfer is captured. + /// + public Action<(DateTime Timestamp, DiagnosticLevel Level, string Message, Exception Exception)> Diagnostic { get; } + /// /// Gets a value indicating whether input streams should be closed upon transfer completion. (Default = false). /// @@ -156,6 +167,7 @@ public TransferOptions WithAdditionalStateChanged(Action<(TransferStates Previou slotAwaiter: SlotAwaiter, slotReleased: SlotReleased, reporter: Reporter, + diagnostic: Diagnostic, maximumLingerTime: MaximumLingerTime, seekInputStreamAutomatically: SeekInputStreamAutomatically, disposeInputStreamOnCompletion: DisposeInputStreamOnCompletion, @@ -183,6 +195,7 @@ public TransferOptions WithDisposalOptions( slotAwaiter: SlotAwaiter, slotReleased: SlotReleased, reporter: Reporter, + diagnostic: Diagnostic, maximumLingerTime: MaximumLingerTime, seekInputStreamAutomatically: SeekInputStreamAutomatically, disposeInputStreamOnCompletion: disposeInputStreamOnCompletion ?? DisposeInputStreamOnCompletion, diff --git a/src/SoulseekClient.cs b/src/SoulseekClient.cs index 54a9ed7ff..d40e98ab9 100644 --- a/src/SoulseekClient.cs +++ b/src/SoulseekClient.cs @@ -3126,6 +3126,26 @@ private async Task DownloadToStreamAsync(string username, string remot Size = size, }; + void Info(string message) + { + Diagnostic.Info(message); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Info, message, null)); + } + + void Debug(string message, Exception exception = null) + { + Diagnostic.Debug(message, exception); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Debug, message, exception)); + } + + void Warning(string message, Exception exception = null) + { + Diagnostic.Warning(message, exception); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Warning, message, exception)); + } + + Debug($"Download of file {Path.GetFileName(remoteFilename)} from {username} initializing (token: {token})"); + // we can't allow more than one concurrent transfer for the same file from the same user. we're already checking for this // in the public-scoped methods, by checking the contents of the Download/UploadDictionary, but that's not thread safe; // a caller can spam calls and get downloads through concurrently. this check is the last line of defense; if we make @@ -3147,6 +3167,8 @@ private async Task DownloadToStreamAsync(string username, string remot throw new DuplicateTransferException($"Duplicate download of {remoteFilename} from {username} aborted"); } + Debug($"Download of file {Path.GetFileName(remoteFilename)} from {username} cleared duplicate checks"); + var lastState = TransferStates.None; void UpdateState(TransferStates state) @@ -3179,13 +3201,14 @@ void UpdateProgress(long bytesDownloaded) // acquire the global download semaphore to ensure we aren't trying to process more than the total allotted // concurrent downloads globally. if we hit this limit, downloads will stack up behind it and will be processed in // a first-in-first-out manner. + Debug($"Awaiting global download semaphore for file {Path.GetFileName(download.Filename)} to {username}"); await GlobalDownloadSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); globalSemaphoreAcquired = true; - Diagnostic.Debug($"Global download semaphore for file {Path.GetFileName(download.Filename)} to {username} acquired"); + Debug($"Global download semaphore for file {Path.GetFileName(download.Filename)} to {username} acquired"); var endpoint = await GetUserEndPointAsync(username, cancellationToken).ConfigureAwait(false); var peerConnection = await PeerConnectionManager.GetOrAddMessageConnectionAsync(username, endpoint, cancellationToken).ConfigureAwait(false); - Diagnostic.Debug($"Fetched peer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {peerConnection.Id}, state: {peerConnection.State})"); + Debug($"Fetched peer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {peerConnection.Id}, state: {peerConnection.State})"); // prepare two waits; one for the transfer response to confirm that our request is acknowledged and another for // the eventual transfer request sent when the peer is ready to send the file. the response message should be @@ -3196,12 +3219,13 @@ void UpdateProgress(long bytesDownloaded) // request the file await peerConnection.WriteAsync(new TransferRequest(TransferDirection.Download, token, remoteFilename), cancellationToken).ConfigureAwait(false); - Diagnostic.Debug($"Wrote transfer request for download of {Path.GetFileName(download.Filename)} from {username} (id: {peerConnection.Id}, state: {peerConnection.State})"); + Debug($"Wrote transfer request for download of {Path.GetFileName(download.Filename)} from {username} (token: {token})"); UpdateState(TransferStates.Requested); + Debug($"Awaiting transfer request ACK for download of {Path.GetFileName(download.Filename)} from {username} (token: {token})"); var transferRequestAcknowledgement = await transferRequestAcknowledged.ConfigureAwait(false); - Diagnostic.Debug($"Received transfer request ACK for download of {Path.GetFileName(download.Filename)} from {username}: allowed: {transferRequestAcknowledgement.IsAllowed}, message: {transferRequestAcknowledgement.Message} (token: {token})"); + Debug($"Received transfer request ACK for download of {Path.GetFileName(download.Filename)} from {username}: allowed: {transferRequestAcknowledgement.IsAllowed}, message: {transferRequestAcknowledgement.Message} (token: {token})"); if (transferRequestAcknowledgement.IsAllowed) { @@ -3225,7 +3249,7 @@ void UpdateProgress(long bytesDownloaded) download.Connection = await PeerConnectionManager .GetTransferConnectionAsync(username, endpoint, transferRequestAcknowledgement.Token, cancellationToken) .ConfigureAwait(false); - Diagnostic.Debug($"Fetched transfer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {download.Connection.Id}, state: {download.Connection.State})"); + Debug($"Fetched transfer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {download.Connection.Id}, state: {download.Connection.State})"); } else if (!string.Equals(transferRequestAcknowledgement.Message.TrimEnd('.'), "Queued", StringComparison.OrdinalIgnoreCase)) { @@ -3237,7 +3261,9 @@ void UpdateProgress(long bytesDownloaded) UpdateState(TransferStates.Queued | TransferStates.Remotely); // wait for the peer to respond that they are ready to start the transfer + Debug($"Awaiting transfer start request for download of {Path.GetFileName(download.Filename)} from {username}"); var transferStartRequest = await transferStartRequested.ConfigureAwait(false); + Debug($"Received transfer start request for download of {Path.GetFileName(download.Filename)} from {username}"); // the size of the remote file may have changed since it was sent in a search or browse response if (download.Size.HasValue && download.Size.Value != transferStartRequest.FileSize) @@ -3257,7 +3283,7 @@ void UpdateProgress(long bytesDownloaded) peerConnection = await PeerConnectionManager .GetOrAddMessageConnectionAsync(username, endpoint, cancellationToken) .ConfigureAwait(false); - Diagnostic.Debug($"Fetched peer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {peerConnection.Id}, state: {peerConnection.State})"); + Debug($"Fetched peer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {peerConnection.Id}, state: {peerConnection.State})"); // prepare a wait for the eventual transfer connection var connectionTask = PeerConnectionManager @@ -3265,11 +3291,13 @@ void UpdateProgress(long bytesDownloaded) // initiate the connection await peerConnection.WriteAsync(new TransferResponse(download.RemoteToken.Value, download.Size ?? 0), cancellationToken).ConfigureAwait(false); + Debug($"Wrote transfer response for download of {Path.GetFileName(download.Filename)} from {username} (remote token: {download.RemoteToken.Value})"); try { + Debug($"Awaiting transfer connection for download of {Path.GetFileName(download.Filename)} from {username}"); download.Connection = await connectionTask.ConfigureAwait(false); - Diagnostic.Debug($"Fetched transfer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {download.Connection.Id}, state: {download.Connection.State})"); + Debug($"Fetched transfer connection for download of {Path.GetFileName(download.Filename)} from {username} (id: {download.Connection.Id}, state: {download.Connection.State})"); } catch (ConnectionException) { @@ -4162,6 +4190,26 @@ private async Task UploadFromStreamAsync(string username, string remot Size = size, }; + void Info(string message) + { + Diagnostic.Info(message); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Info, message, null)); + } + + void Debug(string message, Exception exception = null) + { + Diagnostic.Debug(message, exception); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Debug, message, exception)); + } + + void Warning(string message, Exception exception = null) + { + Diagnostic.Warning(message, exception); + options.Diagnostic?.Invoke((DateTime.UtcNow, DiagnosticLevel.Warning, message, exception)); + } + + Debug($"Upload of file {Path.GetFileName(remoteFilename)} to {username} initializing (token: {token})"); + // we can't allow more than one concurrent transfer for the same file from the same user. we're already checking for this // in the public-scoped methods, by checking the contents of the Download/UploadDictionary, but that's not thread safe; // a caller can spam calls and get transfers through concurrently. this check is the last line of defense; if we make @@ -4183,6 +4231,8 @@ private async Task UploadFromStreamAsync(string username, string remot throw new DuplicateTransferException($"Duplicate upload of {remoteFilename} to {username} aborted"); } + Debug($"Upload of file {Path.GetFileName(remoteFilename)} to {username} cleared duplicate checks"); + var lastState = TransferStates.None; void UpdateState(TransferStates state) @@ -4215,6 +4265,8 @@ void UpdateProgress(long bytesUploaded) try { + Debug($"Awaiting upload sync root for file {Path.GetFileName(upload.Filename)} to {username}"); + await UploadSemaphoreSyncRoot.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -4234,16 +4286,17 @@ void UpdateProgress(long bytesUploaded) // permissive stage 1: acquire the per-user semaphore to ensure we aren't trying to process more than the allotted // concurrent uploads to this user, and ensure that we aren't trying to acquire a slot for an upload until the // requesting user is ready to receive it + Debug($"Awaiting upload semaphore for file {Path.GetFileName(upload.Filename)} to {username}"); await semaphoreWaitTask.ConfigureAwait(false); semaphoreAcquired = true; - Diagnostic.Debug($"Upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} acquired"); + Debug($"Upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} acquired"); // permissive stage 2: acquire an upload slot from the calling code try { await options.SlotAwaiter(new Transfer(upload), cancellationToken).ConfigureAwait(false); uploadSlotAcquired = true; - Diagnostic.Debug($"Upload slot for file {Path.GetFileName(upload.Filename)} to {username} acquired"); + Debug($"Upload slot for file {Path.GetFileName(upload.Filename)} to {username} acquired"); } catch (Exception ex) when (!(ex is OperationCanceledException)) { @@ -4256,14 +4309,14 @@ void UpdateProgress(long bytesUploaded) // by providing an implementation of AcquireSlot() that won't exceed the maximum concurrent upload limit await GlobalUploadSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); globalSemaphoreAcquired = true; - Diagnostic.Debug($"Global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} acquired"); + Debug($"Global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} acquired"); // all permissives have been given fetch the user endpoint and request that the transfer begins endpoint = await GetUserEndPointAsync(username, cancellationToken).ConfigureAwait(false); var messageConnection = await PeerConnectionManager .GetOrAddMessageConnectionAsync(username, endpoint, cancellationToken) .ConfigureAwait(false); - Diagnostic.Debug($"Fetched peer connection for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {messageConnection.Id}, state: {messageConnection.State})"); + Debug($"Fetched peer connection for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {messageConnection.Id}, state: {messageConnection.State})"); // prepare a wait for the transfer response var transferRequestAcknowledged = Waiter.Wait( @@ -4273,12 +4326,12 @@ void UpdateProgress(long bytesUploaded) var transferRequest = new TransferRequest(TransferDirection.Upload, upload.Token, upload.Filename, size); await messageConnection.WriteAsync(transferRequest, cancellationToken).ConfigureAwait(false); - Diagnostic.Debug($"Wrote transfer request for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {messageConnection.Id}, state: {messageConnection.State})"); + Debug($"Wrote transfer request for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {messageConnection.Id}, state: {messageConnection.State})"); UpdateState(TransferStates.Requested); var transferRequestAcknowledgement = await transferRequestAcknowledged.ConfigureAwait(false); - Diagnostic.Debug($"Received transfer request ACK for upload of {Path.GetFileName(upload.Filename)} to {username}: allowed: {transferRequestAcknowledgement.IsAllowed}, message: {transferRequestAcknowledgement.Message} (token: {token})"); + Debug($"Received transfer request ACK for upload of {Path.GetFileName(upload.Filename)} to {username}: allowed: {transferRequestAcknowledgement.IsAllowed}, message: {transferRequestAcknowledgement.Message} (token: {token})"); if (!transferRequestAcknowledgement.IsAllowed) { @@ -4290,7 +4343,7 @@ void UpdateProgress(long bytesUploaded) upload.Connection = await PeerConnectionManager .GetTransferConnectionAsync(upload.Username, endpoint, upload.Token, cancellationToken) .ConfigureAwait(false); - Diagnostic.Debug($"Fetched transfer connection for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {upload.Connection.Id}, state: {upload.Connection.State})"); + Debug($"Fetched transfer connection for upload of {Path.GetFileName(upload.Filename)} to {username} (id: {upload.Connection.Id}, state: {upload.Connection.State})"); // create a task completion source that represents the disconnect of the transfer connection. this is one of two tasks that will 'race' // to determine the outcome of the upload. @@ -4321,7 +4374,7 @@ void UpdateProgress(long bytesUploaded) } catch (Exception ex) when (ex is not OperationCanceledException && ex is not TimeoutException) { - Diagnostic.Debug($"Failed to read start offset for upload of {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}"); + Debug($"Failed to read start offset for upload of {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}"); throw new MessageReadException($"Failed to read transfer start offset: {ex.Message}", ex); } @@ -4330,7 +4383,7 @@ void UpdateProgress(long bytesUploaded) throw new TransferException($"Requested start offset of {upload.StartOffset} bytes exceeds file length of {upload.Size} bytes"); } - Diagnostic.Debug($"Resolving input stream for upload of {Path.GetFileName(upload.Filename)} to {username}"); + Debug($"Resolving input stream for upload of {Path.GetFileName(upload.Filename)} to {username}"); inputStream = await inputStreamFactory(upload.StartOffset).ConfigureAwait(false); if (upload.StartOffset > 0 && options.SeekInputStreamAutomatically) @@ -4340,7 +4393,7 @@ void UpdateProgress(long bytesUploaded) throw new TransferException($"Requested non-zero start offset but input stream does not support seeking"); } - Diagnostic.Debug($"Seeking upload of {Path.GetFileName(upload.Filename)} to {username} to starting offset of {upload.StartOffset} bytes"); + Debug($"Seeking upload of {Path.GetFileName(upload.Filename)} to {username} to starting offset of {upload.StartOffset} bytes"); inputStream.Seek(upload.StartOffset, SeekOrigin.Begin); } @@ -4417,7 +4470,7 @@ void UpdateProgress(long bytesUploaded) UpdateProgress(inputStream?.Position ?? 0); UpdateState(TransferStates.Completed | TransferStates.Succeeded); - Diagnostic.Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} complete ({inputStream.Position} of {upload.Size} bytes)."); + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} complete ({inputStream.Position} of {upload.Size} bytes)."); return new Transfer(upload); } @@ -4426,6 +4479,8 @@ void UpdateProgress(long bytesUploaded) upload.Exception = ex; UpdateState(TransferStates.Completed | TransferStates.Rejected); + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} failed: Rejected: {ex.Message} (state: {upload.State})"); + throw; } catch (OperationCanceledException ex) @@ -4436,7 +4491,8 @@ void UpdateProgress(long bytesUploaded) UpdateProgress(inputStream?.Position ?? 0); UpdateState(TransferStates.Completed | TransferStates.Cancelled); - Diagnostic.Debug(ex.ToString()); + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} cancelled: {ex.Message} (state: {upload.State})"); + Debug(ex.ToString()); // cancelled async operations can throw TaskCanceledException, which is a subclass of OperationCanceledException, // but we want to be deterministic, so wrap and re-throw them. @@ -4450,7 +4506,8 @@ void UpdateProgress(long bytesUploaded) UpdateProgress(inputStream?.Position ?? 0); UpdateState(TransferStates.Completed | TransferStates.TimedOut); - Diagnostic.Debug(ex.ToString()); + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} timed out: {ex.Message} (state: {upload.State})"); + Debug(ex.ToString()); throw; } @@ -4462,17 +4519,21 @@ void UpdateProgress(long bytesUploaded) UpdateProgress(inputStream?.Position ?? 0); UpdateState(TransferStates.Completed | TransferStates.Errored); - Diagnostic.Debug(ex.ToString()); + Debug(ex.ToString()); if (ex is UserOfflineException) { + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} failed: {ex.Message} (state: {upload.State})"); throw; } + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} failed: {ex.Message} (state: {upload.State})"); throw new SoulseekClientException($"Failed to upload file {remoteFilename} to user {username}: {ex.Message}", ex); } finally { + Debug($"Upload of {Path.GetFileName(upload.Filename)} to {username} finalizing (state: {upload.State})"); + /* do our best to clean up, in descending order of importance. this stuff is all 'nice to have' but shouldn't leave the client in an inoperable state if it fails; more like we may leak resource handles over time if @@ -4486,7 +4547,7 @@ void UpdateProgress(long bytesUploaded) } catch (Exception ex) { - Diagnostic.Warning($"Failed to dispose transfer connection for file {remoteFilename} to user {username}: {ex.Message}"); + Warning($"Failed to dispose transfer connection for file {remoteFilename} to user {username}: {ex.Message}"); } long finalStreamPosition = 0; @@ -4500,7 +4561,7 @@ void UpdateProgress(long bytesUploaded) } catch (Exception ex) { - Diagnostic.Warning($"Failed to determine final position of input stream for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); + Warning($"Failed to determine final position of input stream for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); } if (options.DisposeInputStreamOnCompletion && inputStream != null) @@ -4515,7 +4576,7 @@ void UpdateProgress(long bytesUploaded) } catch (Exception ex) { - Diagnostic.Warning($"Failed to finalize input stream for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); + Warning($"Failed to finalize input stream for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); } } @@ -4559,7 +4620,7 @@ client in an inoperable state over time { try { - Diagnostic.Debug($"Upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} released"); + Debug($"Upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} released"); semaphore.Release(releaseCount: 1); } catch (Exception ex) @@ -4577,13 +4638,13 @@ client in an inoperable state over time // plenty of time, as this release and the subsequent thread acquiring it should happen within nanoseconds. await Task.Delay(10, CancellationToken.None).ConfigureAwait(false); - Diagnostic.Debug($"Upload slot for file {Path.GetFileName(upload.Filename)} to {username} released"); + Debug($"Upload slot for file {Path.GetFileName(upload.Filename)} to {username} released"); options.SlotReleased?.Invoke(new Transfer(upload)); } catch (Exception ex) { - Diagnostic.Warning($"Encountered Exception releasing upload slot for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); + Warning($"Encountered Exception releasing upload slot for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}", ex); } } @@ -4592,16 +4653,18 @@ client in an inoperable state over time try { GlobalUploadSemaphore.Release(releaseCount: 1); - Diagnostic.Debug($"Global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} released"); + Debug($"Global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username} released"); } catch (Exception ex) { - Diagnostic.Warning($"Failed to release global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}"); + Warning($"Failed to release global upload semaphore for file {Path.GetFileName(upload.Filename)} to {username}: {ex.Message}"); } } UploadDictionary.TryRemove(upload.Token, out _); UniqueKeyDictionary.TryRemove(uniqueKey, out _); + + Info($"Upload of {Path.GetFileName(upload.Filename)} to {username} finalized (final state: {upload.State})"); } } } diff --git a/tests/Soulseek.Tests.Unit/Options/TransferOptionsTests.cs b/tests/Soulseek.Tests.Unit/Options/TransferOptionsTests.cs index b39cd8abd..a5761967b 100644 --- a/tests/Soulseek.Tests.Unit/Options/TransferOptionsTests.cs +++ b/tests/Soulseek.Tests.Unit/Options/TransferOptionsTests.cs @@ -21,6 +21,8 @@ namespace Soulseek.Tests.Unit.Options using System.Threading; using System.Threading.Tasks; using AutoFixture.Xunit2; + using Soulseek.Diagnostics; + using Xunit; public class TransferOptionsTests @@ -37,6 +39,7 @@ public void Instantiates_Given_Data( Action<(long PreviousBytesTransferred, Transfer Transfer)> progressUpdated, Func acquireSlot, Action reporter, + Action<(DateTime timestamp, DiagnosticLevel Level, string Message, Exception exception)> diagnostic, Action slotReleased) { var o = new TransferOptions( @@ -46,6 +49,7 @@ public void Instantiates_Given_Data( acquireSlot, slotReleased, reporter, + diagnostic, maximumLingerTime, seekInput, disposeInput, @@ -61,6 +65,7 @@ public void Instantiates_Given_Data( Assert.Equal(acquireSlot, o.SlotAwaiter); Assert.Equal(slotReleased, o.SlotReleased); Assert.Equal(reporter, o.Reporter); + Assert.Equal(diagnostic, o.Diagnostic); } [Trait("Category", "Instantiation")] @@ -81,6 +86,8 @@ public async Task Instantiates_With_Defaults() Assert.Null(o.StateChanged); Assert.Null(o.ProgressUpdated); Assert.Null(o.SlotReleased); + Assert.Null(o.Reporter); + Assert.Null(o.Diagnostic); } [Trait("Category", "WithAdditionalStateChanged")] @@ -94,7 +101,8 @@ public void WithAdditionalStateChanged_Returns_Copy_Other_Than_StateChanged( int maximumLingerTime, Action<(long PreviousBytesTransferred, Transfer Transfer)> progressUpdated, Func acquireSlot, - Action slotReleased) + Action slotReleased, + Action<(DateTime timestamp, DiagnosticLevel Level, string Message, Exception exception)> diagnostic) { var n = new TransferOptions( governor: governor, @@ -102,6 +110,7 @@ public void WithAdditionalStateChanged_Returns_Copy_Other_Than_StateChanged( progressUpdated: progressUpdated, slotAwaiter: acquireSlot, slotReleased: slotReleased, + diagnostic: diagnostic, maximumLingerTime: maximumLingerTime, seekInputStreamAutomatically: seekInput, disposeInputStreamOnCompletion: disposeInput, @@ -117,6 +126,7 @@ public void WithAdditionalStateChanged_Returns_Copy_Other_Than_StateChanged( Assert.Equal(maximumLingerTime, o.MaximumLingerTime); Assert.Equal(acquireSlot, o.SlotAwaiter); Assert.Equal(slotReleased, o.SlotReleased); + Assert.Equal(diagnostic, o.Diagnostic); Assert.NotEqual(stateChanged, o.StateChanged); }