Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"paket": {
"version": "8.0.3",
"version": "9.0.2",
"commands": [
"paket"
]
Expand Down
6 changes: 4 additions & 2 deletions Expecto.Tests/Expecto.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Expecto.Tests</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Prelude.fs" />
<Compile Include="OpenTelemetry.fs" />
<Compile Include="Tests.fs" />
<Compile Include="SummaryTests.fs" />
<Compile Include="FocusedTests.fs" />
Expand All @@ -20,4 +22,4 @@
<ProjectReference Include="..\Expecto\Expecto.fsproj" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>
</Project>
17 changes: 15 additions & 2 deletions Expecto.Tests/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ module Main

open Expecto
open Expecto.Logging
open OpenTelemetry.Resources
open OpenTelemetry
open OpenTelemetry.Trace
open System.Threading
open System.Diagnostics
open System

let serviceName = "Expecto.Tests"

let logger = Log.create serviceName

let logger = Log.create "Expecto.Tests"

[<EntryPoint>]
let main args =

let test =
Impl.testFromThisAssembly()
|> Option.orDefault (TestList ([], Normal))
|> Test.shuffle "."
runTestsWithCLIArgs [NUnit_Summary "bin/Expecto.Tests.TestResults.xml"] args test
runTestsWithCLIArgs [NUnit_Summary "bin/Expecto.Tests.TestResults.xml";] args test



204 changes: 204 additions & 0 deletions Expecto.Tests/OpenTelemetry.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
namespace Expecto

module OpenTelemetry =
open System
open System.Diagnostics
open System.Collections.Generic
open Impl
open System.Runtime.CompilerServices
type Activity with
/// <summary>Sets code semantic conventions for <c>code.function.name</c>, <c>code.filepath</c>, and <c>code.lineno</c> </summary>
/// <param name="name_space">Optional: The current namespace. Will default to using <c>Reflection.MethodBase.GetCurrentMethod().DeclaringType</c></param>
/// <param name="memberName">Optional: The current function Don't set this. This uses <c>CallerMemberName.</c></param>
/// <param name="path">Optional: The current filepath. Don't set this. This uses <c>CallerFilePath.</c></param>
/// <param name="line">Optional: The current line number. Don't set this. This uses <c>CallerLineNumber.</c></param>
member inline x.SetSource(
?nameSpace : string,
[<CallerMemberName>] ?memberName: string,
[<CallerFilePath>] ?path: string,
[<CallerLineNumber>] ?line: int) =
if not (isNull x) then
if x.GetTagItem "code.function.name" = null then
let nameSpace =
nameSpace
|> Option.defaultWith (fun () ->
Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName.Split("+") // F# has + in type names that refer to anonymous functions, we typically want the first named type
|> Seq.tryHead
|> Option.defaultValue "")
let memberName = defaultArg memberName ""
x.SetTag("code.function.name", $"{nameSpace}.{memberName}" ) |> ignore
if x.GetTagItem "code.filepath" = null then x.SetTag("code.filepath", defaultArg path "") |> ignore
if x.GetTagItem "code.lineno" = null then x.SetTag("code.lineno", defaultArg line 0) |> ignore

module internal Activity =
let inline isNotNull x = isNull x |> not

let inline setStatus (status : ActivityStatusCode) (span : Activity) =
if isNotNull span then
span.SetStatus status |> ignore

let inline setExn (e : exn) (span : Activity) =
if isNotNull span|> not then
let tags =
ActivityTagsCollection(
seq {
KeyValuePair("exception.type", box (e.GetType().Name))
KeyValuePair("exception.stacktrace", box (e.ToString()))
if not <| String.IsNullOrEmpty(e.Message) then
KeyValuePair("exception.message", box e.Message)
}
)

ActivityEvent("exception", tags = tags)
|> span.AddEvent
|> ignore

let inline setExnMarkFailed (e : exn) (span : Activity) =
if isNotNull span then
setExn e span
span |> setStatus ActivityStatusCode.Error

let setSourceLocation (sourceLoc : SourceLocation) (span : Activity) =
if isNotNull span && sourceLoc <> SourceLocation.empty then
span.SetTag("code.lineno", sourceLoc.lineNumber) |> ignore
span.SetTag("code.filepath", sourceLoc.sourcePath) |> ignore

let inline addOutcome (result : TestResult) (span : Activity) =
if isNotNull span then
let status = match result with
| Passed -> "Passed"
| Ignored _ -> "Ignored"
| Failed _ -> "Failed"
| Error _ -> "Error"
span.SetTag("test.result.status", status) |> ignore
span.SetTag("test.result.message", result) |> ignore

let inline start (span : Activity) =
if isNotNull span then
span.Start() |> ignore
span

let inline stop (span : Activity) =
if isNotNull span then
span.Stop() |> ignore

let inline setEndTimeNow (span : Activity) =
if isNotNull span then
span.SetEndTime DateTime.UtcNow |> ignore

let inline createActivity (name : string) (source : ActivitySource) =
if isNotNull source then
source.CreateActivity(name, ActivityKind.Internal)
else
null

open Activity
open System.Runtime.ExceptionServices

let inline internal reraiseAnywhere<'a> (e: exn) : 'a =
ExceptionDispatchInfo.Capture(e).Throw()
Unchecked.defaultof<'a>

module TestResult =
let ofException (e:Exception) : TestResult =
match e with
| :? AssertException as e ->
let msg =
"\n" + e.Message + "\n" +
(e.StackTrace.Split('\n')
|> Seq.skipWhile (fun l -> l.StartsWith(" at Expecto.Expect."))
|> Seq.truncate 5
|> String.concat "\n")
Failed msg

| :? FailedException as e ->
Failed ("\n"+e.Message)
| :? IgnoreException as e ->
Ignored e.Message
| :? AggregateException as e when e.InnerExceptions.Count = 1 ->
if e.InnerException :? IgnoreException then
Ignored e.InnerException.Message
else
Error e.InnerException
| e ->
Error e


let addExceptionOutcomeToSpan (span: Activity) (e: Exception) =
let testResult = TestResult.ofException e

addOutcome testResult span
match testResult with
| Ignored _ ->
setExn e span
| _ ->
setExnMarkFailed e span

let wrapCodeWithSpan (span: Activity) (test: TestCode) =
let inline handleSuccess span =
setEndTimeNow span
addOutcome Passed span
setStatus ActivityStatusCode.Ok span
let inline handleFailure span e =
setEndTimeNow span
addExceptionOutcomeToSpan span e
reraiseAnywhere e

match test with
| Sync test ->
TestCode.Sync (fun () ->
use span = start span
try
test ()
handleSuccess span
with
| e ->
handleFailure span e
)

| Async test ->
TestCode.Async (async {
use span = start span
try
do! test
handleSuccess span
with
| e ->
handleFailure span e
})
| AsyncFsCheck (testConfig, stressConfig, test) ->
TestCode.AsyncFsCheck (testConfig, stressConfig, fun fsCheckConfig -> async {
use span = start span
try
do! test fsCheckConfig
handleSuccess span
with
| e ->
handleFailure span e
})
| SyncWithCancel test->
TestCode.SyncWithCancel (fun ct ->
use span = start span
try
test ct
handleSuccess span
with
| e ->
handleFailure span e
)

/// <summary>Wraps each test with an OpenTelemetry Span/System.Diagnostics.Activity.</summary>
/// <param name="config">ExpectoConfig</param>
/// <param name="activitySource">Provides APIs start OpenTelemetry Span/System.Diagnostics.Activity</param>
/// <param name="rootTest">The tests to wrap in span/activity.</param>
/// <returns>Tests wrapped in a Span/Activity</returns>
let addOpenTelemetry_SpanPerTest (config: ExpectoConfig) (activitySource: ActivitySource) (rootTest: Test) : Test =
rootTest
|> Test.toTestCodeList
|> List.map (fun test ->
let span = activitySource |> createActivity (config.joinWith.format test.name)
span |> setSourceLocation (config.locate test.test)
{test with test = wrapCodeWithSpan span test.test}
)
|> Test.fromFlatTests config.joinWith.asString

30 changes: 30 additions & 0 deletions Expecto.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ open Expecto
open Expecto.Impl
open Expecto.Logging
open System.Globalization
open OpenTelemetry.Resources
open OpenTelemetry.Trace
open System.Diagnostics
open OpenTelemetry
open OpenTelemetry.Exporter

let serviceName = "Expecto.Tests"

let source = new ActivitySource(serviceName)

let resourceBuilder () =
ResourceBuilder
.CreateDefault()
.AddService(serviceName = serviceName)

let traceProvider () =
Sdk
.CreateTracerProviderBuilder()
.AddSource(serviceName)
.SetResourceBuilder(resourceBuilder ())
.AddOtlpExporter()
.Build()
do
let provider = traceProvider()
AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> provider.Dispose())


