From efd5565f3f8e209f7fada00e0c8c0779459faf35 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 24 Jul 2020 14:56:12 +0100 Subject: [PATCH] Add CBOR property-based tests (#39828) * Add CBOR property-based tests * address feedback --- eng/Versions.props | 1 + .../System.Formats.Cbor.sln | 7 + .../tests/CborDocument/CborDocument.fs | 51 ++++ .../CborDocument/CborDocumentSerializer.fs | 185 +++++++++++++++ .../CborDocument/CborPropertyTestContext.fs | 106 +++++++++ ...System.Formats.Cbor.Tests.DataModel.fsproj | 16 ++ .../tests/CborRoundtripTests.cs | 180 -------------- .../tests/PropertyTests/CborPropertyTests.cs | 219 ++++++++++++++++++ .../PropertyTests/CborRandomGenerators.cs | 68 ++++++ .../tests/System.Formats.Cbor.Tests.csproj | 22 +- 10 files changed, 673 insertions(+), 182 deletions(-) create mode 100644 src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocument.fs create mode 100644 src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocumentSerializer.fs create mode 100644 src/libraries/System.Formats.Cbor/tests/CborDocument/CborPropertyTestContext.fs create mode 100644 src/libraries/System.Formats.Cbor/tests/CborDocument/System.Formats.Cbor.Tests.DataModel.fsproj delete mode 100644 src/libraries/System.Formats.Cbor/tests/CborRoundtripTests.cs create mode 100644 src/libraries/System.Formats.Cbor/tests/PropertyTests/CborPropertyTests.cs create mode 100644 src/libraries/System.Formats.Cbor/tests/PropertyTests/CborRandomGenerators.cs diff --git a/eng/Versions.props b/eng/Versions.props index 4d00ac9bfe4e19..8a36d350af3512 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -142,6 +142,7 @@ 2.0.5 12.0.3 4.12.0 + 2.14.3 3.0.0-preview-20200715.1 diff --git a/src/libraries/System.Formats.Cbor/System.Formats.Cbor.sln b/src/libraries/System.Formats.Cbor/System.Formats.Cbor.sln index baed5dfb19566d..31f26813ae83a3 100644 --- a/src/libraries/System.Formats.Cbor/System.Formats.Cbor.sln +++ b/src/libraries/System.Formats.Cbor/System.Formats.Cbor.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Formats.Cbor", "ref\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Formats.Cbor.Tests", "tests\System.Formats.Cbor.Tests.csproj", "{1C84C43C-69A8-493E-AC11-0DA9C22BFB46}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Formats.Cbor.Tests.DataModel", "tests\CborDocument\System.Formats.Cbor.Tests.DataModel.fsproj", "{3BF538B4-0985-4918-90A7-8F15BA9E661C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {1C84C43C-69A8-493E-AC11-0DA9C22BFB46}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C84C43C-69A8-493E-AC11-0DA9C22BFB46}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C84C43C-69A8-493E-AC11-0DA9C22BFB46}.Release|Any CPU.Build.0 = Release|Any CPU + {3BF538B4-0985-4918-90A7-8F15BA9E661C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BF538B4-0985-4918-90A7-8F15BA9E661C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BF538B4-0985-4918-90A7-8F15BA9E661C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BF538B4-0985-4918-90A7-8F15BA9E661C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -40,6 +46,7 @@ Global {57B5AD1E-D0BA-4811-9276-9BBAFF44AB31} = {59B37ECC-D5BB-488A-9571-1E239EDF19A9} {B17130D2-0A75-464A-A3E7-67123C567CAF} = {B3417D0B-D64E-4CC8-AEAA-F0C7C963248F} {1C84C43C-69A8-493E-AC11-0DA9C22BFB46} = {00675670-C8E7-4428-A16F-9E6B933586A8} + {3BF538B4-0985-4918-90A7-8F15BA9E661C} = {00675670-C8E7-4428-A16F-9E6B933586A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36EE2FB8-4CD8-4A12-8FD5-9FDF3D0F6D40} diff --git a/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocument.fs b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocument.fs new file mode 100644 index 00000000000000..7e63f401de2018 --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocument.fs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Formats.Cbor.Tests.DataModel + +open System + +// -- +// Models a CBOR document type using a discriminated union. +// Discriminated unions have structural equality for free +// and random instances can be generated using FsCheck. +// +// The type and serialization helpers are used by the +// property tests in the main test project. +// + +type CborDocument = + // Major type 0 + | UnsignedInteger of uint64 + // Major type 1 + | NegativeInteger of uint64 + // Major type 2 + | ByteString of byte[] + | ByteStringIndefiniteLength of byte[][] + // Major type 3 + | TextString of string + | TextStringIndefiniteLength of string[] + // Major type 4 + | Array of isDefiniteLength:bool * CborDocument[] + // Major type 5 + | Map of isDefiniteLength:bool * Map + // Major type 6 + | Tag of tag:uint64 * CborDocument + | SemanticValue of CborSemanticValue + // Major type 7 + | Double of double + | SimpleValue of value:byte + +// Defines a set of semantic values drawing either from the CBOR spec +// or invented here for the purposes of this test. +and CborSemanticValue = + | Null + | Bool of bool + | Int32 of int + | Int64 of int64 + | Half of Half + | Single of single + | DateTimeOffset of DateTimeOffset + | UnixTimeSeconds of seconds:int64 + | BigInt of bigint + | Decimal of decimal diff --git a/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocumentSerializer.fs b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocumentSerializer.fs new file mode 100644 index 00000000000000..44728b426ed7f1 --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocumentSerializer.fs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +module rec System.Formats.Cbor.Tests.DataModel.CborDocumentSerializer + +open System +open System.Formats.Cbor +open System.Formats.Cbor.Tests.DataModel + +let createWriter (context : CborPropertyTestContext) = + new CborWriter( + conformanceMode = context.ConformanceMode, + convertIndefiniteLengthEncodings = context.ConvertIndefiniteLengthItems, + allowMultipleRootLevelValues = (context.RootDocuments.Length > 1)) + +let createReader (context : CborPropertyTestContext) (encoding : byte[]) = + new CborReader( + data = ReadOnlyMemory.op_Implicit encoding, + conformanceMode = context.ConformanceMode, + allowMultipleRootLevelValues = (context.RootDocuments.Length > 1)) + +let encode (context : CborPropertyTestContext) = + let writer = createWriter context + for doc in context.RootDocuments do + write writer doc + writer.Encode() + +let decode (context : CborPropertyTestContext) (data : byte[]) = + let reader = createReader context data + let values = new System.Collections.Generic.List<_>() + while reader.PeekState() <> CborReaderState.Finished do + values.Add(read reader) + values.ToArray() + +let rec write (writer : CborWriter) (doc : CborDocument) = + match doc with + | UnsignedInteger i -> writer.WriteUInt64 i + | NegativeInteger i -> writer.WriteCborNegativeIntegerRepresentation i + | ByteString bs -> writer.WriteByteString bs + | TextString ts -> writer.WriteTextString ts + | ByteStringIndefiniteLength bss -> + writer.WriteStartIndefiniteLengthByteString() + for bs in bss do + writer.WriteByteString bs + writer.WriteEndIndefiniteLengthByteString() + + | TextStringIndefiniteLength tss -> + writer.WriteStartIndefiniteLengthTextString() + for ts in tss do + writer.WriteTextString ts + writer.WriteEndIndefiniteLengthTextString() + + | Array (isDefiniteLength, elements) -> + writer.WriteStartArray (if isDefiniteLength then Nullable elements.Length else Nullable()) + for elem in elements do + write writer elem + writer.WriteEndArray() + + | Map (isDefiniteLength, pairs) -> + writer.WriteStartMap (if isDefiniteLength then Nullable pairs.Count else Nullable()) + for kv in pairs do + write writer kv.Key ; write writer kv.Value + writer.WriteEndMap() + + | Tag (tag, nested) -> + writer.WriteTag (LanguagePrimitives.EnumOfValue tag) + write writer nested + + | Double d -> writer.WriteDouble d + | SimpleValue v -> writer.WriteSimpleValue(LanguagePrimitives.EnumOfValue v) + + | SemanticValue value -> + writer.WriteTag CborTagExt.SemValueGuard + writeSemanticValue writer value + +let private writeSemanticValue (writer : CborWriter) (s : CborSemanticValue) = + match s with + | Null -> writer.WriteTag CborTagExt.Null; writer.WriteNull() + | Bool b -> writer.WriteTag CborTagExt.Bool; writer.WriteBoolean b + | Int32 i -> writer.WriteTag CborTagExt.Int32; writer.WriteInt32 i + | Int64 i -> writer.WriteTag CborTagExt.Int64; writer.WriteInt64 i + | Half f -> writer.WriteTag CborTagExt.Half; writer.WriteHalf f + | Single f -> writer.WriteTag CborTagExt.Single; writer.WriteSingle f + | DateTimeOffset d -> writer.WriteDateTimeOffset d + | UnixTimeSeconds s -> writer.WriteUnixTimeSeconds s + | BigInt i -> writer.WriteBigInteger i + | Decimal d -> writer.WriteDecimal d + +let rec read (reader : CborReader) : CborDocument = + match reader.PeekState() with + | CborReaderState.UnsignedInteger -> UnsignedInteger(reader.ReadUInt64()) + | CborReaderState.NegativeInteger -> NegativeInteger(reader.ReadCborNegativeIntegerRepresentation()) + | CborReaderState.ByteString -> ByteString(reader.ReadByteString()) + | CborReaderState.TextString -> TextString(reader.ReadTextString()) + | CborReaderState.StartArray -> + let length = reader.ReadStartArray() + if length.HasValue then + let results = Array.zeroCreate length.Value + for i = 0 to results.Length - 1 do + results.[i] <- read reader + + reader.ReadEndArray() + Array(true, results) + else + let results = new System.Collections.Generic.List<_>() + while reader.PeekState() <> CborReaderState.EndArray do + results.Add(read reader) + + reader.ReadEndArray() + Array(false, results.ToArray()) + + | CborReaderState.StartIndefiniteLengthByteString -> + reader.ReadStartIndefiniteLengthByteString() + let chunks = new System.Collections.Generic.List() + while reader.PeekState() <> CborReaderState.EndIndefiniteLengthByteString do + chunks.Add(reader.ReadByteString()) + reader.ReadEndIndefiniteLengthByteString() + ByteStringIndefiniteLength(chunks.ToArray()) + + | CborReaderState.StartIndefiniteLengthTextString -> + reader.ReadStartIndefiniteLengthTextString() + let chunks = new System.Collections.Generic.List() + while reader.PeekState() <> CborReaderState.EndIndefiniteLengthTextString do + chunks.Add(reader.ReadTextString()) + reader.ReadEndIndefiniteLengthTextString() + TextStringIndefiniteLength(chunks.ToArray()) + + | CborReaderState.StartMap -> + let length = reader.ReadStartMap() + if length.HasValue then + let results = Array.zeroCreate length.Value + for i = 0 to results.Length - 1 do + results.[i] <- (read reader, read reader) + reader.ReadEndMap() + Map(true, Map.ofArray results) + else + let results = new System.Collections.Generic.List<_>() + while reader.PeekState() <> CborReaderState.EndMap do + results.Add(read reader, read reader) + reader.ReadEndMap() + Map(false, Map.ofSeq results) + + | CborReaderState.Tag -> + let tag = reader.ReadTag() + if tag = CborTagExt.SemValueGuard then SemanticValue(readSemanticValue reader) + else Tag (LanguagePrimitives.EnumToValue tag, read reader) + + | CborReaderState.HalfPrecisionFloat + | CborReaderState.SinglePrecisionFloat + | CborReaderState.DoublePrecisionFloat -> Double (reader.ReadDouble()) + + | CborReaderState.Null + | CborReaderState.Boolean + | CborReaderState.SimpleValue -> + let value = reader.ReadSimpleValue() + SimpleValue(LanguagePrimitives.EnumToValue value) + + | state -> failwithf "Unrecognized reader state %O" state + +let private readSemanticValue (reader : CborReader) : CborSemanticValue = + match reader.PeekTag() with + | CborTagExt.Null -> let _ = reader.ReadTag() in reader.ReadNull(); Null + | CborTagExt.Bool -> let _ = reader.ReadTag() in Bool(reader.ReadBoolean()) + | CborTagExt.Int32 -> let _ = reader.ReadTag() in Int32(reader.ReadInt32()) + | CborTagExt.Int64 -> let _ = reader.ReadTag() in Int64(reader.ReadInt64()) + | CborTagExt.Half -> let _ = reader.ReadTag() in Half(reader.ReadHalf()) + | CborTagExt.Single -> let _ = reader.ReadTag() in Single(reader.ReadSingle()) + | CborTag.DateTimeString -> DateTimeOffset(reader.ReadDateTimeOffset()) + | CborTag.UnixTimeSeconds -> let dto = reader.ReadUnixTimeSeconds() in UnixTimeSeconds(dto.ToUnixTimeSeconds()) + | CborTag.UnsignedBigNum + | CborTag.NegativeBigNum -> BigInt(reader.ReadBigInteger()) + | CborTag.DecimalFraction -> Decimal(reader.ReadDecimal()) + | tag -> failwithf "Unrecognized tag %O" tag + +// defines a set of custom CBOR tags for semantic value encodings +module private CborTagExt = + + let [] SemValueGuard : CborTag = LanguagePrimitives.EnumOfValue 50015001uL + + let [] Null : CborTag = LanguagePrimitives.EnumOfValue 5001uL + let [] Bool : CborTag = LanguagePrimitives.EnumOfValue 5002uL + let [] Int32 : CborTag = LanguagePrimitives.EnumOfValue 5003uL + let [] Int64 : CborTag = LanguagePrimitives.EnumOfValue 5004uL + let [] Half : CborTag = LanguagePrimitives.EnumOfValue 5005uL + let [] Single : CborTag = LanguagePrimitives.EnumOfValue 5006uL diff --git a/src/libraries/System.Formats.Cbor/tests/CborDocument/CborPropertyTestContext.fs b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborPropertyTestContext.fs new file mode 100644 index 00000000000000..921000f3ef5151 --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/CborDocument/CborPropertyTestContext.fs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Formats.Cbor.Tests.DataModel + +open System.Formats.Cbor +open System.Formats.Cbor.Tests.DataModel + +/// Randomly generated record containing parameters for a CBOR property-based test +[] +type CborPropertyTestContext = + { + RootDocuments : CborDocument[] + ConformanceMode : CborConformanceMode + ConvertIndefiniteLengthItems : bool + } + +module rec CborPropertyTestContextHelper = + + /// Identifies & transforms documents that might not be accepted under the supplied conformance mode + let create (mode : CborConformanceMode) (convertIndefiniteLengthItems : bool) (docs : CborDocument[]) = + { + RootDocuments = Array.map (normalize mode convertIndefiniteLengthItems) docs + ConvertIndefiniteLengthItems = convertIndefiniteLengthItems + ConformanceMode = mode + } + + /// Gets the expected value that we would expected to see after a full serialization/deserialization roundtrip + let getExpectedRoundtripValues (context : CborPropertyTestContext) = + Array.map (getExpectedRoundtripValue context.ConvertIndefiniteLengthItems) context.RootDocuments + + /// Identifies & transforms documents that might not be accepted under the supplied conformance mode + let private normalize (mode : CborConformanceMode) (convertIndefiniteLengthItems : bool) (doc : CborDocument) = + // normalization can lead to collisions in conformance modes that require field uniqueness + // mitigate by wrapping with a unique CBOR node + let mutable counter = 19000uL + let createUniqueWrapper (doc : CborDocument) = + counter <- counter + 1uL + Array(true, [|UnsignedInteger counter; doc|]) + + // not accepted by strict & canonical conformance modes + let trimNonCanonicalSimpleValue doc = + match doc with + | SimpleValue value when value >= 24uy && value < 32uy -> createUniqueWrapper(SimpleValue 0uy) + | _ -> doc + + // completely replace indefinite-length nodes in canonical conformance modes + let trimIndefiniteLengthNode doc = + match doc with + | ByteStringIndefiniteLength bss -> createUniqueWrapper(ByteString (Array.concat bss)) + | TextStringIndefiniteLength tss -> createUniqueWrapper(TextString (String.concat "" tss)) + | Array(false, elems) -> createUniqueWrapper(Array(true, elems)) + | Map(false, fields) -> createUniqueWrapper(Map(true, fields)) + | _ -> doc + + // CTAP2 does not allow major type 6, at all + let trimTagNode doc = + match doc with + | Tag (tag, doc) -> Array(true, [| UnsignedInteger (uint64 tag) ; doc|]) + | SemanticValue _ -> createUniqueWrapper(UnsignedInteger 0uL) + | _ -> doc + + match mode with + | CborConformanceMode.Lax -> doc + | CborConformanceMode.Strict + | CborConformanceMode.Canonical -> map (trimNonCanonicalSimpleValue << trimIndefiniteLengthNode) doc + | CborConformanceMode.Ctap2Canonical -> map (trimNonCanonicalSimpleValue << trimIndefiniteLengthNode << trimTagNode) doc + | _ -> doc + + /// Gets the expected value that we would expected to see after a full serialization/deserialization roundtrip + let private getExpectedRoundtripValue (convertIndefiniteLengthItems : bool) (doc : CborDocument) = + if not convertIndefiniteLengthItems then doc else + + let trimIndefiniteLengthNode doc = + match doc with + | ByteStringIndefiniteLength bss -> ByteString (Array.concat bss) + | TextStringIndefiniteLength tss -> TextString (String.concat "" tss) + | Array(false, elems) -> Array(true, elems) + | Map(false, fields) -> Map(true, fields) + | _ -> doc + + map trimIndefiniteLengthNode doc + + /// Depth-first application of update function on a CBOR doc tree + let rec private map (updater : CborDocument -> CborDocument) (doc : CborDocument) = + match updater doc with + | UnsignedInteger _ + | NegativeInteger _ + | ByteString _ + | ByteStringIndefiniteLength _ + | TextString _ + | TextStringIndefiniteLength _ + | SemanticValue _ + | SimpleValue _ + | Double _ as updated -> updated + + | Tag(tag, value) -> Tag(tag, map updater value) + | Array(isDefiniteLength, elems) -> Array(isDefiniteLength, Array.map (map updater) elems) + | Map(isDefiniteLength, fields) -> + let updatedFields = + fields + |> Map.toSeq + |> Seq.map (fun (k,v) -> map updater k, map updater v) + |> Map.ofSeq + + Map(isDefiniteLength, updatedFields) diff --git a/src/libraries/System.Formats.Cbor/tests/CborDocument/System.Formats.Cbor.Tests.DataModel.fsproj b/src/libraries/System.Formats.Cbor/tests/CborDocument/System.Formats.Cbor.Tests.DataModel.fsproj new file mode 100644 index 00000000000000..94a84ccfa46132 --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/CborDocument/System.Formats.Cbor.Tests.DataModel.fsproj @@ -0,0 +1,16 @@ + + + + $(NetCoreAppCurrent) + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.Formats.Cbor/tests/CborRoundtripTests.cs b/src/libraries/System.Formats.Cbor/tests/CborRoundtripTests.cs deleted file mode 100644 index 1e410d77a99aaf..00000000000000 --- a/src/libraries/System.Formats.Cbor/tests/CborRoundtripTests.cs +++ /dev/null @@ -1,180 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Linq; -using Test.Cryptography; -using Xunit; -#if CBOR_PROPERTY_TESTS -using FsCheck.Xunit; -#endif - -namespace System.Formats.Cbor.Tests -{ - public partial class CborRoundtripTests - { - -#if CBOR_PROPERTY_TESTS - private const string ReplaySeed = "(0,0)"; // set a seed for deterministic runs - private const int MaxTests = 10_000; -#endif - -#if CBOR_PROPERTY_TESTS - [Property(Replay = ReplaySeed, MaxTest = MaxTests)] -#else - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(23)] - [InlineData(24)] - [InlineData(25)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(1000000)] - [InlineData(1000000000000)] - [InlineData(-1)] - [InlineData(-10)] - [InlineData(-100)] - [InlineData(-1000)] - [InlineData(byte.MaxValue)] - [InlineData(byte.MaxValue + 1)] - [InlineData(-1 - byte.MaxValue)] - [InlineData(-2 - byte.MaxValue)] - [InlineData(ushort.MaxValue)] - [InlineData(ushort.MaxValue + 1)] - [InlineData(-1 - ushort.MaxValue)] - [InlineData(-2 - ushort.MaxValue)] - [InlineData(uint.MaxValue)] - [InlineData((long)uint.MaxValue + 1)] - [InlineData(-1 - uint.MaxValue)] - [InlineData(-2 - uint.MaxValue)] - [InlineData(long.MinValue)] - [InlineData(long.MaxValue)] -#endif - public static void Roundtrip_Int64(long input) - { - var writer = new CborWriter(); - writer.WriteInt64(input); - byte[] encoding = writer.Encode(); - - var reader = new CborReader(encoding); - long result = reader.ReadInt64(); - Assert.Equal(input, result); - } - -#if CBOR_PROPERTY_TESTS - [Property(Replay = ReplaySeed, MaxTest = MaxTests)] -#else - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(23)] - [InlineData(24)] - [InlineData(25)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(1000000)] - [InlineData(1000000000000)] - [InlineData(byte.MaxValue)] - [InlineData(byte.MaxValue + 1)] - [InlineData(ushort.MaxValue)] - [InlineData(ushort.MaxValue + 1)] - [InlineData(uint.MaxValue)] - [InlineData((long)uint.MaxValue + 1)] - [InlineData(long.MaxValue)] - [InlineData(ulong.MaxValue)] -#endif - public static void Roundtrip_UInt64(ulong input) - { - var writer = new CborWriter(); - writer.WriteUInt64(input); - byte[] encoding = writer.Encode(); - - var reader = new CborReader(encoding); - ulong result = reader.ReadUInt64(); - Assert.Equal(input, result); - } - -#if CBOR_PROPERTY_TESTS - [Property(Replay = ReplaySeed, MaxTest = MaxTests)] - public static void Roundtrip_ByteString(byte[] input) - { -#else - [Theory] - [InlineData("")] - [InlineData("01020304")] - [InlineData("ffffffffffffffffffffffffffff")] - public static void Roundtrip_ByteString(string hexInput) - { - byte[] input = hexInput.HexToByteArray(); -#endif - var writer = new CborWriter(); - writer.WriteByteString(input); - byte[] encoding = writer.Encode(); - - var reader = new CborReader(encoding); - byte[] result = reader.ReadByteString(); - AssertHelper.HexEqual(input ?? Array.Empty(), result); - } - -#if CBOR_PROPERTY_TESTS - [Property(Replay = ReplaySeed, MaxTest = MaxTests)] -#else - [Theory] - [InlineData("")] - [InlineData("a")] - [InlineData("IETF")] - [InlineData("\"\\")] - [InlineData("\u00fc")] - [InlineData("\u6c34")] - [InlineData("\ud800\udd51")] -#endif - public static void Roundtrip_TextString(string input) - { - var writer = new CborWriter(); - writer.WriteTextString(input); - byte[] encoding = writer.Encode(); - - var reader = new CborReader(encoding); - string result = reader.ReadTextString(); - Assert.Equal(input ?? "", result); - } - -#if CBOR_PROPERTY_TESTS - [Property(Replay = ReplaySeed, MaxTest = MaxTests)] - public static void ByteString_Encoding_ShouldContainInputBytes(byte[] input) - { -#else - [Theory] - [InlineData("")] - [InlineData("01020304")] - [InlineData("ffffffffffffffffffffffffffff")] - public static void ByteString_Encoding_ShouldContainInputBytes(string hexInput) - { - byte[] input = hexInput.HexToByteArray(); -#endif - var writer = new CborWriter(); - writer.WriteByteString(input); - byte[] encoding = writer.Encode(); - - int length = input?.Length ?? 0; - int lengthEncodingLength = GetLengthEncodingLength(length); - - Assert.Equal(lengthEncodingLength + length, encoding.Length); - AssertHelper.HexEqual(input ?? Array.Empty(), encoding.Skip(lengthEncodingLength).ToArray()); - - static int GetLengthEncodingLength(int length) - { - return length switch - { - _ when (length < 24) => 1, - _ when (length < byte.MaxValue) => 1 + sizeof(byte), - _ when (length < ushort.MaxValue) => 1 + sizeof(ushort), - _ => 1 + sizeof(uint) - }; - } - } - } -} diff --git a/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborPropertyTests.cs b/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborPropertyTests.cs new file mode 100644 index 00000000000000..6aca08ffc7bc6e --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborPropertyTests.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Cbor.Tests.DataModel; +using System.Linq; +using FsCheck; +using FsCheck.Xunit; +using Xunit; + +namespace System.Formats.Cbor.Tests +{ + public static class CborPropertyTests + { + private const string? ReplaySeed = "(42,42)"; // set a seed for deterministic runs, null for randomized runs + private const int MaxTests = 10_000; + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_Int64(CborConformanceMode mode, long input) + { + var writer = new CborWriter(mode); + writer.WriteInt64(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + long result = reader.ReadInt64(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_UInt64(CborConformanceMode mode, ulong input) + { + var writer = new CborWriter(mode); + writer.WriteUInt64(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + ulong result = reader.ReadUInt64(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_NegativeInteger(CborConformanceMode mode, ulong input) + { + var writer = new CborWriter(mode); + writer.WriteCborNegativeIntegerRepresentation(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + ulong result = reader.ReadCborNegativeIntegerRepresentation(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_ByteString(CborConformanceMode mode, byte[] input) + { + var writer = new CborWriter(mode); + writer.WriteByteString(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + byte[] result = reader.ReadByteString(); + AssertHelper.HexEqual(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_TextString(CborConformanceMode mode, string input) + { + var writer = new CborWriter(mode); + writer.WriteTextString(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + string result = reader.ReadTextString(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_IndefiniteByteString(CborConformanceMode mode, byte[][] chunks) + { + bool convertIndefiniteLengthEncodings = mode is CborConformanceMode.Canonical or CborConformanceMode.Ctap2Canonical; + var writer = new CborWriter(convertIndefiniteLengthEncodings: convertIndefiniteLengthEncodings); + + writer.WriteStartIndefiniteLengthByteString(); + foreach (byte[] chunk in chunks) + { + writer.WriteByteString(chunk); + } + writer.WriteEndIndefiniteLengthByteString(); + + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding); + byte[] expected = chunks.SelectMany(ch => ch).ToArray(); + byte[] result = reader.ReadByteString(); + AssertHelper.HexEqual(expected, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_IndefiniteTextString(CborConformanceMode mode, string[] chunks) + { + bool convertIndefiniteLengthEncodings = mode is CborConformanceMode.Canonical or CborConformanceMode.Ctap2Canonical; + var writer = new CborWriter(convertIndefiniteLengthEncodings: convertIndefiniteLengthEncodings); + + writer.WriteStartIndefiniteLengthTextString(); + foreach (string chunk in chunks) + { + writer.WriteTextString(chunk); + } + writer.WriteEndIndefiniteLengthTextString(); + + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding); + string expected = String.Concat(chunks); + string result = reader.ReadTextString(); + Assert.Equal(expected, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_Half(CborConformanceMode mode, Half input) + { + var writer = new CborWriter(mode); + writer.WriteHalf(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding, mode); + Half result = reader.ReadHalf(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_Double(CborConformanceMode mode, double input) + { + var writer = new CborWriter(); + writer.WriteDouble(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding); + double result = reader.ReadDouble(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void Roundtrip_Decimal(CborConformanceMode mode, decimal input) + { + var writer = new CborWriter(); + writer.WriteDecimal(input); + byte[] encoding = writer.Encode(); + + var reader = new CborReader(encoding); + decimal result = reader.ReadDecimal(); + Assert.Equal(input, result); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void ByteString_Encoding_ShouldContainInputBytes(CborConformanceMode mode, byte[] input) + { + var writer = new CborWriter(mode); + writer.WriteByteString(input); + byte[] encoding = writer.Encode(); + + int length = input?.Length ?? 0; + int lengthEncodingLength = GetLengthEncodingLength(length); + + Assert.Equal(lengthEncodingLength + length, encoding.Length); + AssertHelper.HexEqual(input ?? Array.Empty(), encoding.Skip(lengthEncodingLength).ToArray()); + + static int GetLengthEncodingLength(int length) + { + return length switch + { + _ when (length < 24) => 1, + _ when (length < byte.MaxValue) => 1 + sizeof(byte), + _ when (length < ushort.MaxValue) => 1 + sizeof(ushort), + _ => 1 + sizeof(uint) + }; + } + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void PropertyTest_Roundtrip(CborPropertyTestContext input) + { + byte[] encoding = CborDocumentSerializer.encode(input); + + CborDocument[] expectedResults = CborPropertyTestContextHelper.getExpectedRoundtripValues(input); + CborDocument[] roundtrippedDocuments = CborDocumentSerializer.decode(input, encoding); + Assert.Equal(expectedResults, roundtrippedDocuments); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void PropertyTest_SkipValue(CborPropertyTestContext input) + { + int length = input.RootDocuments.Length; + input.RootDocuments = new[] { CborDocument.NewArray(_isDefiniteLength: true, input.RootDocuments) }; + byte[] encoding = CborDocumentSerializer.encode(input); + + CborReader reader = CborDocumentSerializer.createReader(input, encoding); + reader.ReadStartArray(); + for (int i = 0; i < length; i++) + { + reader.SkipValue(); + } + reader.ReadEndArray(); + Assert.Equal(CborReaderState.Finished, reader.PeekState()); + } + + [Property(Replay = ReplaySeed, MaxTest = MaxTests, Arbitrary = new[] { typeof(CborRandomGenerators) })] + public static void PropertyTest_SkipToParent(CborPropertyTestContext input) + { + input.RootDocuments = new[] { CborDocument.NewArray(_isDefiniteLength: true, input.RootDocuments) }; + byte[] encoding = CborDocumentSerializer.encode(input); + + CborReader reader = CborDocumentSerializer.createReader(input, encoding); + reader.ReadStartArray(); + reader.SkipToParent(); + Assert.Equal(CborReaderState.Finished, reader.PeekState()); + } + } +} diff --git a/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborRandomGenerators.cs b/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborRandomGenerators.cs new file mode 100644 index 00000000000000..2cf77cf39d97f9 --- /dev/null +++ b/src/libraries/System.Formats.Cbor/tests/PropertyTests/CborRandomGenerators.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Formats.Cbor.Tests.DataModel; +using System.Linq; +using FsCheck; + +namespace System.Formats.Cbor.Tests +{ + public static class CborRandomGenerators + { + public static Arbitrary PropertyTestInput() + { + Arbitrary> documentArb = Arb.Default.NonEmptyArray(); + Arbitrary convertArb = Arb.Default.Bool(); + Gen conformanceModes = Gen.Elements( + CborConformanceMode.Lax, + CborConformanceMode.Strict, + CborConformanceMode.Canonical, + CborConformanceMode.Ctap2Canonical); + + Gen inputGen = + from docs in documentArb.Generator + from convert in convertArb.Generator + from mode in conformanceModes + select CborPropertyTestContextHelper.create(mode, convert, docs.Get); + + IEnumerable Shrinker(CborPropertyTestContext input) + { + var nonEmptyArrayInput = NonEmptyArray.NewNonEmptyArray(input.RootDocuments); + + foreach (NonEmptyArray shrunkDoc in documentArb.Shrinker(nonEmptyArrayInput)) + { + yield return CborPropertyTestContextHelper.create(input.ConformanceMode, input.ConvertIndefiniteLengthItems, input.RootDocuments); + } + } + + return Arb.From(inputGen, Shrinker); + } + + // Do not generate null strings and byte arrays + public static Arbitrary String() => Arb.Default.String().Filter(s => s is not null); + public static Arbitrary ByteArray() => Arb.Default.Array().Filter(s => s is not null); + + // forgo NaN value generation in order to simplify equality checks + public static Arbitrary Single() => Arb.Default.Float32().Filter(s => !float.IsNaN(s)); + public static Arbitrary Double() => Arb.Default.Float().Filter(s => !double.IsNaN(s)); + + // FsCheck has no built-in System.Half generator, define one here + public static Arbitrary Half() + { + Arbitrary singleArb = Arb.Default.Float32(); + + Gen generator = + from f in singleArb.Generator + where !float.IsNaN(f) + select (Half)f; + + IEnumerable Shrinker(Half h) + { + foreach (float shrunk in singleArb.Shrinker((float)h)) + { + yield return (Half)shrunk; + } + } + + return Arb.From(generator, Shrinker); + } + } +} diff --git a/src/libraries/System.Formats.Cbor/tests/System.Formats.Cbor.Tests.csproj b/src/libraries/System.Formats.Cbor/tests/System.Formats.Cbor.Tests.csproj index e72dd108eeeb5f..71735ea6c7b821 100644 --- a/src/libraries/System.Formats.Cbor/tests/System.Formats.Cbor.Tests.csproj +++ b/src/libraries/System.Formats.Cbor/tests/System.Formats.Cbor.Tests.csproj @@ -1,8 +1,16 @@ - + $(NetCoreAppCurrent) enable + false + + + $(DefineConstants),CBOR_PROPERTY_TESTS + + CS8002 + + CommonTest\System\Security\Cryptography\ByteUtils.cs @@ -26,10 +34,20 @@ - + + + + + + + + + + +