Multi snapshots (#8)

This commit is contained in:
Patrick Stevens
2025-06-16 17:45:35 +01:00
committed by GitHub
parent dbe9511793
commit 9c1960722a
11 changed files with 347 additions and 92 deletions

View File

@@ -46,6 +46,8 @@ let ``This test fails: plain text comparison of ToString`` () =
} }
``` ```
## Updating an individual snapshot
If a snapshot is failing, add a `'` to the `expect` builder and rerun. If a snapshot is failing, add a `'` to the `expect` builder and rerun.
The rerun will throw, but it will update the snapshot; then remove the `'` again to put the test back into "assert snapshot" mode. The rerun will throw, but it will update the snapshot; then remove the `'` again to put the test back into "assert snapshot" mode.
@@ -78,6 +80,51 @@ let ``Example of automatically updating`` () =
} }
``` ```
## Bulk update of snapshots
*Warning*: when doing this, you should probably make sure your test fixture is `[<Parallelizable(ParallelScope.Children)>]` or less parallelizable,
or the equivalent in your test runner of choice.
Otherwise, the global state used by this mechanism may interfere with other fixtures.
You can put WoofWare.Expect into "bulk update" mode as follows:
```fsharp
open NUnit.Framework
open WoofWare.Expect
[<TestFixture>]
[<NonParallelizable>]
module BulkUpdateExample =
[<OneTimeSetUp>]
let ``Prepare to bulk-update tests`` () =
// If you don't want to enter bulk-update mode, just replace this line with a no-op `()`.
// The `updateAllSnapshots` tear-down below will simply do nothing in that case.
GlobalBuilderConfig.enterBulkUpdateMode ()
[<OneTimeTearDown>]
let ``Update all tests`` () =
GlobalBuilderConfig.updateAllSnapshots ()
[<Test>]
let ``Snapshot 2`` () =
// this snapshot fails: the "expected" isn't even JSON!
expect {
snapshotJson ""
return Map.ofList [ "1", "hi" ; "2", "my" ; "3", "name" ; "4", "is" ]
}
[<Test>]
let ``Snapshot 1`` () =
// this snapshot fails: the "expected" is not equal to the "actual"
expect {
snapshotJson @"124"
return 123
}
```
Observe the `OneTimeSetUp` which sets global state to enter "bulk update" mode, and the `OneTimeTearDown` which performs all the updates to rectify failures which were accumulated during this test run.
# Limitations # Limitations

View File

@@ -0,0 +1,48 @@
namespace WoofWare.Expect.Test
open WoofWare.Expect
open NUnit.Framework
[<TestFixture>]
[<Parallelizable(ParallelScope.Children)>]
module BulkUpdateExample =
[<OneTimeSetUp>]
let ``Prepare to bulk-update tests`` () =
// Uncomment the `enterBulkUpdateMode` to cause all failing tests to accumulate their results
// into a global mutable collection.
// At the end of the test run, you should then call `updateAllSnapshots ()`
// to commit these accumulated failures to the source files.
//
// When in bulk update mode, all tests will fail, to remind you to exit bulk update mode afterwards.
//
// We *strongly* recommend making these test fixtures `[<Parallelizable(ParallelScope.Children)>]`
// or less parallelisable.
// GlobalBuilderConfig.enterBulkUpdateMode ()
()
[<OneTimeTearDown>]
let ``Update all tests`` () =
GlobalBuilderConfig.updateAllSnapshots ()
[<Test>]
let ``Snapshot 2`` () =
expect {
snapshotJson
@"{
""1"": ""hi"",
""2"": ""my"",
""3"": ""name"",
""4"": ""is""
}"
return Map.ofList [ "1", "hi" ; "2", "my" ; "3", "name" ; "4", "is" ]
}
[<Test>]
let ``Snapshot 1`` () =
expect {
snapshotJson @"123"
return 123
}

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="Assembly.fs" /> <Compile Include="Assembly.fs" />
<Compile Include="BulkUpdateExample.fs" />
<Compile Include="SimpleTest.fs" /> <Compile Include="SimpleTest.fs" />
<Compile Include="TestSnapshotFinding.fs" /> <Compile Include="TestSnapshotFinding.fs" />
<Compile Include="TestSurface.fs" /> <Compile Include="TestSurface.fs" />