module Dummy =

Expand Down Expand Up @@ -1400,6 +1426,8 @@ let asyncTests =
]

open System.Threading.Tasks
open OpenTelemetry
open System.Diagnostics

[<Tests>]
let taskTests =
Expand Down Expand Up @@ -1848,6 +1876,7 @@ let cancel =
)
]


[<Tests>]
let theory =
testList "theory testing" [
Expand Down Expand Up @@ -1875,3 +1904,4 @@ let theory =
}
]
]
|> addOpenTelemetry_SpanPerTest ExpectoConfig.defaultConfig source
5 changes: 4 additions & 1 deletion Expecto.Tests/paket.references
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
FsCheck
FsCheck
OpenTelemetry.Exporter.OpenTelemetryProtocol
YoloDev.Expecto.TestSdk
Microsoft.NET.Test.Sdk
1 change: 1 addition & 0 deletions Expecto/Expecto.Impl.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Expecto

open System
open System.Collections.Generic
open System.Diagnostics
open System.Reflection
open System.Threading
Expand Down
2 changes: 2 additions & 0 deletions Expecto/Expecto.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
open Impl
open Helpers
open Expecto.Logging
open System.Diagnostics

let mutable private afterRunTestsList = []
let private afterRunTestsListLock = obj()
Expand Down Expand Up @@ -259,7 +260,7 @@
member inline __.TryFinally(p, cf) = task.TryFinally(p, cf)
member inline __.TryWith(p, cf) = task.TryWith(p, cf)
member __.Run f =
let deferred () = task.Run f

