@@ -10,6 +10,7 @@ open FSharp.Core.UnitTests.LibraryTestFx
1010open Xunit
1111open System.Threading
1212open System.Threading .Tasks
13+ open Xunit.Internal
1314
1415// Cancels default token.
1516[<Collection( nameof FSharp.Test.NotThreadSafeResourceCollection) >]
@@ -754,4 +755,85 @@ module AsyncTaskLikeAwaitTests =
754755 }
755756 tcs.SetException( InvalidOperationException " boom" )
756757 let ok = Async.RunSynchronously a
757- Assert.True ok
758+ Assert.True ok
759+
760+ [<Collection( nameof FSharp.Test.NotThreadSafeResourceCollection) >]
761+ module AsyncAwaitStackTraceTests =
762+
763+ open System.Runtime .CompilerServices
764+
765+ // Minimal wrapper to route through the SRTP overload instead of the specific Task<'T> overload.
766+ // Task<'T>, Task, ValueTask<'T>, and ValueTask all have higher-priority intrinsic overloads.
767+ type TaskWrapper < 'T >( inner : Task < 'T >) =
768+ member _.GetAwaiter () = inner.GetAwaiter()
769+
770+ // Plain function — provides a stable named frame at the outermost throw site.
771+ [<MethodImpl( MethodImplOptions.NoInlining) >]
772+ let throwAtLevel1 () : unit = invalidOp " boom"
773+
774+ // Level-1 task: thin wrapper around the direct throw.
775+ [<MethodImpl( MethodImplOptions.NoInlining) >]
776+ let level1Task () : Task < unit > = task { throwAtLevel1 () }
777+
778+ // Level-2 task: introduces a real async await boundary between levels 1 and 2.
779+ [<MethodImpl( MethodImplOptions.NoInlining) >]
780+ let level2Task () : Task < unit > = task { do ! level1Task () }
781+
782+ // Run via StartImmediateAsTask + .Wait() and return the inner exception.
783+ // Using StartImmediateAsTask (not RunSynchronously) ensures that the async-layer
784+ // exception machinery goes through TaskCompletionSource.SetException, which preserves
785+ // the stack trace rather than rethrowing synchronously and potentially truncating it.
786+ let runAndCaptureException ( computation : Async < unit >) : exn =
787+ // TODO swap in usage of Async.RunSynchronouslyImmediate
788+ let t = Async.StartImmediateAsTask computation
789+ let ae = Assert.Throws< AggregateException>( fun () -> t.Wait())
790+ ae.InnerException
791+
792+ // Template assertion: levels 1 and 2 must be traceable in the stack trace
793+ // regardless of which Async.Await overload is used.
794+ let checkTrace totalCount ( e : exn ) =
795+ let trace = e.StackTrace
796+ Assert.NotNull( trace)
797+ Assert.Contains( " throwAtLevel1" , trace)
798+ Assert.Contains( " level1Task" , trace)
799+ Assert.Contains( " level2Task" , trace)
800+ Assert.True(( totalCount = trace.Split( '\n' ) .Length), trace)
801+
802+ // --- Tests per overload ---
803+ // The common skeleton is: build a 3-level chain (throwAtLevel1 → level1Task → level2Task),
804+ // wrap the outermost level in an async block using Async.Await, run via
805+ // StartImmediateAsTask + .Wait(), and assert on the resulting exception's stack trace.
806+
807+ [<Fact>]
808+ let ``Await Task - of - T : all three levels visible in stack trace`` () =
809+ let e = runAndCaptureException ( async { do ! Async.Await( level2Task()) })
810+ checkTrace 3 e
811+
812+ [<Fact>]
813+ let ``Await Task ( non - generic ): all three levels visible in stack trace`` () =
814+ let e = runAndCaptureException ( async { do ! Async.Await( level2Task() :> Task) })
815+ checkTrace 3 e
816+ // Same behaviour as the Task<'T> overload — see comment there.
817+
818+ #if NETSTANDARD2_ 1
819+ [<Fact>]
820+ let ``Await ValueTask - of - T : all three levels visible in stack trace`` () =
821+ // For a faulted ValueTask<unit>, IsCompletedSuccessfully is false; the overload falls
822+ // through to AwaitTask, which takes the same path as the specific Task<'T> overload.
823+ let e = runAndCaptureException ( async { do ! Async.Await( ValueTask< unit>( level2Task())) })
824+ checkTrace 3 e
825+
826+ [<Fact>]
827+ let ``Await ValueTask ( non - generic ): all three levels visible in stack trace`` () =
828+ // Same as ValueTask<'T>: falls through to AwaitUnitTask for the non-successfully-completed case.
829+ let e = runAndCaptureException ( async { do ! Async.Await( ValueTask( level2Task() :> Task)) })
830+
831+ checkTrace 3 e
832+ #endif
833+
834+ [<Fact>]
835+ let ``Await task - like via SRTP overload : all three levels visible in stack trace`` () =
836+ let e = runAndCaptureException ( async { do ! Async.Await( TaskWrapper( level2Task())) })
837+
838+ // 4 instead of 3 as current impl has an outer "at FSharp.Core.UnitTests.Control.AsyncAwaitStackTraceTests.e@836-9.Invoke(Tuple`3 tupledArg)
839+ checkTrace 4 e
0 commit comments