View File

@@ -2,19 +2,6 @@
open System.IO open System.IO
open System.Runtime.CompilerServices open System.Runtime.CompilerServices
open System.Text.Json
open System.Text.Json.Serialization
type private CallerInfo =
{
MemberName : string
FilePath : string
LineNumber : int
}
type private SnapshotValue =
| BareString of string
| Json of string
/// An exception indicating that a value failed to match its snapshot. /// An exception indicating that a value failed to match its snapshot.
exception ExpectException of Message : string exception ExpectException of Message : string
@@ -23,19 +10,6 @@ exception ExpectException of Message : string
/// the `snapshot` keyword multiple times. /// the `snapshot` keyword multiple times.
type YouHaveSuppliedMultipleSnapshots = private | NonConstructible type YouHaveSuppliedMultipleSnapshots = private | NonConstructible
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type ExpectState<'T> =
private
{
Snapshot : (SnapshotValue * CallerInfo) option
Actual : 'T option
}
[<RequireQualifiedAccess>]
module private Text =
let predent (c : char) (s : string) =
s.Split '\n' |> Seq.map (sprintf "%c %s" c) |> String.concat "\n"
/// <summary>Specify how the Expect computation expression treats failures.</summary> /// <summary>Specify how the Expect computation expression treats failures.</summary>
/// <remarks>You probably don't want to use this directly; use the computation expression definitions /// <remarks>You probably don't want to use this directly; use the computation expression definitions
/// like <c>expect</c> in the <c>Builder</c> module instead.</remarks> /// like <c>expect</c> in the <c>Builder</c> module instead.</remarks>
@@ -171,71 +145,49 @@ type ExpectBuilder (mode : Mode) =
/// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails. /// Computation expression `Run`, which runs a `Delay`ed snapshot assertion, throwing if the assertion fails.
member _.Run (f : unit -> ExpectState<'T>) : unit = member _.Run (f : unit -> ExpectState<'T>) : unit =
let state = f () let state = f () |> CompletedSnapshotGeneric.make
let options = JsonFSharpOptions.Default().ToJsonSerializerOptions () let raiseError (snapshot : string) (actual : string) : unit =
match mode with
match state.Snapshot, state.Actual with | Mode.AssertMockingSource (mockSource, line) ->
| Some (snapshot, source), Some actual -> sprintf
let raiseError (snapshot : string) (actual : string) : unit = "snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
match mode with mockSource
| Mode.AssertMockingSource (mockSource, line) -> line
state.Caller.MemberName
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
|> ExpectException
|> raise
| Mode.Assert ->
if GlobalBuilderConfig.bulkUpdate.Value > 0 then
GlobalBuilderConfig.registerTest state
else
sprintf sprintf
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s" "snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s"
mockSource state.Caller.FilePath
line state.Caller.LineNumber
source.MemberName state.Caller.MemberName
(snapshot |> Text.predent '-') (snapshot |> Text.predent '-')
(actual |> Text.predent '+') (actual |> Text.predent '+')
|> ExpectException |> ExpectException
|> raise |> raise
| Mode.Assert -> | Mode.Update ->
sprintf let lines = File.ReadAllLines state.Caller.FilePath
"snapshot mismatch! snapshot at %s:%i (%s) was:\n\n%s\n\nactual was:\n\n%s" let oldContents = String.concat "\n" lines
source.FilePath let lines = SnapshotUpdate.updateSnapshotAtLine lines state.Caller.LineNumber actual
source.LineNumber File.WriteAllLines (state.Caller.FilePath, lines)
source.MemberName failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents)
(snapshot |> Text.predent '-')
(actual |> Text.predent '+')
|> ExpectException
|> raise
| Mode.Update ->
let lines = File.ReadAllLines source.FilePath
let oldContents = String.concat "\n" lines
let lines = SnapshotUpdate.updateSnapshotAtLine lines source.LineNumber actual
File.WriteAllLines (source.FilePath, lines)
failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents)
match snapshot with match CompletedSnapshotGeneric.passesAssertion state with
| SnapshotValue.Json snapshot -> | None ->
let canonicalSnapshot = JsonDocument.Parse snapshot match mode, GlobalBuilderConfig.bulkUpdate.Value with
| Mode.Update, _
let canonicalActual = | _, 1 ->
JsonSerializer.Serialize (actual, options) |> JsonDocument.Parse failwith
"Snapshot assertion passed, but we are in snapshot-updating mode. Use the `expect` builder instead of `expect'` to assert the contents of a single snapshot; disable `GlobalBuilderConfig.bulkUpdate` to move back to assertion-checking mode."
if not (JsonElement.DeepEquals (canonicalActual.RootElement, canonicalSnapshot.RootElement)) then | _ -> ()
raiseError (canonicalSnapshot.RootElement.ToString ()) (canonicalActual.RootElement.ToString ()) | Some (expected, actual) -> raiseError expected actual
else
match mode with
| Mode.Update ->
failwith
"Snapshot assertion passed, but we are in snapshot-updating mode. Use the `expect` builder instead of `expect'` to assert the contents of a snapshot."
| _ -> ()
| SnapshotValue.BareString snapshot ->
let actual = actual.ToString ()
if actual <> snapshot then
raiseError snapshot actual
else
match mode with
| Mode.Update ->
failwith
"Snapshot assertion passed, but we are in snapshot-updating mode. Use the `expect` builder instead of `expect'` to assert the contents of a snapshot."
| _ -> ()
| None, _ -> failwith "Must specify snapshot"
| _, None -> failwith "Must specify actual value with 'return'"
/// Module containing the `expect` builder. /// Module containing the `expect` builder.
[<AutoOpen>] [<AutoOpen>]

