using System; using System.Threading; using System.Threading.Tasks; using SteamKit2; using Xunit; namespace Tests { public class AsyncJobFacts { class Callback : CallbackMsg { public bool IsFinished { get; set; } } [Fact] public void AsyncJobCtorRegistersJob() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Assert.True( client.jobManager.asyncJobs.ContainsKey( asyncJob ), "Async job dictionary should contain the jobid key" ); Assert.True( client.jobManager.asyncJobs.ContainsKey( 123 ), "Async job dictionary should contain jobid key as a value type" ); } [Fact] public void AysncJobCompletesOnCallback() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Task asyncTask = asyncJob.ToTask(); client.PostCallback( new Callback { JobID = 123 } ); Assert.True( asyncTask.IsCompleted, "Async job should be completed after callback is posted" ); Assert.False( asyncTask.IsCanceled, "Async job should not be canceled after callback is posted" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted after callback is posted" ); } [Fact] public void AsyncJobClearsOnCompletion() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); client.PostCallback( new Callback { JobID = 123 } ); Assert.False( client.jobManager.asyncJobs.ContainsKey( asyncJob ), "Async job dictionary should no longer contain jobid key after callback is posted" ); Assert.False( client.jobManager.asyncJobs.ContainsKey( 123 ), "Async job dictionary should no longer contain jobid key (as value type) after callback is posted" ); } [Fact] public async Task AsyncJobClearsOnTimeout() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); Assert.False( client.jobManager.asyncJobs.ContainsKey( asyncJob ), "Async job dictionary should no longer contain jobid key after timeout" ); Assert.False( client.jobManager.asyncJobs.ContainsKey( 123 ), "Async job dictionary should no longer contain jobid key (as value type) after timeout" ); } [Fact] public async Task AsyncJobCancelsOnSetFailedTimeout() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Task asyncTask = asyncJob.ToTask(); asyncJob.SetFailed( dueToRemoteFailure: false ); Assert.True( asyncTask.IsCompleted, "Async job should be completed on message timeout" ); Assert.True( asyncTask.IsCanceled, "Async job should be canceled on message timeout" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted on message timeout" ); await Assert.ThrowsAsync( async () => await asyncTask ); } [Fact] public async Task AsyncJobGivesBackCallback() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Task jobTask = asyncJob.ToTask(); Callback ourCallback = new Callback { JobID = 123 }; client.PostCallback( ourCallback ); Assert.Same( await jobTask, ourCallback ); } [Fact] public async Task AsyncJobTimesout() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); Task asyncTask = asyncJob.ToTask(); await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); Assert.True( asyncTask.IsCompleted, "Async job should be completed yet" ); Assert.True( asyncTask.IsCanceled, "Async job should be canceled yet" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted yet" ); await Assert.ThrowsAsync( async () => await asyncTask ); } [Fact] public void AsyncJobThrowsExceptionOnNullCallback() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Assert.Throws( () => asyncJob.AddResult( null ) ); } [Fact] public async Task AsyncJobThrowsFailureExceptionOnFailure() { SteamClient client = new SteamClient(); AsyncJob asyncJob = new AsyncJob( client, 123 ); Task asyncTask = asyncJob.ToTask(); asyncJob.SetFailed( dueToRemoteFailure: true ); Assert.True( asyncTask.IsCompleted, "Async job should be completed after job failure" ); Assert.False( asyncTask.IsCanceled, "Async job should not be canceled after job failure" ); Assert.True( asyncTask.IsFaulted, "Async job should be faulted after job failure" ); await Assert.ThrowsAsync( async () => await asyncTask ); } [Fact] public void AsyncJobMultipleFinishedOnEmptyPredicate() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => true ); Task.ResultSet> asyncTask = asyncJob.ToTask(); bool jobFinished = asyncJob.AddResult( new Callback { JobID = 123 } ); Assert.True( jobFinished, "Async job should inform that it is completed when completion predicate is always true and a result is given" ); Assert.True( asyncTask.IsCompleted, "Async job should be completed when empty predicate result is given" ); Assert.False( asyncTask.IsCanceled, "Async job should not be canceled when empty predicate result is given" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted when empty predicate result is given" ); } [Fact] public void AsyncJobMultipleFinishedOnPredicate() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => call.IsFinished ); Task.ResultSet> asyncTask = asyncJob.ToTask(); bool jobFinished = asyncJob.AddResult( new Callback { JobID = 123, IsFinished = false } ); Assert.False( jobFinished, "Async job should not inform that it is finished when completion predicate is false after a result is given" ); Assert.False( asyncTask.IsCompleted, "Async job should not be completed when completion predicate is false" ); Assert.False( asyncTask.IsCanceled, "Async job should not be canceled when completion predicate is false" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted when completion predicate is false" ); jobFinished = asyncJob.AddResult( new Callback { JobID = 123, IsFinished = true } ); Assert.True( jobFinished, "Async job should inform completion when completion predicat is passed after a result is given" ); Assert.True( asyncTask.IsCompleted, "Async job should be completed when completion predicate is true" ); Assert.False( asyncTask.IsCanceled, "Async job should not be canceled when completion predicate is true" ); Assert.False( asyncTask.IsFaulted, "Async job should not be faulted when completion predicate is true" ); } [Fact] public void AsyncJobMultipleClearsOnCompletion() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => call.IsFinished ); client.PostCallback( new Callback { JobID = 123, IsFinished = true } ); Assert.False( client.jobManager.asyncJobs.ContainsKey( asyncJob ), "Async job dictionary should not contain jobid key for AsyncJobMultiple on completion" ); Assert.False( client.jobManager.asyncJobs.ContainsKey( 123 ), "Async job dictionary should not contain jobid key (as value type) for AsyncJobMultiple on completion" ); } [Fact] public async Task AsyncJobMultipleClearsOnTimeout() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, ccall => true ); asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); Assert.False( client.jobManager.asyncJobs.ContainsKey( asyncJob ), "Async job dictionary should no longer contain jobid key after timeout" ); Assert.False( client.jobManager.asyncJobs.ContainsKey( 123 ), "Async job dictionary should no longer contain jobid key (as value type) after timeout" ); } [Fact] public async Task AsyncJobMultipleExtendsTimeoutOnMessage() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => call.IsFinished ); asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); Task.ResultSet> asyncTask = asyncJob.ToTask(); // we should not be completed or canceled yet Assert.False( asyncTask.IsCompleted, "AsyncJobMultiple should not be completed yet" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled yet" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted yet" ); // give result 1 of 2 asyncJob.AddResult( new Callback { JobID = 123, IsFinished = false } ); // delay for what the original timeout would have been await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); // we still shouldn't be completed or canceled (timed out) Assert.False( asyncTask.IsCompleted, "AsyncJobMultiple should not be completed yet after result was added (result should extend timeout)" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled yet after result was added (result should extend timeout)" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted yet after a result was added (result should extend timeout)" ); asyncJob.AddResult( new Callback { JobID = 123, IsFinished = true } ); // we should be completed but not canceled or faulted Assert.True( asyncTask.IsCompleted, "AsyncJobMultiple should be completed when final result is added to set" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled when final result is added to set" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted when final result is added to set" ); } [Fact] public async Task AsyncJobMultipleTimesout() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => false ); asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); Task.ResultSet> asyncTask = asyncJob.ToTask(); await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); Assert.True( asyncTask.IsCompleted, "AsyncJobMultiple should be completed after job timeout" ); Assert.True( asyncTask.IsCanceled, "AsyncJobMultiple should be canceled after job timeout" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted after job timeout" ); await Assert.ThrowsAsync( async () => await asyncTask ); } [Fact] public async Task AsyncJobMultipleCompletesOnIncompleteResult() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => call.IsFinished ); asyncJob.Timeout = TimeSpan.FromSeconds( 1 ); Task.ResultSet> asyncTask = asyncJob.ToTask(); Callback onlyResult = new Callback { JobID = 123, IsFinished = false }; asyncJob.AddResult( onlyResult ); // adding a result will extend the job's timeout, but we'll cheat here and decrease it asyncJob.Timeout = TimeSpan.FromMilliseconds( 50 ); await Task.Delay( TimeSpan.FromMilliseconds( 70 ) ); client.jobManager.CancelTimedoutJobs(); Assert.True( asyncTask.IsCompleted, "AsyncJobMultiple should be completed on partial (timed out) result set" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled on partial (timed out) result set" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted on a partial (failed) result set" ); AsyncJobMultiple.ResultSet result = await asyncTask; Assert.False( result.Complete, "ResultSet should be incomplete" ); Assert.False( result.Failed, "ResultSet should not be failed" ); Assert.Single( result.Results ); Assert.Contains( onlyResult, result.Results ); } [Fact] public async Task AsyncJobMultipleCompletesOnIncompleteResultAndFailure() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => call.IsFinished ); asyncJob.Timeout = TimeSpan.FromSeconds( 1 ); Task.ResultSet> asyncTask = asyncJob.ToTask(); Callback onlyResult = new Callback { JobID = 123, IsFinished = false }; asyncJob.AddResult( onlyResult ); asyncJob.SetFailed( dueToRemoteFailure: true ); Assert.True( asyncTask.IsCompleted, "AsyncJobMultiple should be completed on partial (failed) result set" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled on partial (failed) result set" ); Assert.False( asyncTask.IsFaulted, "AsyncJobMultiple should not be faulted on a partial (failed) result set" ); AsyncJobMultiple.ResultSet result = await asyncTask; Assert.False( result.Complete, "ResultSet should be incomplete" ); Assert.True( result.Failed, "ResultSet should be failed" ); Assert.Single( result.Results ); Assert.Contains( onlyResult, result.Results ); } [Fact] public void AsyncJobMultipleThrowsExceptionOnNullCallback() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => true ); Assert.Throws( () => asyncJob.AddResult( null ) ); } [Fact] public async Task AsyncJobMultipleThrowsFailureExceptionOnFailure() { SteamClient client = new SteamClient(); AsyncJobMultiple asyncJob = new AsyncJobMultiple( client, 123, call => false ); Task.ResultSet> asyncTask = asyncJob.ToTask(); asyncJob.SetFailed( dueToRemoteFailure: true ); Assert.True( asyncTask.IsCompleted, "AsyncJobMultiple should be completed after job failure" ); Assert.False( asyncTask.IsCanceled, "AsyncJobMultiple should not be canceled after job failure" ); Assert.True( asyncTask.IsFaulted, "AsyncJobMultiple should be faulted after job failure" ); await Assert.ThrowsAsync( async () => await asyncTask ); } [Fact] public void AsyncJobContinuesAsynchronously() { SteamClient client = new SteamClient(); var asyncJob = new AsyncJob( client, 123 ); var asyncTask = asyncJob.ToTask(); var continuationThreadID = -1; var continuation = asyncTask.ContinueWith( t => { continuationThreadID = Environment.CurrentManagedThreadId; }, TaskContinuationOptions.ExecuteSynchronously ); var completionThreadID = Environment.CurrentManagedThreadId; asyncJob.AddResult( new Callback { JobID = 123 } ); WaitForTaskWithoutRunningInline( continuation ); Assert.NotEqual( -1, continuationThreadID ); Assert.NotEqual( completionThreadID, continuationThreadID ); } [Fact] public void AsyncJobMultipleContinuesAsynchronously() { SteamClient client = new SteamClient(); var asyncJob = new AsyncJobMultiple( client, 123, call => true ); var asyncTask = asyncJob.ToTask(); var continuationThreadID = -1; var continuation = asyncTask.ContinueWith( t => { continuationThreadID = Environment.CurrentManagedThreadId; }, TaskContinuationOptions.ExecuteSynchronously ); var completionThreadID = Environment.CurrentManagedThreadId; asyncJob.AddResult( new Callback { JobID = 123 } ); WaitForTaskWithoutRunningInline( continuation ); Assert.NotEqual( -1, continuationThreadID ); Assert.NotEqual( completionThreadID, continuationThreadID ); } static void WaitForTaskWithoutRunningInline( Task task ) { // If we await the task, our Thread can go back to the scheduler and come eligible to // run task continuations. If we call .Wait with an infinite timeout / no cancellation token, then // the .NET runtime will attempt to run the task inline... on the current thread. // To avoid that we need to supply a cancellable-but-never-cancelled token, or do other hackery // with IAsyncResult or mutexes. This appears to be the simplest. using var cts = new CancellationTokenSource(); task.Wait( cts.Token ); } } }