Check warning on line 263 in Expecto/Expecto.fs

View workflow job for this annotation

GitHub Actions / build

This state machine is not statically compilable. The resumable code value(s) 'f' does not have a definition. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.

Check warning on line 263 in Expecto/Expecto.fs

View workflow job for this annotation

GitHub Actions / build

This state machine is not statically compilable. The resumable code value(s) 'f' does not have a definition. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.
match focusState with
| Normal -> testCaseTask name deferred
| Focused -> ftestCaseTask name deferred
Expand Down Expand Up @@ -456,6 +457,7 @@
/// Specify test names join character.
| JoinWith of split: string


let options = [
"--sequenced", "Don't run the tests in parallel.", Args.none Sequenced
"--parallel", "Run all tests in parallel (default).", Args.none Parallel
Expand Down
3 changes: 3 additions & 0 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ nuget Hopac ~> 0.4
nuget DiffPlex ~> 1.5
nuget Mono.Cecil ~> 0.11
nuget BenchmarkDotNet ~> 0.14.0
nuget OpenTelemetry.Exporter.OpenTelemetryProtocol
nuget YoloDev.Expecto.TestSdk
nuget Microsoft.NET.Test.Sdk

group FsCheck3
source https://api.nuget.org/v3/index.json
Expand Down
Loading
Loading