52
WoofWare.Expect/Config.fs Normal file
View File

@@ -0,0 +1,52 @@
namespace WoofWare.Expect
open System.Threading
/// Module holding global mutable state controlling the behaviour of WoofWare.Expect
/// when running in bulk-update mode.
[<RequireQualifiedAccess>]
module GlobalBuilderConfig =
let internal bulkUpdate = ref 0
/// <summary>
/// Call this to make the <c>expect</c> builder register all tests for bulk update as it runs.
/// </summary>
/// <remarks>
/// We *strongly* recommend making test fixtures <c>Parallelizable(ParallelScope.Children)</c> or less parallelizable (for NUnit) if you're running in bulk update mode.
/// The implied global mutable state is liable to interfere with other expect builders in other fixtures otherwise.
/// </remarks>
let enterBulkUpdateMode () =
if Interlocked.Increment bulkUpdate <> 1 then
failwith
"WoofWare.Expect requires bulk updates to happen serially: for example, make the test fixture `[<NonParallelizable>]` if you're using NUnit."
let private allTests : ResizeArray<CompletedSnapshot> = ResizeArray ()
/// <summary>
/// Clear the set of failing tests registered by any previous bulk-update runs.
/// </summary>
///
/// <remarks>
/// You probably don't need to do this, because your test runner is probably tearing down
/// anyway after the tests have failed; this is mainly here for WoofWare.Expect's own internal testing.
/// </remarks>
let clearTests () = lock allTests allTests.Clear
let internal registerTest (s : CompletedSnapshotGeneric<'T>) : unit =
let toAdd = s |> CompletedSnapshot.make
lock allTests (fun () -> allTests.Add toAdd)
/// <summary>
/// For all tests whose failures have already been registered,
/// transform the files on disk so that the failing snapshots now pass.
/// </summary>
let updateAllSnapshots () =
let bulkUpdate' = Interlocked.Decrement bulkUpdate
try
if bulkUpdate' = 0 then
let allTests = lock allTests (fun () -> Seq.toArray allTests)
SnapshotUpdate.updateAll allTests
finally
clearTests ()

106
WoofWare.Expect/Domain.fs Normal file
View File

@@ -0,0 +1,106 @@
namespace WoofWare.Expect
open System.Text.Json
open System.Text.Json.Serialization
/// <summary>
/// Information about where in source code a specific snapshot is located.
/// </summary>
type CallerInfo =
internal
{
MemberName : string
FilePath : string
LineNumber : int
}
type private SnapshotValue =
| BareString of string
| Json of string
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type ExpectState<'T> =
private
{
Snapshot : (SnapshotValue * CallerInfo) option
Actual : 'T option
}
/// The state accumulated by the `expect` builder. You should never find yourself interacting with this type.
type internal CompletedSnapshotGeneric<'T> =
private
{
SnapshotValue : SnapshotValue
Caller : CallerInfo
Actual : 'T
}
[<RequireQualifiedAccess>]
module internal CompletedSnapshotGeneric =
let make (state : ExpectState<'T>) : CompletedSnapshotGeneric<'T> =
match state.Snapshot, state.Actual with
| Some (snapshot, source), Some actual ->
{
SnapshotValue = snapshot
Caller = source
Actual = actual
}
| None, _ -> failwith "Must specify snapshot"
| _, None -> failwith "Must specify actual value with 'return'"
let private jsonOptions =
let options = JsonFSharpOptions.Default().ToJsonSerializerOptions ()
options.AllowTrailingCommas <- true
options.WriteIndented <- true
options
let internal replacement (s : CompletedSnapshotGeneric<'T>) =
match s.SnapshotValue with
| SnapshotValue.BareString _existing -> s.Actual.ToString ()
| SnapshotValue.Json _existing ->
JsonSerializer.Serialize (s.Actual, jsonOptions)
|> JsonDocument.Parse
|> _.RootElement
|> _.ToString()
/// Returns None if the assertion passes, or Some (expected, actual) if the assertion fails.
let internal passesAssertion (state : CompletedSnapshotGeneric<'T>) : (string * string) option =
match state.SnapshotValue with
| SnapshotValue.Json snapshot ->
let canonicalSnapshot =
try
JsonDocument.Parse snapshot |> Some
with _ ->
None
let canonicalActual =
JsonSerializer.Serialize (state.Actual, jsonOptions) |> JsonDocument.Parse
match canonicalSnapshot with
| None -> Some (snapshot, canonicalActual.RootElement.ToString ())
| Some canonicalSnapshot ->
if not (JsonElement.DeepEquals (canonicalActual.RootElement, canonicalSnapshot.RootElement)) then
Some (canonicalSnapshot.RootElement.ToString (), canonicalActual.RootElement.ToString ())
else
None
| SnapshotValue.BareString snapshot ->
let actual = state.Actual.ToString ()
if actual = snapshot then None else Some (snapshot, actual)
/// Represents a snapshot test that has failed and is awaiting update or report to the user.
type CompletedSnapshot =
internal
{
CallerInfo : CallerInfo
Replacement : string
}
[<RequireQualifiedAccess>]
module internal CompletedSnapshot =
let make (s : CompletedSnapshotGeneric<'T>) =
{
CallerInfo = s.Caller
Replacement = CompletedSnapshotGeneric.replacement s
}

View File

@@ -254,15 +254,19 @@ module internal SnapshotUpdate =
/// <remarks>Example usage: /// <remarks>Example usage:
/// <c>updateSnapshotAtLine [|lines-of-file|] 42 "new test output"</c> /// <c>updateSnapshotAtLine [|lines-of-file|] 42 "new test output"</c>
/// /// <br />
/// This will find a snapshot call on line 42 like: /// This will find a snapshot call on line 42 like:
/// snapshot "old value" -> snapshot @"new test output" /// <ul>
/// snapshot @"old value" -> snapshot @"new test output" /// <li><c>snapshot "old value"</c> -> <c>snapshot @"new test output"</c></li>
/// snapshot """old value""" -> snapshot @"new test output" /// <li><c>snapshot @"old value"</c> -> <c>snapshot @"new test output"</c></li>
/// snapshot """multi /// <li><c>snapshot """old value"""</c> -> <c>snapshot @"new test output"</c></li>
/// line""" -> snapshot """multi /// <li><c>snapshot "has \"\"\" in it"</c> -> <c>snapshot @"has """""" in it"</c></li>
/// line""" /// <li>
/// snapshot "has \"\"\" in it" -> snapshot @"has """""" in it" /// <code>snapshot """multi
/// line"""</code> -> <code>snapshot """multi
/// line"""</code>
/// </li>
/// </ul>
/// </remarks> /// </remarks>
let updateSnapshotAtLine (fileLines : string[]) (snapshotLine : int) (newValue : string) : string[] = let updateSnapshotAtLine (fileLines : string[]) (snapshotLine : int) (newValue : string) : string[] =
match findSnapshotString fileLines snapshotLine with match findSnapshotString fileLines snapshotLine with
@@ -270,3 +274,31 @@ module internal SnapshotUpdate =
Console.Error.WriteLine ("String literal to update: " + string<StringLiteralInfo> info) Console.Error.WriteLine ("String literal to update: " + string<StringLiteralInfo> info)
updateSnapshot fileLines info newValue updateSnapshot fileLines info newValue
| None -> failwithf "Could not find string literal after snapshot at line %d" snapshotLine | None -> failwithf "Could not find string literal after snapshot at line %d" snapshotLine
/// <summary>
/// Bulk-apply all the snapshot replacements.
/// </summary>
/// <param name="fileLines">The original file contents, as an array of lines.</param>
/// <param name="sources">The (unsorted) line numbers of the snapshots which need to be replaced, and the replacement value for each.</param>
/// <returns>The entire desired new contents of the file, as an array of lines.</returns>
let private updateAllLines (fileLines : string[]) (sources : (int * string) seq) : string[] =
sources
|> Seq.sortByDescending fst
|> Seq.fold (fun lines (lineNum, replacement) -> updateSnapshotAtLine lines lineNum replacement) fileLines
/// <summary>
/// Update every failed snapshot in the input, editing the files on disk.
/// </summary>
let updateAll (sources : CompletedSnapshot seq) : unit =
sources
|> Seq.groupBy (fun csc -> csc.CallerInfo.FilePath)
|> Seq.iter (fun (callerFile, callers) ->
let contents = System.IO.File.ReadAllLines callerFile
let sources =
callers |> Seq.map (fun csc -> csc.CallerInfo.LineNumber, csc.Replacement)
let newContents = updateAllLines contents sources
System.IO.File.WriteAllLines (callerFile, newContents)
)

View File

@@ -4,6 +4,10 @@ WoofWare.Expect.Builder.expect' [static property]: [read-only] WoofWare.Expect.E
WoofWare.Expect.Builder.expectWithMockedFilePath [static method]: (string, int) -> WoofWare.Expect.ExpectBuilder WoofWare.Expect.Builder.expectWithMockedFilePath [static method]: (string, int) -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.ExpectBuilder WoofWare.Expect.Builder.get_expect [static method]: unit -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder WoofWare.Expect.Builder.get_expect' [static method]: unit -> WoofWare.Expect.ExpectBuilder
WoofWare.Expect.CallerInfo inherit obj, implements WoofWare.Expect.CallerInfo System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CallerInfo System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.CallerInfo.Equals [method]: (WoofWare.Expect.CallerInfo, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.CompletedSnapshot inherit obj, implements WoofWare.Expect.CompletedSnapshot System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.CompletedSnapshot System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.CompletedSnapshot.Equals [method]: (WoofWare.Expect.CompletedSnapshot, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.ExpectBuilder inherit obj WoofWare.Expect.ExpectBuilder inherit obj
WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int) WoofWare.Expect.ExpectBuilder..ctor [constructor]: (string * int)
WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool WoofWare.Expect.ExpectBuilder..ctor [constructor]: bool
@@ -23,6 +27,10 @@ WoofWare.Expect.ExpectException.Equals [method]: System.Exception -> bool
WoofWare.Expect.ExpectException.Message [property]: [read-only] string WoofWare.Expect.ExpectException.Message [property]: [read-only] string
WoofWare.Expect.ExpectState`1 inherit obj, implements 'T WoofWare.Expect.ExpectState System.IEquatable, System.Collections.IStructuralEquatable, 'T WoofWare.Expect.ExpectState System.IComparable, System.IComparable, System.Collections.IStructuralComparable WoofWare.Expect.ExpectState`1 inherit obj, implements 'T WoofWare.Expect.ExpectState System.IEquatable, System.Collections.IStructuralEquatable, 'T WoofWare.Expect.ExpectState System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.ExpectState`1.Equals [method]: ('T WoofWare.Expect.ExpectState, System.Collections.IEqualityComparer) -> bool WoofWare.Expect.ExpectState`1.Equals [method]: ('T WoofWare.Expect.ExpectState, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.GlobalBuilderConfig inherit obj
WoofWare.Expect.GlobalBuilderConfig.clearTests [static method]: unit -> unit
WoofWare.Expect.GlobalBuilderConfig.enterBulkUpdateMode [static method]: unit -> unit
WoofWare.Expect.GlobalBuilderConfig.updateAllSnapshots [static method]: unit -> unit
WoofWare.Expect.Mode inherit obj, implements WoofWare.Expect.Mode System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.Mode System.IComparable, System.IComparable, System.Collections.IStructuralComparable WoofWare.Expect.Mode inherit obj, implements WoofWare.Expect.Mode System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.Mode System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.Expect.Mode.Equals [method]: (WoofWare.Expect.Mode, System.Collections.IEqualityComparer) -> bool WoofWare.Expect.Mode.Equals [method]: (WoofWare.Expect.Mode, System.Collections.IEqualityComparer) -> bool
WoofWare.Expect.YouHaveSuppliedMultipleSnapshots inherit obj, implements WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IComparable, System.IComparable, System.Collections.IStructuralComparable WoofWare.Expect.YouHaveSuppliedMultipleSnapshots inherit obj, implements WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Expect.YouHaveSuppliedMultipleSnapshots System.IComparable, System.IComparable, System.Collections.IStructuralComparable

6
WoofWare.Expect/Text.fs Normal file
View File

@@ -0,0 +1,6 @@
namespace WoofWare.Expect
[<RequireQualifiedAccess>]
module internal Text =
let predent (c : char) (s : string) =
s.Split '\n' |> Seq.map (sprintf "%c %s" c) |> String.concat "\n"

View File

@@ -17,7 +17,10 @@
<ItemGroup> <ItemGroup>
<Compile Include="AssemblyInfo.fs" /> <Compile Include="AssemblyInfo.fs" />
<Compile Include="Text.fs" />
<Compile Include="Domain.fs" />
<Compile Include="SnapshotUpdate.fs" /> <Compile Include="SnapshotUpdate.fs" />
<Compile Include="Config.fs" />
<Compile Include="Builder.fs" /> <Compile Include="Builder.fs" />
<None Include="..\README.md"> <None Include="..\README.md">
<Pack>True</Pack> <Pack>True</Pack>

View File

@@ -1,5 +1,5 @@
{ {
"version": "0.2", "version": "0.3",
"publicReleaseRefSpec": [ "publicReleaseRefSpec": [
"^refs/heads/main$" "^refs/heads/main$"
], ],