Skip to content

Commit

Permalink
Add CBOR property-based tests (dotnet#39828)
Browse files Browse the repository at this point in the history
* Add CBOR property-based tests

* address feedback
  • Loading branch information
eiriktsarpalis authored Jul 24, 2020
1 parent 2b2eb5a commit efd5565
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 182 deletions.
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
<TraceEventVersion>2.0.5</TraceEventVersion>
<NewtonsoftJsonVersion>12.0.3</NewtonsoftJsonVersion>
<MoqVersion>4.12.0</MoqVersion>
<FsCheckVersion>2.14.3</FsCheckVersion>
<!-- Docs -->
<MicrosoftPrivateIntellisenseVersion>3.0.0-preview-20200715.1</MicrosoftPrivateIntellisenseVersion>
<!-- ILLink -->
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/System.Formats.Cbor/System.Formats.Cbor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CborDocument, CborDocument>
// 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
Original file line number Diff line number Diff line change
@@ -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<byte>.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<CborDocument> 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<byte[]>()
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<string>()
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<CborDocument * CborDocument> 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 [<Literal>] SemValueGuard : CborTag = LanguagePrimitives.EnumOfValue 50015001uL

let [<Literal>] Null : CborTag = LanguagePrimitives.EnumOfValue 5001uL
let [<Literal>] Bool : CborTag = LanguagePrimitives.EnumOfValue 5002uL
let [<Literal>] Int32 : CborTag = LanguagePrimitives.EnumOfValue 5003uL
let [<Literal>] Int64 : CborTag = LanguagePrimitives.EnumOfValue 5004uL
let [<Literal>] Half : CborTag = LanguagePrimitives.EnumOfValue 5005uL
let [<Literal>] Single : CborTag = LanguagePrimitives.EnumOfValue 5006uL
Original file line number Diff line number Diff line change
@@ -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
[<CLIMutable>]
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<Compile Include="CborDocument.fs" />
<Compile Include="CborPropertyTestContext.fs" />
<Compile Include="CborDocumentSerializer.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\System.Formats.Cbor.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit efd5565

Please sign in to comment.