From 8811704a12ba7d211c249d9b14e9950f92b56be9 Mon Sep 17 00:00:00 2001 From: Milos Chaloupka Date: Tue, 20 Jan 2026 22:08:54 -0800 Subject: [PATCH 1/3] Remove .NET 4.5 targets and update to .NET 9 - Remove net452 targets from all projects - Delete netfx.props (Mono support no longer needed) - Update test/example projects from net6.0 to net9.0 - Keep netstandard2.0 for library projects (TickSpec, TickSpec.Xunit) - Update GitHub Actions to .NET 9.0.x and actions v4 - Remove Linux framework workaround from Build.fs - Simplify Directory.Build.props (remove explicit FSharp.Core pinning) - Update package dependencies to latest versions: - NUnit 3.14.0, NUnit3TestAdapter 4.5.0 - Microsoft.NET.Test.Sdk 17.12.0 - Xunit 2.9.3, xunit.runner.visualstudio 3.0.1 - MSTest 3.7.0, Autofac 8.2.0, Expecto 10.2.1 - Microsoft.SourceLink.GitHub 8.0.0 - Fix Expecto API change (runTestsInAssemblyWithCLIArgs) - Fix NUnit CollectionAssert usage in Prelude.fs - Remove unnecessary System.Runtime references Co-Authored-By: Claude Opus 4.5 --- .github/workflows/main.yml | 8 +++--- .github/workflows/release.yml | 6 ++-- Directory.Build.props | 13 ++------- .../CustomContainer/CustomContainer.fsproj | 15 +++++----- .../DependencyInjection.fsproj | 10 +++---- .../FunctionalInjection.fsproj | 10 +++---- .../TaggedExamples/TaggedExamples.fsproj | 10 +++---- .../CommandLine/CSharp/CSharp.csproj | 4 +-- .../CommandLine/FSharp/FSharp.fsproj | 4 +-- .../CommandLine/TicTacToe/TicTacToe.fsproj | 4 +-- .../FSharp.Expecto/Expecto.FSharp.fsproj | 11 ++++---- .../Expecto/FSharp.Expecto/Program.fs | 2 +- .../MSTest/MSTest.FSharp/MSTest.FSharp.fsproj | 11 ++++---- .../NUnit/CSharp.NUnit/NUnit.CSharp.csproj | 10 +++---- .../NUnit/FSharp.NUnit/NUnit.FSharp.fsproj | 10 +++---- .../xUnit/FSharp.xUnit/Xunit.FSharp.fsproj | 11 ++++---- Examples/ByStyle/Attributes/Attributes.fsproj | 10 +++---- Examples/ByStyle/Attributes/Prelude.fs | 2 +- Examples/ByStyle/Functional/Functional.fsproj | 10 +++---- .../ByStyle/Interactive/Interactive.fsproj | 4 +-- TickSpec.Tests/TickSpec.Tests.fsproj | 9 +++--- TickSpec/TickSpec.fsproj | 3 +- Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj | 5 ++-- fake-build/Build.fs | 5 ---- fake-build/Build.fsproj | 5 ++-- netfx.props | 28 ------------------- 26 files changed, 85 insertions(+), 135 deletions(-) delete mode 100644 netfx.props diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c37d0aa..dbbcf41b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,13 +16,13 @@ jobs: fail-fast: false matrix: os: [windows-latest, ubuntu-latest] - dotnet: [6.0.x] + dotnet: [9.0.x] runs-on: ${{ matrix.os }} - + steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet }} - name: Build & Tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 376f9b00..f5b50a4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,11 +12,11 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 9.0.x - name: Build, Tests & Release if: github.ref == 'refs/heads/master' run: dotnet run --project ./fake-build/Build.fsproj -- -t PublishNuget diff --git a/Directory.Build.props b/Directory.Build.props index 8705d910..3127391b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,16 +1,7 @@ - - true - true - - - - - - - + Phillip Trelford, Ruben Bartelink, Milos Chaloupka Copyright © 2010-21 Phillip Trelford, Ruben Bartelink, Milos Chaloupka https://github.com/fsprojects/TickSpec @@ -29,7 +20,7 @@ snupkg - + diff --git a/Examples/ByFeature/CustomContainer/CustomContainer.fsproj b/Examples/ByFeature/CustomContainer/CustomContainer.fsproj index 45311d8a..1031dfcf 100644 --- a/Examples/ByFeature/CustomContainer/CustomContainer.fsproj +++ b/Examples/ByFeature/CustomContainer/CustomContainer.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -14,13 +14,12 @@ - - - - - + + + + + - \ No newline at end of file diff --git a/Examples/ByFeature/DependencyInjection/DependencyInjection.fsproj b/Examples/ByFeature/DependencyInjection/DependencyInjection.fsproj index bc6677ff..92d42d67 100644 --- a/Examples/ByFeature/DependencyInjection/DependencyInjection.fsproj +++ b/Examples/ByFeature/DependencyInjection/DependencyInjection.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -14,8 +14,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByFeature/FunctionalInjection/FunctionalInjection.fsproj b/Examples/ByFeature/FunctionalInjection/FunctionalInjection.fsproj index 170a9f55..15919d0c 100644 --- a/Examples/ByFeature/FunctionalInjection/FunctionalInjection.fsproj +++ b/Examples/ByFeature/FunctionalInjection/FunctionalInjection.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -27,8 +27,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByFeature/TaggedExamples/TaggedExamples.fsproj b/Examples/ByFeature/TaggedExamples/TaggedExamples.fsproj index 15ed9633..7d213298 100644 --- a/Examples/ByFeature/TaggedExamples/TaggedExamples.fsproj +++ b/Examples/ByFeature/TaggedExamples/TaggedExamples.fsproj @@ -1,8 +1,8 @@ - + - net6.0;net452 + net9.0 @@ -14,8 +14,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByFramework/CommandLine/CSharp/CSharp.csproj b/Examples/ByFramework/CommandLine/CSharp/CSharp.csproj index 95cacdec..0b4f7390 100644 --- a/Examples/ByFramework/CommandLine/CSharp/CSharp.csproj +++ b/Examples/ByFramework/CommandLine/CSharp/CSharp.csproj @@ -1,9 +1,9 @@  - + Exe - net6.0;net452 + net9.0 diff --git a/Examples/ByFramework/CommandLine/FSharp/FSharp.fsproj b/Examples/ByFramework/CommandLine/FSharp/FSharp.fsproj index 90b66bbf..3ce0ece3 100644 --- a/Examples/ByFramework/CommandLine/FSharp/FSharp.fsproj +++ b/Examples/ByFramework/CommandLine/FSharp/FSharp.fsproj @@ -1,9 +1,9 @@  - + Exe - net6.0;net452 + net9.0 diff --git a/Examples/ByFramework/CommandLine/TicTacToe/TicTacToe.fsproj b/Examples/ByFramework/CommandLine/TicTacToe/TicTacToe.fsproj index f7875bc6..0d504955 100644 --- a/Examples/ByFramework/CommandLine/TicTacToe/TicTacToe.fsproj +++ b/Examples/ByFramework/CommandLine/TicTacToe/TicTacToe.fsproj @@ -1,9 +1,9 @@  - + Exe - net6.0;net452 + net9.0 diff --git a/Examples/ByFramework/Expecto/FSharp.Expecto/Expecto.FSharp.fsproj b/Examples/ByFramework/Expecto/FSharp.Expecto/Expecto.FSharp.fsproj index cd822462..c788e2d2 100644 --- a/Examples/ByFramework/Expecto/FSharp.Expecto/Expecto.FSharp.fsproj +++ b/Examples/ByFramework/Expecto/FSharp.Expecto/Expecto.FSharp.fsproj @@ -1,8 +1,8 @@  - + - net6.0 + net9.0 Exe False @@ -19,11 +19,10 @@ - - - - + + + diff --git a/Examples/ByFramework/Expecto/FSharp.Expecto/Program.fs b/Examples/ByFramework/Expecto/FSharp.Expecto/Program.fs index 30e1cad4..b77ccf35 100644 --- a/Examples/ByFramework/Expecto/FSharp.Expecto/Program.fs +++ b/Examples/ByFramework/Expecto/FSharp.Expecto/Program.fs @@ -30,5 +30,5 @@ let additionTests = featureTest "Addition.feature" [] let main args = - runTestsInAssembly defaultConfig args + runTestsInAssemblyWithCLIArgs [] args diff --git a/Examples/ByFramework/MSTest/MSTest.FSharp/MSTest.FSharp.fsproj b/Examples/ByFramework/MSTest/MSTest.FSharp/MSTest.FSharp.fsproj index b5695990..cd52acc8 100644 --- a/Examples/ByFramework/MSTest/MSTest.FSharp/MSTest.FSharp.fsproj +++ b/Examples/ByFramework/MSTest/MSTest.FSharp/MSTest.FSharp.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -12,11 +12,10 @@ - - - + + + - \ No newline at end of file diff --git a/Examples/ByFramework/NUnit/CSharp.NUnit/NUnit.CSharp.csproj b/Examples/ByFramework/NUnit/CSharp.NUnit/NUnit.CSharp.csproj index 83a2de3c..e44c6295 100644 --- a/Examples/ByFramework/NUnit/CSharp.NUnit/NUnit.CSharp.csproj +++ b/Examples/ByFramework/NUnit/CSharp.NUnit/NUnit.CSharp.csproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -12,8 +12,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByFramework/NUnit/FSharp.NUnit/NUnit.FSharp.fsproj b/Examples/ByFramework/NUnit/FSharp.NUnit/NUnit.FSharp.fsproj index 6a87d5e5..a4ec0991 100644 --- a/Examples/ByFramework/NUnit/FSharp.NUnit/NUnit.FSharp.fsproj +++ b/Examples/ByFramework/NUnit/FSharp.NUnit/NUnit.FSharp.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -14,8 +14,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByFramework/xUnit/FSharp.xUnit/Xunit.FSharp.fsproj b/Examples/ByFramework/xUnit/FSharp.xUnit/Xunit.FSharp.fsproj index 93661370..68fdd1bf 100644 --- a/Examples/ByFramework/xUnit/FSharp.xUnit/Xunit.FSharp.fsproj +++ b/Examples/ByFramework/xUnit/FSharp.xUnit/Xunit.FSharp.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -16,11 +16,10 @@ - - - + + + - \ No newline at end of file diff --git a/Examples/ByStyle/Attributes/Attributes.fsproj b/Examples/ByStyle/Attributes/Attributes.fsproj index cc2a90fc..9609176e 100644 --- a/Examples/ByStyle/Attributes/Attributes.fsproj +++ b/Examples/ByStyle/Attributes/Attributes.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -38,8 +38,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByStyle/Attributes/Prelude.fs b/Examples/ByStyle/Attributes/Prelude.fs index 745b22c2..c2edd249 100644 --- a/Examples/ByStyle/Attributes/Prelude.fs +++ b/Examples/ByStyle/Attributes/Prelude.fs @@ -5,7 +5,7 @@ module NUnitExtensions = open NUnit.Framework type Assert with static member Contains(expected:obj, xs:'a list) = - Assert.Contains(expected, xs |> List.toArray) + CollectionAssert.Contains(xs |> List.toArray, expected) module Union = open Microsoft.FSharp.Reflection diff --git a/Examples/ByStyle/Functional/Functional.fsproj b/Examples/ByStyle/Functional/Functional.fsproj index a9c76abe..e1f9dac1 100644 --- a/Examples/ByStyle/Functional/Functional.fsproj +++ b/Examples/ByStyle/Functional/Functional.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 @@ -18,8 +18,8 @@ - - - + + + \ No newline at end of file diff --git a/Examples/ByStyle/Interactive/Interactive.fsproj b/Examples/ByStyle/Interactive/Interactive.fsproj index 7ca8b60a..3b247047 100644 --- a/Examples/ByStyle/Interactive/Interactive.fsproj +++ b/Examples/ByStyle/Interactive/Interactive.fsproj @@ -1,8 +1,8 @@  - + - net6.0;net452 + net9.0 diff --git a/TickSpec.Tests/TickSpec.Tests.fsproj b/TickSpec.Tests/TickSpec.Tests.fsproj index fccb25bb..73372190 100644 --- a/TickSpec.Tests/TickSpec.Tests.fsproj +++ b/TickSpec.Tests/TickSpec.Tests.fsproj @@ -1,8 +1,7 @@  - - net6.0;net452 + net9.0 @@ -13,8 +12,8 @@ - - - + + + \ No newline at end of file diff --git a/TickSpec/TickSpec.fsproj b/TickSpec/TickSpec.fsproj index 1a7426d9..9ef00686 100644 --- a/TickSpec/TickSpec.fsproj +++ b/TickSpec/TickSpec.fsproj @@ -1,8 +1,7 @@  - - netstandard2.0;net452 + netstandard2.0 TickSpec TickSpec is a lightweight Behaviour Driven Development (BDD) framework for .Net. diff --git a/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj b/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj index e1218694..cf78af65 100644 --- a/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj +++ b/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj @@ -1,8 +1,7 @@  - - netstandard2.0;net452 + netstandard2.0 TickSpec.Xunit TickSpec integration with Xunit. @@ -18,6 +17,6 @@ - + diff --git a/fake-build/Build.fs b/fake-build/Build.fs index 87eb2a06..5168892b 100644 --- a/fake-build/Build.fs +++ b/fake-build/Build.fs @@ -81,17 +81,12 @@ let createTargets () = ) Target.create "Test" (fun _ -> - // Xunit seems to be failing under Linux with net452 runner, let's just skip it - // the .NET 4 tests all together there - let framework = if Environment.isWindows then None else Some "net6.0" - Sln |> DotNet.test (fun o -> { o with Configuration = DotNet.Release NoBuild = true MSBuildParams = { o.MSBuildParams with DisableInternalBinLog = true } - Framework = framework } ) ) diff --git a/fake-build/Build.fsproj b/fake-build/Build.fsproj index 74d2462b..b572bf59 100644 --- a/fake-build/Build.fsproj +++ b/fake-build/Build.fsproj @@ -1,8 +1,7 @@ Exe - net6.0 - Major + net9.0 @@ -18,6 +17,6 @@ - + diff --git a/netfx.props b/netfx.props deleted file mode 100644 index c41b419a..00000000 --- a/netfx.props +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - true - - - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono - /usr/lib/mono - /usr/local/lib/mono - - - $(BaseFrameworkPathOverrideForMono)/4.5-api - $(BaseFrameworkPathOverrideForMono)/4.5.1-api - $(BaseFrameworkPathOverrideForMono)/4.5.2-api - $(BaseFrameworkPathOverrideForMono)/4.6-api - $(BaseFrameworkPathOverrideForMono)/4.6.1-api - $(BaseFrameworkPathOverrideForMono)/4.6.2-api - $(BaseFrameworkPathOverrideForMono)/4.7-api - $(BaseFrameworkPathOverrideForMono)/4.7.1-api - true - - - $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) - - \ No newline at end of file From b2860ca9156b733f267a9f2067465295e775012c Mon Sep 17 00:00:00 2001 From: Milos Chaloupka Date: Tue, 20 Jan 2026 22:11:23 -0800 Subject: [PATCH 2/3] Add macOS to CI build matrix Co-Authored-By: Claude Opus 4.5 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dbbcf41b..95cfba0a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, ubuntu-latest] + os: [windows-latest, ubuntu-latest, macos-latest] dotnet: [9.0.x] runs-on: ${{ matrix.os }} From 98b08dab09c5f60a7431beff6da96f95a60c0894 Mon Sep 17 00:00:00 2001 From: Milos Chaloupka Date: Thu, 22 Jan 2026 20:06:00 -0800 Subject: [PATCH 3/3] Enable debugging breakpoints in .NET Core --- TickSpec/FeatureGen.fs | 97 +++++++++++++++++---- TickSpec/ScenarioGen.fs | 44 +++++++--- TickSpec/StepAttributes.fs | 6 +- TickSpec/TickSpec.fs | 40 +++++---- TickSpec/TickSpec.fsproj | 2 +- Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj | 2 +- 6 files changed, 140 insertions(+), 51 deletions(-) diff --git a/TickSpec/FeatureGen.fs b/TickSpec/FeatureGen.fs index b3eac162..a31aeea0 100644 --- a/TickSpec/FeatureGen.fs +++ b/TickSpec/FeatureGen.fs @@ -1,21 +1,28 @@ -namespace TickSpec +namespace TickSpec -#if !NETSTANDARD2_0 open System open System.Collections.Generic open System.Diagnostics +open System.Diagnostics.SymbolStore +open System.IO open System.Reflection open System.Reflection.Emit +open System.Reflection.Metadata +open System.Reflection.Metadata.Ecma335 +open System.Reflection.PortableExecutable +open System.Runtime.Loader open TickSpec.ScenarioGen -type internal FeatureGen(featureName:string,documentUrl:string) = - let assemblyName = "Feature" - /// Feature dynamic assembly +type internal FeatureGen(featureName:string, documentUrl:string) = + // Create unique assembly name to avoid conflicts when multiple features are loaded + let assemblyName = sprintf "Feature_%s_%s" (featureName.Replace(" ", "_")) (Guid.NewGuid().ToString("N")) + /// Feature persisted assembly builder (for PDB support) let assemblyBuilder = - AppDomain.CurrentDomain - .DefineDynamicAssembly( - AssemblyName(assemblyName), - AssemblyBuilderAccess.Run) + PersistedAssemblyBuilder( + AssemblyName(assemblyName), + typeof.Assembly) + /// Separate load context for this feature assembly + let loadContext = AssemblyLoadContext(assemblyName, isCollectible = true) /// Set assembly debuggable attribute do let debuggableAttribute = let ctor = @@ -28,12 +35,72 @@ type internal FeatureGen(featureName:string,documentUrl:string) = assemblyBuilder.SetCustomAttribute debuggableAttribute /// Feature dynamic module let module_ = - assemblyBuilder.DefineDynamicModule - (featureName+".dll", true) - /// Feature source document - let doc = module_.DefineDocument(documentUrl, Guid.Empty, Guid.Empty, Guid.Empty) + assemblyBuilder.DefineDynamicModule(featureName+".dll") + /// Document writer for sequence points + let doc = module_.DefineDocument(documentUrl, SymLanguageType.CSharp, SymLanguageVendor.Microsoft, SymDocumentType.Text) + + /// Mutable to track if assembly has been saved + let mutable savedAssembly : Assembly option = None + + /// Save the assembly to memory and load it + member private this.EnsureAssemblyLoaded() = + match savedAssembly with + | Some asm -> asm + | None -> + // Generate metadata with PDB support using out parameters + let mutable ilStream : BlobBuilder = null + let mutable mappedFieldData : BlobBuilder = null + let mutable pdbBuilder : MetadataBuilder = null + let metadataBuilder = assemblyBuilder.GenerateMetadata(&ilStream, &mappedFieldData, &pdbBuilder) + + // Get row counts for PDB builder + let rowCounts = metadataBuilder.GetRowCounts() + + // Create portable PDB builder (no entry point for DLL) + let portablePdbBuilder = + PortablePdbBuilder( + pdbBuilder, + rowCounts, + Unchecked.defaultof) + + // Serialize PDB to get content ID + let pdbBlobBuilder = BlobBuilder() + let pdbContentId = portablePdbBuilder.Serialize(pdbBlobBuilder) + let pdbBytes = pdbBlobBuilder.ToArray() + + // Create debug directory entry + let debugDirectoryBuilder = DebugDirectoryBuilder() + debugDirectoryBuilder.AddCodeViewEntry(assemblyName + ".pdb", pdbContentId, portablePdbBuilder.FormatVersion) + + // Create PE with debug info + let peHeaderBuilder = PEHeaderBuilder(imageCharacteristics = Characteristics.Dll) + + let peBuilder = + ManagedPEBuilder( + peHeaderBuilder, + MetadataRootBuilder(metadataBuilder), + ilStream, + mappedFieldData = mappedFieldData, + debugDirectoryBuilder = debugDirectoryBuilder) + + // Write PE to byte array + let peBlobBuilder = BlobBuilder() + peBuilder.Serialize(peBlobBuilder) |> ignore + let peBytes = peBlobBuilder.ToArray() + + // Load assembly from stream with PDB for debugging + use peStream = new MemoryStream(peBytes) + use pdbStream = new MemoryStream(pdbBytes) + let asm = loadContext.LoadFromStream(peStream, pdbStream) + savedAssembly <- Some asm + asm + /// Assembly of generated feature - member this.Assembly = assemblyBuilder :> Assembly + member this.Assembly = this.EnsureAssemblyLoaded() + + /// Gets the document writer for sequence points + member this.Document = doc + /// Generates scenario type from lines member this.GenScenario (events) @@ -42,5 +109,3 @@ type internal FeatureGen(featureName:string,documentUrl:string) = lines:(LineSource * MethodInfo * string[]) [], parameters:(string * string)[]) = generateScenario module_ doc events parsers (scenarioName,lines,parameters) - -#endif \ No newline at end of file diff --git a/TickSpec/ScenarioGen.fs b/TickSpec/ScenarioGen.fs index a6c15287..1758a013 100644 --- a/TickSpec/ScenarioGen.fs +++ b/TickSpec/ScenarioGen.fs @@ -1,18 +1,34 @@ module internal TickSpec.ScenarioGen -#if !NETSTANDARD2_0 open System open System.Collections.Generic +open System.Diagnostics.SymbolStore open System.Reflection open System.Reflection.Emit open Microsoft.FSharp.Reflection +/// Sanitizes scenario name for use as a type name +/// Replaces characters that have special meaning in .NET type names: +/// comma (,), plus (+), ampersand (&), asterisk (*), brackets ([ ]), period (.), backslash (\) +/// Also replaces angle brackets (< >) which cause issues in metadata +let sanitizeTypeName (name: string) = + name + .Replace("\\", "_") // backslash first to avoid double-escaping + .Replace(",", "_") + .Replace("+", "_") + .Replace("&", "_") + .Replace("*", "_") + .Replace("[", "_") + .Replace("]", "_") + .Replace("<", "_") + .Replace(">", "_") + /// Defines scenario type let defineScenarioType (module_:ModuleBuilder) (scenarioName) = module_.DefineType( - scenarioName, + sanitizeTypeName scenarioName, TypeAttributes.Public ||| TypeAttributes.Class) /// Defines _provider field @@ -368,7 +384,7 @@ let storeMethodResultInProvider /// Defines step method let defineStepMethod - doc + (doc:ISymbolDocumentWriter) (scenarioBuilder:TypeBuilder) (providerField:FieldBuilder) (parsers:IDictionary) @@ -383,8 +399,8 @@ let defineStepMethod [||]) /// Step method ILGenerator let gen = stepMethod.GetILGenerator() - // Set marker in source document - gen.MarkSequencePoint(doc,n,1,n,line.Text.Length+1) + // Mark sequence point for debugging/breakpoints + gen.MarkSequencePoint(doc, n, 1, n, 100) // Handle generic methods let mi = if mi.ContainsGenericParameters then @@ -410,7 +426,7 @@ let defineStepMethod emitArgument gen providerField parsers (arg,p) gen.Emit(OpCodes.Stloc, locals.[i]) // } - gen.Emit(OpCodes.Leave_S, block) + gen.Emit(OpCodes.Leave, block) // catch(Exception ex) { gen.BeginCatchBlock(typeof) // throw ArgumentException(message, name, ex); @@ -533,24 +549,24 @@ let defineRunMethod beforeStepEvents |> emitEvents gen.Emit(OpCodes.Ldarg_0) gen.Emit(OpCodes.Callvirt,stepMethod) - gen.Emit(OpCodes.Leave_S, exit) + gen.Emit(OpCodes.Leave, exit) gen.BeginFinallyBlock() afterStepEvents |> emitEvents gen.EndExceptionBlock() ) - gen.Emit(OpCodes.Leave_S, exit) + gen.Emit(OpCodes.Leave, exit) gen.BeginFinallyBlock() // Execute after scenario events afterScenarioEvents |> emitEvents gen.EndExceptionBlock() - gen.Emit(OpCodes.Leave_S, exitOuter) + gen.Emit(OpCodes.Leave, exitOuter) gen.BeginFinallyBlock() // Dispose the ServiceProvider if it is IDisposable gen.Emit(OpCodes.Ldarg_0) gen.Emit(OpCodes.Ldfld, providerField) gen.Emit(OpCodes.Isinst, typeof) let labelNoDispose = gen.DefineLabel() - gen.Emit(OpCodes.Brfalse_S, labelNoDispose) + gen.Emit(OpCodes.Brfalse, labelNoDispose) gen.Emit(OpCodes.Ldarg_0) gen.Emit(OpCodes.Ldfld, providerField) gen.Emit(OpCodes.Callvirt, typeof.GetMethod("Dispose")) @@ -560,6 +576,7 @@ let defineRunMethod gen.Emit(OpCodes.Ret) /// Generates Type for specified Scenario +/// Returns the type name (for lookup after assembly is loaded) let generateScenario (module_:ModuleBuilder) doc @@ -581,7 +598,6 @@ let generateScenario |> Array.map (defineStepMethod doc scenarioBuilder providerField parsers) defineRunMethod scenarioBuilder providerField events stepMethods - /// Return scenario - scenarioBuilder.CreateType() - -#endif \ No newline at end of file + /// Create the type and return its sanitized name (for lookup after assembly load) + scenarioBuilder.CreateType() |> ignore + sanitizeTypeName scenarioName \ No newline at end of file diff --git a/TickSpec/StepAttributes.fs b/TickSpec/StepAttributes.fs index 7614da27..fd200fac 100644 --- a/TickSpec/StepAttributes.fs +++ b/TickSpec/StepAttributes.fs @@ -12,21 +12,21 @@ type StepAttribute internal (step:string) = type GivenAttribute(step:string) = inherit StepAttribute (step) new () = GivenAttribute(null) - new (format,[] args) = + new (format:string,[] args:obj[]) = GivenAttribute(String.Format(format,args)) /// Method annotation for when step [] type WhenAttribute(step) = inherit StepAttribute (step) new () = WhenAttribute(null) - new (format,[] args) = + new (format:string,[] args:obj[]) = WhenAttribute(String.Format(format,args)) /// Method annotation for then step [] type ThenAttribute(step) = inherit StepAttribute(step) new () = ThenAttribute(null) - new (format,[] args) = + new (format:string,[] args:obj[]) = ThenAttribute(String.Format(format,args)) /// Method annotation for parsers of string -> 'a diff --git a/TickSpec/TickSpec.fs b/TickSpec/TickSpec.fs index 5b13d6c9..e51fef6f 100644 --- a/TickSpec/TickSpec.fs +++ b/TickSpec/TickSpec.fs @@ -26,8 +26,8 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) = static let getStepAttributes (m:MemberInfo) = Attribute.GetCustomAttributes(m,typeof) static let isMethodInScope (feature:string) (scenario:ScenarioSource) (scopedTags,scopedFeatures,scopedScenarios,m) = - let trim p (s:string) = - if s.StartsWith p then (s.Substring p.Length).Trim() else s + let trim (p:string) (s:string) = + if s.StartsWith(p) then (s.Substring p.Length).Trim() else s let tagged = match scopedTags with | [] -> true @@ -259,7 +259,6 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) = member __.GenerateFeature (sourceUrl:string,lines:string[]) = let featureSource = parseFeature lines let feature = featureSource.Name -#if !NETSTANDARD2_0 let gen = FeatureGen(featureSource.Name,sourceUrl) let genType scenario = let lines = @@ -271,10 +270,26 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) = events valueParsers (scenario.Name, lines, scenario.Parameters) - let createAction scenario (scenarioMetadata: ScenarioMetadata) = - let t = lazy (genType scenario) + // Generate ALL types BEFORE accessing assembly (required for PersistedAssemblyBuilder) + let scenarioTypes = + featureSource.Scenarios + |> Seq.map (fun scenario -> + let scenarioMetadata = + { Name=scenario.Name;Description=getDescription scenario.Steps;Parameters=scenario.Parameters;Tags=scenario.Tags;Rule=scenario.Rule } + let typeName = genType scenario + (scenario, scenarioMetadata, typeName)) + |> Seq.toArray + // Now get the assembly (this finalizes the builder) + let assembly = gen.Assembly + let createAction (typeName: string) (scenarioMetadata: ScenarioMetadata) = TickSpec.Action(fun () -> - let ctor = t.Force().GetConstructor([| + let t = + assembly.GetTypes() + |> Array.tryFind (fun t -> t.Name = typeName) + |> Option.defaultWith (fun () -> + failwithf "Type '%s' not found in generated assembly. Available types: %A" + typeName (assembly.GetTypes() |> Array.map (fun t -> t.FullName))) + let ctor = t.GetConstructor([| typeof> typeof |]) @@ -288,18 +303,11 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) = mi.Invoke(instance,[||]) |> ignore ) let scenarios = - featureSource.Scenarios - |> Seq.map (fun scenario -> - let scenarioMetadata = - { Name=scenario.Name;Description=getDescription scenario.Steps;Parameters=scenario.Parameters;Tags=scenario.Tags;Rule=scenario.Rule } - createAction scenario scenarioMetadata + scenarioTypes + |> Seq.map (fun (_, scenarioMetadata, typeName) -> + createAction typeName scenarioMetadata |> Scenario.fromScenarioMetadata scenarioMetadata ) - let assembly = gen.Assembly -#else - let scenarios = __.GenerateScenarios lines - let assembly = null -#endif { Name = featureSource.Name; Source = sourceUrl; Assembly = assembly; diff --git a/TickSpec/TickSpec.fsproj b/TickSpec/TickSpec.fsproj index 9ef00686..06620192 100644 --- a/TickSpec/TickSpec.fsproj +++ b/TickSpec/TickSpec.fsproj @@ -1,7 +1,7 @@  - netstandard2.0 + net9.0 TickSpec TickSpec is a lightweight Behaviour Driven Development (BDD) framework for .Net. diff --git a/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj b/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj index cf78af65..a9a05afb 100644 --- a/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj +++ b/Wiring/TickSpec.Xunit/TickSpec.Xunit.fsproj @@ -1,7 +1,7 @@  - netstandard2.0 + net9.0 TickSpec.Xunit TickSpec integration with Xunit.