First pass at data-driven conformance tests

These use the test files from
https://github.com/cloudevents/conformance/tree/format-tests/format,
which is included via a submodule called conformance.

The XML tests are not included in this commit, as the C# XML formatter
has not been reviewed yet.

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2023-04-12 14:40:10 +01:00 committed by Jon Skeet
parent 8887ed21f9
commit 4dd0f93b55
15 changed files with 1656 additions and 11 deletions

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29001.49
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents", "src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj", "{C5DC9F44-7C03-4A70-80EF-7A29696455EB}"
EndProject
@ -35,7 +35,42 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.New
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.SystemTextJson", "src\CloudNative.CloudEvents.SystemTextJson\CloudNative.CloudEvents.SystemTextJson.csproj", "{FACB3EF2-F078-479A-A91C-719894CB66BF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudNative.CloudEvents.Protobuf", "src\CloudNative.CloudEvents.Protobuf\CloudNative.CloudEvents.Protobuf.csproj", "{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.Protobuf", "src\CloudNative.CloudEvents.Protobuf\CloudNative.CloudEvents.Protobuf.csproj", "{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "conformance", "conformance", "{8CCC98B3-1776-49FF-96D6-947A9E5DFB0A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "format", "format", "{A5906FBA-D73A-4A09-8539-CB10D7B586AE}"
ProjectSection(SolutionItems) = preProject
conformance\format\README.md = conformance\format\README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "json", "json", "{D8055631-E6BB-4CD2-8162-F674D6D30E76}"
ProjectSection(SolutionItems) = preProject
conformance\format\json\invalid-batches.json = conformance\format\json\invalid-batches.json
conformance\format\json\invalid-events.json = conformance\format\json\invalid-events.json
conformance\format\json\README.md = conformance\format\json\README.md
conformance\format\json\valid-batches.json = conformance\format\json\valid-batches.json
conformance\format\json\valid-events.json = conformance\format\json\valid-events.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "protobuf", "protobuf", "{119AD438-878B-4383-BC9F-779F1605E711}"
ProjectSection(SolutionItems) = preProject
conformance\format\protobuf\conformance_tests.proto = conformance\format\protobuf\conformance_tests.proto
conformance\format\protobuf\invalid-batches.json = conformance\format\protobuf\invalid-batches.json
conformance\format\protobuf\invalid-events.json = conformance\format\protobuf\invalid-events.json
conformance\format\protobuf\README.md = conformance\format\protobuf\README.md
conformance\format\protobuf\valid-batches.json = conformance\format\protobuf\valid-batches.json
conformance\format\protobuf\valid-events.json = conformance\format\protobuf\valid-events.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "xml", "xml", "{4012C753-68DE-4737-936F-F5DBC485C51B}"
ProjectSection(SolutionItems) = preProject
conformance\format\xml\invalid-batches.xml = conformance\format\xml\invalid-batches.xml
conformance\format\xml\invalid-events.xml = conformance\format\xml\invalid-events.xml
conformance\format\xml\README.md = conformance\format\xml\README.md
conformance\format\xml\valid-batches.xml = conformance\format\xml\valid-batches.xml
conformance\format\xml\valid-events.xml = conformance\format\xml\valid-events.xml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -207,6 +242,12 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A5906FBA-D73A-4A09-8539-CB10D7B586AE} = {8CCC98B3-1776-49FF-96D6-947A9E5DFB0A}
{D8055631-E6BB-4CD2-8162-F674D6D30E76} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
{119AD438-878B-4383-BC9F-779F1605E711} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
{4012C753-68DE-4737-936F-F5DBC485C51B} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F77A454C-CC17-4AD6-823A-64E1A94FDA0A}
EndGlobalSection

View File

@ -63,5 +63,14 @@ $PROTOC \
--csharp_opt=file_extension=.g.cs \
test/CloudNative.CloudEvents.UnitTests/Protobuf/*.proto
# Conformance test protos
$PROTOC \
-I tmp/include \
-I tmp/cloudevents \
-I conformance/format/protobuf \
--csharp_out=test/CloudNative.CloudEvents.UnitTests/Protobuf \
--csharp_opt=file_extension=.g.cs \
conformance/format/protobuf/*.proto
echo "Generated code."
rm -rf tmp

View File

@ -95,7 +95,6 @@ namespace CloudNative.CloudEvents.AspNetCore.UnitTests
var parsed = new JsonEventFormatter().DecodeStructuredModeMessage(content, new ContentType(response.ContentType), extensionAttributes: null);
AssertCloudEventsEqual(cloudEvent, parsed);
Assert.Equal(cloudEvent.Data, parsed.Data);
// We populate headers even though we don't strictly need to; let's validate that.
Assert.Equal("1.0", response.Headers["ce-specversion"]);

View File

@ -0,0 +1,42 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
public static class SampleBatches
{
private static ConcurrentDictionary<string, IReadOnlyList<CloudEvent>> batchesById = new ConcurrentDictionary<string, IReadOnlyList<CloudEvent>>();
private static readonly IReadOnlyList<CloudEvent> empty = Register("empty");
private static readonly IReadOnlyList<CloudEvent> minimal = Register("minimal", SampleEvents.Minimal);
private static readonly IReadOnlyList<CloudEvent> minimal2 = Register("minimal2", SampleEvents.Minimal, SampleEvents.Minimal);
private static readonly IReadOnlyList<CloudEvent> minimalAndAllCore = Register("minimalAndAllCore", SampleEvents.Minimal, SampleEvents.AllCore);
private static readonly IReadOnlyList<CloudEvent> minimalAndAllExtensionTypes =
Register("minimalAndAllExtensionTypes", SampleEvents.Minimal, SampleEvents.AllExtensionTypes);
internal static IReadOnlyList<CloudEvent> Empty => Clone(empty);
internal static IReadOnlyList<CloudEvent> Minimal => Clone(minimal);
internal static IReadOnlyList<CloudEvent> Minimal2 => Clone(minimal2);
internal static IReadOnlyList<CloudEvent> MinimalAndAllCore => Clone(minimalAndAllCore);
internal static IReadOnlyList<CloudEvent> MinimalAndAllExtensionTypes => Clone(minimalAndAllExtensionTypes);
internal static IReadOnlyList<CloudEvent> FromId(string id) => batchesById.TryGetValue(id, out var batch)
? Clone(batch)
: throw new ArgumentException($"No such sample batch: '{id}'");
private static IReadOnlyList<CloudEvent> Clone(IReadOnlyList<CloudEvent> cloudEvents) =>
cloudEvents.Select(SampleEvents.Clone).ToList().AsReadOnly();
private static IReadOnlyList<CloudEvent> Register(string id, params CloudEvent[] cloudEvents)
{
var list = new List<CloudEvent>(cloudEvents).AsReadOnly();
batchesById[id] = list;
return list;
}
}

View File

@ -0,0 +1,123 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
internal static class SampleEvents
{
private static ConcurrentDictionary<string, CloudEvent> eventsById = new ConcurrentDictionary<string, CloudEvent>();
private static IReadOnlyList<CloudEventAttribute> allExtensionAttributes = new List<CloudEventAttribute>()
{
CloudEventAttribute.CreateExtension("extinteger", CloudEventAttributeType.Integer),
CloudEventAttribute.CreateExtension("extboolean", CloudEventAttributeType.Boolean),
CloudEventAttribute.CreateExtension("extstring", CloudEventAttributeType.String),
CloudEventAttribute.CreateExtension("exttimestamp", CloudEventAttributeType.Timestamp),
CloudEventAttribute.CreateExtension("exturi", CloudEventAttributeType.Uri),
CloudEventAttribute.CreateExtension("exturiref", CloudEventAttributeType.UriReference),
CloudEventAttribute.CreateExtension("extbinary", CloudEventAttributeType.Binary),
}.AsReadOnly();
private static readonly CloudEvent minimal = new CloudEvent
{
Id = "minimal",
Type = "io.cloudevents.test",
Source = new Uri("https://cloudevents.io")
}.Register();
private static readonly CloudEvent allCore = minimal.With(evt =>
{
evt.Id = "allCore";
evt.DataContentType = "text/plain";
evt.DataSchema = new Uri("https://cloudevents.io/dataschema");
evt.Subject = "tests";
evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero);
}).Register();
private static readonly CloudEvent minimalWithTime = minimal.With(evt =>
{
evt.Id = "minimalWithTime";
evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero);
}).Register();
private static readonly CloudEvent minimalWithRelativeSource = minimal.With(evt =>
{
evt.Id = "minimalWithRelativeSource";
evt.Source = new Uri("#fragment", UriKind.RelativeOrAbsolute);
}).Register();
private static readonly CloudEvent simpleTextData = minimal.With(evt =>
{
evt.Id = "simpleTextData";
evt.Data = "Simple text";
evt.DataContentType = "text/plain";
}).Register();
private static readonly CloudEvent allExtensionTypes = minimal.WithSampleExtensionAttributes().With(evt =>
{
evt.Id = "allExtensionTypes";
evt["extinteger"] = 10;
evt["extboolean"] = true;
evt["extstring"] = "text";
evt["extbinary"] = new byte[] { 77, 97 };
evt["exttimestamp"] = new DateTimeOffset(2023, 3, 31, 15, 12, 0, TimeSpan.Zero);
evt["exturi"] = new Uri("https://cloudevents.io");
evt["exturiref"] = new Uri("//authority/path", UriKind.RelativeOrAbsolute);
}).Register();
internal static CloudEvent Minimal => Clone(minimal);
internal static CloudEvent AllCore => Clone(allCore);
internal static CloudEvent MinimalWithTime => Clone(minimalWithTime);
internal static CloudEvent MinimalWithRelativeSource => Clone(minimalWithRelativeSource);
internal static CloudEvent SimpleTextData => Clone(simpleTextData);
internal static CloudEvent AllExtensionTypes => Clone(allExtensionTypes);
internal static IReadOnlyList<CloudEventAttribute> SampleExtensionAttributes => allExtensionAttributes;
internal static CloudEvent FromId(string id) => eventsById.TryGetValue(id, out var evt)
? Clone(evt)
: throw new ArgumentException($"No such sample event: '{id}'");
// TODO: Make this available somewhere else?
internal static CloudEvent Clone(CloudEvent evt)
{
var clone = new CloudEvent(evt.SpecVersion, evt.ExtensionAttributes);
foreach (var attr in evt.GetPopulatedAttributes())
{
clone[attr.Key] = attr.Value;
}
// TODO: Deep copy where appropriate?
clone.Data = evt.Data;
return clone;
}
private static CloudEvent With(this CloudEvent evt, Action<CloudEvent> action)
{
var clone = Clone(evt);
action(clone);
return clone;
}
/// <summary>
/// Returns a clone of the given CloudEvent, with all attributes in <see cref="allExtensionAttributes"/>
/// registered but without values.
/// </summary>
private static CloudEvent WithSampleExtensionAttributes(this CloudEvent evt) => evt.With(clone =>
{
foreach (var attribute in allExtensionAttributes)
{
clone[attribute] = null;
}
});
private static CloudEvent Register(this CloudEvent evt)
{
eventsById[evt.Id ?? throw new InvalidOperationException("No ID in sample event")] = evt;
return evt;
}
}

View File

@ -0,0 +1,62 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
internal class TestDataProvider
{
private static readonly string ConformanceTestDataRoot = Path.Combine(FindRepoRoot(), "conformance", "format");
public static TestDataProvider Json { get; } = new TestDataProvider("json", "*.json");
public static TestDataProvider Protobuf { get; } = new TestDataProvider("protobuf", "*.json");
public static TestDataProvider Xml { get; } = new TestDataProvider("xml", "*.xml");
private readonly string testDataDirectory;
private readonly string searchPattern;
private TestDataProvider(string relativeDirectory, string searchPattern)
{
testDataDirectory = Path.Combine(ConformanceTestDataRoot, relativeDirectory);
this.searchPattern = searchPattern;
}
public IEnumerable<string> ListTestFiles() => Directory.EnumerateFiles(testDataDirectory, searchPattern);
/// <summary>
/// Loads all tests, assuming multiple tests per file, to be loaded based on textual file content.
/// </summary>
/// <typeparam name="TFile">The deserialized test file type.</typeparam>
/// <typeparam name="TTest">The deserialized test type.</typeparam>
/// <param name="fileParser">A function to parse the content of the file (provided as a string) to a test file.</param>
/// <param name="testExtractor">A function to extract all the tests within the given test file.</param>
public IReadOnlyList<TTest> LoadTests<TFile, TTest>(Func<string, TFile> fileParser, Func<TFile, IEnumerable<TTest>> testExtractor) =>
ListTestFiles()
.Select(file => fileParser(File.ReadAllText(file)))
.SelectMany(testExtractor)
.ToList()
.AsReadOnly();
private static string FindRepoRoot()
{
var currentDirectory = Path.GetFullPath(".");
var directory = new DirectoryInfo(currentDirectory);
while (directory != null &&
(!File.Exists(Path.Combine(directory.FullName, "LICENSE"))
|| !File.Exists(Path.Combine(directory.FullName, "CloudEvents.sln"))))
{
directory = directory.Parent;
}
if (directory == null)
{
throw new Exception("Unable to determine root directory. Please run within the sdk-csharp repository.");
}
return directory.FullName;
}
}

View File

@ -260,7 +260,6 @@ namespace CloudNative.CloudEvents.Http.UnitTests
var parsed = new JsonEventFormatter().DecodeStructuredModeMessage(bytes, MimeUtilities.ToContentType(content.Headers.ContentType), extensionAttributes: null);
AssertCloudEventsEqual(cloudEvent, parsed);
Assert.Equal(cloudEvent.Data, parsed.Data);
// We populate headers even though we don't strictly need to; let's validate that.
Assert.Equal("1.0", response.Headers.GetValues("ce-specversion").Single());

View File

@ -0,0 +1,74 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.UnitTests;
using CloudNative.CloudEvents.UnitTests.ConformanceTestData;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests;
public class ConformanceTest
{
private static readonly IReadOnlyList<JsonConformanceTest> allTests =
TestDataProvider.Json.LoadTests(ConformanceTestFile.FromJson, file => file.Tests);
private static JsonConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id);
private static IEnumerable<object[]> SelectTestIds(ConformanceTestType type) =>
allTests
.Where(test => test.TestType == type)
.Select(test => new object[] { test.Id });
public static IEnumerable<object[]> ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent);
public static IEnumerable<object[]> InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent);
public static IEnumerable<object[]> ValidBatchTestIds => SelectTestIds(ConformanceTestType.ValidBatch);
public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch);
[Theory, MemberData(nameof(ValidEventTestIds))]
public void ValidEvent(string testId)
{
var test = GetTestById(testId);
CloudEvent expected = SampleEvents.FromId(test.SampleId);
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
CloudEvent actual = new JsonEventFormatter().ConvertFromJObject(test.Event, extensions);
TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidEventTestIds))]
public void InvalidEvent(string testId)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
Assert.Throws<ArgumentException>(() => formatter.ConvertFromJObject(test.Event, extensions));
}
[Theory, MemberData(nameof(ValidBatchTestIds))]
public void ValidBatch(string testId)
{
var test = GetTestById(testId);
IReadOnlyList<CloudEvent> expected = SampleBatches.FromId(test.SampleId);
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString();
var body = Encoding.UTF8.GetBytes(json);
IReadOnlyList<CloudEvent> actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions);
TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidBatchTestIds))]
public void InvalidBatch(string testId)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString();
var body = Encoding.UTF8.GetBytes(json);
Assert.Throws<ArgumentException>(() => formatter.DecodeBatchModeMessage(body, contentType: null, extensions));
}
}

View File

@ -0,0 +1,51 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests;
#nullable disable
public class ConformanceTestFile
{
private static readonly JsonSerializerSettings serializerSeettings = new() { DateParseHandling = DateParseHandling.None };
public ConformanceTestType? TestType { get; set; }
public List<JsonConformanceTest> Tests { get; } = new List<JsonConformanceTest>();
public static ConformanceTestFile FromJson(string json)
{
var testFile = JsonConvert.DeserializeObject<ConformanceTestFile>(json, serializerSeettings) ?? throw new InvalidOperationException();
foreach (var test in testFile.Tests)
{
test.TestType ??= testFile.TestType;
}
return testFile;
}
}
public class JsonConformanceTest
{
public string Id { get; set; }
public string Description { get; set; }
public ConformanceTestType? TestType { get; set; }
public string SampleId { get; set; }
public JObject Event { get; set; }
public JArray Batch { get; set; }
public bool RoundTrip { get; set; }
public bool SampleExtensionAttributes { get; set; }
public bool ExtensionConstraints { get; set; }
}
public enum ConformanceTestType
{
ValidSingleEvent,
ValidBatch,
InvalidSingleEvent,
InvalidBatch
}

View File

@ -0,0 +1,69 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.UnitTests;
using CloudNative.CloudEvents.UnitTests.ConformanceTestData;
using Google.Protobuf;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace CloudNative.CloudEvents.Protobuf.UnitTests;
public class Conformance
{
private static readonly IReadOnlyList<ConformanceTest> allTests =
TestDataProvider.Protobuf.LoadTests(ConformanceTestFile.FromJson, file => file.Tests);
private static ConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id);
private static IEnumerable<object[]> SelectTestIds(ConformanceTest.EventOneofCase eventCase) =>
allTests
.Where(test => test.EventCase == eventCase)
.Select(test => new object[] { test.Id });
public static IEnumerable<object[]> ValidEventTestIds => SelectTestIds(ConformanceTest.EventOneofCase.ValidSingle);
public static IEnumerable<object[]> InvalidEventTestIds => SelectTestIds(ConformanceTest.EventOneofCase.InvalidSingle);
public static IEnumerable<object[]> ValidBatchTestIds => SelectTestIds(ConformanceTest.EventOneofCase.ValidBatch);
public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTest.EventOneofCase.InvalidBatch);
[Theory, MemberData(nameof(ValidEventTestIds))]
public void ValidEvent(string testId)
{
var test = GetTestById(testId);
CloudEvent expected = SampleEvents.FromId(test.SampleId);
CloudEvent actual = new ProtobufEventFormatter().ConvertFromProto(test.ValidSingle, null);
TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidEventTestIds))]
public void InvalidEvent(string testId)
{
var test = GetTestById(testId);
var formatter = new ProtobufEventFormatter();
Assert.Throws<ArgumentException>(() => formatter.ConvertFromProto(test.InvalidSingle, null));
}
[Theory, MemberData(nameof(ValidBatchTestIds))]
public void ValidBatch(string testId)
{
var test = GetTestById(testId);
IReadOnlyList<CloudEvent> expected = SampleBatches.FromId(test.SampleId);
// We don't have a convenience method for batches, so serialize batch back to binary.
var body = test.ValidBatch.ToByteArray();
IReadOnlyList<CloudEvent> actual = new ProtobufEventFormatter().DecodeBatchModeMessage(body, null, null);
TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidBatchTestIds))]
public void InvalidBatch(string testId)
{
var test = GetTestById(testId);
var formatter = new ProtobufEventFormatter();
// We don't have a convenience method for batches, so serialize batch back to binary.
var body = test.InvalidBatch.ToByteArray();
Assert.Throws<ArgumentException>(() => formatter.DecodeBatchModeMessage(body, null, null));
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using Google.Protobuf;
using Google.Protobuf.Reflection;
namespace CloudNative.CloudEvents.Protobuf.UnitTests;
public partial class ConformanceTestFile
{
private static readonly JsonParser jsonParser =
new(JsonParser.Settings.Default.WithTypeRegistry(TypeRegistry.FromFiles(ConformanceTestsReflection.Descriptor)));
internal static ConformanceTestFile FromJson(string json) =>
jsonParser.Parse<ConformanceTestFile>(json);
}

View File

@ -0,0 +1,964 @@
// <auto-generated>
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: conformance_tests.proto
// </auto-generated>
#pragma warning disable 1591, 0612, 3021, 8981
#region Designer generated code
using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace CloudNative.CloudEvents.Protobuf.UnitTests {
/// <summary>Holder for reflection information generated from conformance_tests.proto</summary>
public static partial class ConformanceTestsReflection {
#region Descriptor
/// <summary>File descriptor for conformance_tests.proto</summary>
public static pbr::FileDescriptor Descriptor {
get { return descriptor; }
}
private static pbr::FileDescriptor descriptor;
static ConformanceTestsReflection() {
byte[] descriptorData = global::System.Convert.FromBase64String(
string.Concat(
"Chdjb25mb3JtYW5jZV90ZXN0cy5wcm90bxIRaW8uY2xvdWRldmVudHMudjEa",
"EWNsb3VkZXZlbnRzLnByb3RvIkgKE0NvbmZvcm1hbmNlVGVzdEZpbGUSMQoF",
"dGVzdHMYASADKAsyIi5pby5jbG91ZGV2ZW50cy52MS5Db25mb3JtYW5jZVRl",
"c3QitgIKD0NvbmZvcm1hbmNlVGVzdBIKCgJpZBgBIAEoCRITCgtkZXNjcmlw",
"dGlvbhgCIAEoCRIRCglzYW1wbGVfaWQYAyABKAkSNQoMdmFsaWRfc2luZ2xl",
"GAQgASgLMh0uaW8uY2xvdWRldmVudHMudjEuQ2xvdWRFdmVudEgAEjkKC3Zh",
"bGlkX2JhdGNoGAUgASgLMiIuaW8uY2xvdWRldmVudHMudjEuQ2xvdWRFdmVu",
"dEJhdGNoSAASNwoOaW52YWxpZF9zaW5nbGUYBiABKAsyHS5pby5jbG91ZGV2",
"ZW50cy52MS5DbG91ZEV2ZW50SAASOwoNaW52YWxpZF9iYXRjaBgHIAEoCzIi",
"LmlvLmNsb3VkZXZlbnRzLnYxLkNsb3VkRXZlbnRCYXRjaEgAQgcKBWV2ZW50",
"IioKGkNvbmZvcm1hbmNlVGVzdE1lc3NhZ2VEYXRhEgwKBHRleHQYASABKAlC",
"LaoCKkNsb3VkTmF0aXZlLkNsb3VkRXZlbnRzLlByb3RvYnVmLlVuaXRUZXN0",
"c2IGcHJvdG8z"));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::CloudNative.CloudEvents.V1.CloudeventsReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestFile), global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestFile.Parser, new[]{ "Tests" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest), global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest.Parser, new[]{ "Id", "Description", "SampleId", "ValidSingle", "ValidBatch", "InvalidSingle", "InvalidBatch" }, new[]{ "Event" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestMessageData), global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestMessageData.Parser, new[]{ "Text" }, null, null, null, null)
}));
}
#endregion
}
#region Messages
/// <summary>
/// A simple container for conformance tests.
/// </summary>
public sealed partial class ConformanceTestFile : pb::IMessage<ConformanceTestFile>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser<ConformanceTestFile> _parser = new pb::MessageParser<ConformanceTestFile>(() => new ConformanceTestFile());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser<ConformanceTestFile> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestsReflection.Descriptor.MessageTypes[0]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestFile() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestFile(ConformanceTestFile other) : this() {
tests_ = other.tests_.Clone();
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestFile Clone() {
return new ConformanceTestFile(this);
}
/// <summary>Field number for the "tests" field.</summary>
public const int TestsFieldNumber = 1;
private static readonly pb::FieldCodec<global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest> _repeated_tests_codec
= pb::FieldCodec.ForMessage(10, global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest.Parser);
private readonly pbc::RepeatedField<global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest> tests_ = new pbc::RepeatedField<global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest>();
/// <summary>
/// The tests within this file.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public pbc::RepeatedField<global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTest> Tests {
get { return tests_; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
return Equals(other as ConformanceTestFile);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Equals(ConformanceTestFile other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if(!tests_.Equals(other.tests_)) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
hash ^= tests_.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
tests_.WriteTo(output, _repeated_tests_codec);
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
tests_.WriteTo(ref output, _repeated_tests_codec);
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
}
#endif
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
size += tests_.CalculateSize(_repeated_tests_codec);
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(ConformanceTestFile other) {
if (other == null) {
return;
}
tests_.Add(other.tests_);
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(pb::CodedInputStream input) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
input.ReadRawMessage(this);
#else
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
tests_.AddEntriesFrom(input, _repeated_tests_codec);
break;
}
}
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 10: {
tests_.AddEntriesFrom(ref input, _repeated_tests_codec);
break;
}
}
}
}
#endif
}
/// <summary>
/// A single test in the conformance test suite.
/// </summary>
public sealed partial class ConformanceTest : pb::IMessage<ConformanceTest>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser<ConformanceTest> _parser = new pb::MessageParser<ConformanceTest>(() => new ConformanceTest());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser<ConformanceTest> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestsReflection.Descriptor.MessageTypes[1]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTest() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTest(ConformanceTest other) : this() {
id_ = other.id_;
description_ = other.description_;
sampleId_ = other.sampleId_;
switch (other.EventCase) {
case EventOneofCase.ValidSingle:
ValidSingle = other.ValidSingle.Clone();
break;
case EventOneofCase.ValidBatch:
ValidBatch = other.ValidBatch.Clone();
break;
case EventOneofCase.InvalidSingle:
InvalidSingle = other.InvalidSingle.Clone();
break;
case EventOneofCase.InvalidBatch:
InvalidBatch = other.InvalidBatch.Clone();
break;
}
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTest Clone() {
return new ConformanceTest(this);
}
/// <summary>Field number for the "id" field.</summary>
public const int IdFieldNumber = 1;
private string id_ = "";
/// <summary>
/// The ID of the test; must be unique across all protobuf conformance tests.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string Id {
get { return id_; }
set {
id_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
/// <summary>Field number for the "description" field.</summary>
public const int DescriptionFieldNumber = 2;
private string description_ = "";
/// <summary>
/// The description of the test.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string Description {
get { return description_; }
set {
description_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
/// <summary>Field number for the "sample_id" field.</summary>
public const int SampleIdFieldNumber = 3;
private string sampleId_ = "";
/// <summary>
/// For valid tests, the ID of the well-known sample event/batch that
/// this test data should be equivalent to.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string SampleId {
get { return sampleId_; }
set {
sampleId_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
/// <summary>Field number for the "valid_single" field.</summary>
public const int ValidSingleFieldNumber = 4;
/// <summary>
/// A single event that should be converted to an in-memory representation without error.
/// sample_id indicates the sample event that the result should be equivalent to.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::CloudNative.CloudEvents.V1.CloudEvent ValidSingle {
get { return eventCase_ == EventOneofCase.ValidSingle ? (global::CloudNative.CloudEvents.V1.CloudEvent) event_ : null; }
set {
event_ = value;
eventCase_ = value == null ? EventOneofCase.None : EventOneofCase.ValidSingle;
}
}
/// <summary>Field number for the "valid_batch" field.</summary>
public const int ValidBatchFieldNumber = 5;
/// <summary>
/// A batch of events that should be converted to an in-memory representation without error.
/// sample_id indicates the sample batch that the result should be equivalent to.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::CloudNative.CloudEvents.V1.CloudEventBatch ValidBatch {
get { return eventCase_ == EventOneofCase.ValidBatch ? (global::CloudNative.CloudEvents.V1.CloudEventBatch) event_ : null; }
set {
event_ = value;
eventCase_ = value == null ? EventOneofCase.None : EventOneofCase.ValidBatch;
}
}
/// <summary>Field number for the "invalid_single" field.</summary>
public const int InvalidSingleFieldNumber = 6;
/// <summary>
/// A single event that should be rejected when converted to an in-memory representation.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::CloudNative.CloudEvents.V1.CloudEvent InvalidSingle {
get { return eventCase_ == EventOneofCase.InvalidSingle ? (global::CloudNative.CloudEvents.V1.CloudEvent) event_ : null; }
set {
event_ = value;
eventCase_ = value == null ? EventOneofCase.None : EventOneofCase.InvalidSingle;
}
}
/// <summary>Field number for the "invalid_batch" field.</summary>
public const int InvalidBatchFieldNumber = 7;
/// <summary>
/// A batch of events that should be rejected when converted to an in-memory representation.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::CloudNative.CloudEvents.V1.CloudEventBatch InvalidBatch {
get { return eventCase_ == EventOneofCase.InvalidBatch ? (global::CloudNative.CloudEvents.V1.CloudEventBatch) event_ : null; }
set {
event_ = value;
eventCase_ = value == null ? EventOneofCase.None : EventOneofCase.InvalidBatch;
}
}
private object event_;
/// <summary>Enum of possible cases for the "event" oneof.</summary>
public enum EventOneofCase {
None = 0,
ValidSingle = 4,
ValidBatch = 5,
InvalidSingle = 6,
InvalidBatch = 7,
}
private EventOneofCase eventCase_ = EventOneofCase.None;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public EventOneofCase EventCase {
get { return eventCase_; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void ClearEvent() {
eventCase_ = EventOneofCase.None;
event_ = null;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
return Equals(other as ConformanceTest);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Equals(ConformanceTest other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if (Id != other.Id) return false;
if (Description != other.Description) return false;
if (SampleId != other.SampleId) return false;
if (!object.Equals(ValidSingle, other.ValidSingle)) return false;
if (!object.Equals(ValidBatch, other.ValidBatch)) return false;
if (!object.Equals(InvalidSingle, other.InvalidSingle)) return false;
if (!object.Equals(InvalidBatch, other.InvalidBatch)) return false;
if (EventCase != other.EventCase) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
if (Id.Length != 0) hash ^= Id.GetHashCode();
if (Description.Length != 0) hash ^= Description.GetHashCode();
if (SampleId.Length != 0) hash ^= SampleId.GetHashCode();
if (eventCase_ == EventOneofCase.ValidSingle) hash ^= ValidSingle.GetHashCode();
if (eventCase_ == EventOneofCase.ValidBatch) hash ^= ValidBatch.GetHashCode();
if (eventCase_ == EventOneofCase.InvalidSingle) hash ^= InvalidSingle.GetHashCode();
if (eventCase_ == EventOneofCase.InvalidBatch) hash ^= InvalidBatch.GetHashCode();
hash ^= (int) eventCase_;
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
if (Id.Length != 0) {
output.WriteRawTag(10);
output.WriteString(Id);
}
if (Description.Length != 0) {
output.WriteRawTag(18);
output.WriteString(Description);
}
if (SampleId.Length != 0) {
output.WriteRawTag(26);
output.WriteString(SampleId);
}
if (eventCase_ == EventOneofCase.ValidSingle) {
output.WriteRawTag(34);
output.WriteMessage(ValidSingle);
}
if (eventCase_ == EventOneofCase.ValidBatch) {
output.WriteRawTag(42);
output.WriteMessage(ValidBatch);
}
if (eventCase_ == EventOneofCase.InvalidSingle) {
output.WriteRawTag(50);
output.WriteMessage(InvalidSingle);
}
if (eventCase_ == EventOneofCase.InvalidBatch) {
output.WriteRawTag(58);
output.WriteMessage(InvalidBatch);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
if (Id.Length != 0) {
output.WriteRawTag(10);
output.WriteString(Id);
}
if (Description.Length != 0) {
output.WriteRawTag(18);
output.WriteString(Description);
}
if (SampleId.Length != 0) {
output.WriteRawTag(26);
output.WriteString(SampleId);
}
if (eventCase_ == EventOneofCase.ValidSingle) {
output.WriteRawTag(34);
output.WriteMessage(ValidSingle);
}
if (eventCase_ == EventOneofCase.ValidBatch) {
output.WriteRawTag(42);
output.WriteMessage(ValidBatch);
}
if (eventCase_ == EventOneofCase.InvalidSingle) {
output.WriteRawTag(50);
output.WriteMessage(InvalidSingle);
}
if (eventCase_ == EventOneofCase.InvalidBatch) {
output.WriteRawTag(58);
output.WriteMessage(InvalidBatch);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
}
#endif
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
if (Id.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(Id);
}
if (Description.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(Description);
}
if (SampleId.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(SampleId);
}
if (eventCase_ == EventOneofCase.ValidSingle) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ValidSingle);
}
if (eventCase_ == EventOneofCase.ValidBatch) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ValidBatch);
}
if (eventCase_ == EventOneofCase.InvalidSingle) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(InvalidSingle);
}
if (eventCase_ == EventOneofCase.InvalidBatch) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(InvalidBatch);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(ConformanceTest other) {
if (other == null) {
return;
}
if (other.Id.Length != 0) {
Id = other.Id;
}
if (other.Description.Length != 0) {
Description = other.Description;
}
if (other.SampleId.Length != 0) {
SampleId = other.SampleId;
}
switch (other.EventCase) {
case EventOneofCase.ValidSingle:
if (ValidSingle == null) {
ValidSingle = new global::CloudNative.CloudEvents.V1.CloudEvent();
}
ValidSingle.MergeFrom(other.ValidSingle);
break;
case EventOneofCase.ValidBatch:
if (ValidBatch == null) {
ValidBatch = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
}
ValidBatch.MergeFrom(other.ValidBatch);
break;
case EventOneofCase.InvalidSingle:
if (InvalidSingle == null) {
InvalidSingle = new global::CloudNative.CloudEvents.V1.CloudEvent();
}
InvalidSingle.MergeFrom(other.InvalidSingle);
break;
case EventOneofCase.InvalidBatch:
if (InvalidBatch == null) {
InvalidBatch = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
}
InvalidBatch.MergeFrom(other.InvalidBatch);
break;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(pb::CodedInputStream input) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
input.ReadRawMessage(this);
#else
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
Id = input.ReadString();
break;
}
case 18: {
Description = input.ReadString();
break;
}
case 26: {
SampleId = input.ReadString();
break;
}
case 34: {
global::CloudNative.CloudEvents.V1.CloudEvent subBuilder = new global::CloudNative.CloudEvents.V1.CloudEvent();
if (eventCase_ == EventOneofCase.ValidSingle) {
subBuilder.MergeFrom(ValidSingle);
}
input.ReadMessage(subBuilder);
ValidSingle = subBuilder;
break;
}
case 42: {
global::CloudNative.CloudEvents.V1.CloudEventBatch subBuilder = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
if (eventCase_ == EventOneofCase.ValidBatch) {
subBuilder.MergeFrom(ValidBatch);
}
input.ReadMessage(subBuilder);
ValidBatch = subBuilder;
break;
}
case 50: {
global::CloudNative.CloudEvents.V1.CloudEvent subBuilder = new global::CloudNative.CloudEvents.V1.CloudEvent();
if (eventCase_ == EventOneofCase.InvalidSingle) {
subBuilder.MergeFrom(InvalidSingle);
}
input.ReadMessage(subBuilder);
InvalidSingle = subBuilder;
break;
}
case 58: {
global::CloudNative.CloudEvents.V1.CloudEventBatch subBuilder = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
if (eventCase_ == EventOneofCase.InvalidBatch) {
subBuilder.MergeFrom(InvalidBatch);
}
input.ReadMessage(subBuilder);
InvalidBatch = subBuilder;
break;
}
}
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 10: {
Id = input.ReadString();
break;
}
case 18: {
Description = input.ReadString();
break;
}
case 26: {
SampleId = input.ReadString();
break;
}
case 34: {
global::CloudNative.CloudEvents.V1.CloudEvent subBuilder = new global::CloudNative.CloudEvents.V1.CloudEvent();
if (eventCase_ == EventOneofCase.ValidSingle) {
subBuilder.MergeFrom(ValidSingle);
}
input.ReadMessage(subBuilder);
ValidSingle = subBuilder;
break;
}
case 42: {
global::CloudNative.CloudEvents.V1.CloudEventBatch subBuilder = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
if (eventCase_ == EventOneofCase.ValidBatch) {
subBuilder.MergeFrom(ValidBatch);
}
input.ReadMessage(subBuilder);
ValidBatch = subBuilder;
break;
}
case 50: {
global::CloudNative.CloudEvents.V1.CloudEvent subBuilder = new global::CloudNative.CloudEvents.V1.CloudEvent();
if (eventCase_ == EventOneofCase.InvalidSingle) {
subBuilder.MergeFrom(InvalidSingle);
}
input.ReadMessage(subBuilder);
InvalidSingle = subBuilder;
break;
}
case 58: {
global::CloudNative.CloudEvents.V1.CloudEventBatch subBuilder = new global::CloudNative.CloudEvents.V1.CloudEventBatch();
if (eventCase_ == EventOneofCase.InvalidBatch) {
subBuilder.MergeFrom(InvalidBatch);
}
input.ReadMessage(subBuilder);
InvalidBatch = subBuilder;
break;
}
}
}
}
#endif
}
/// <summary>
/// A sample message for tests using CloudEvent.proto_data.
/// </summary>
public sealed partial class ConformanceTestMessageData : pb::IMessage<ConformanceTestMessageData>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser<ConformanceTestMessageData> _parser = new pb::MessageParser<ConformanceTestMessageData>(() => new ConformanceTestMessageData());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser<ConformanceTestMessageData> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::CloudNative.CloudEvents.Protobuf.UnitTests.ConformanceTestsReflection.Descriptor.MessageTypes[2]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestMessageData() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestMessageData(ConformanceTestMessageData other) : this() {
text_ = other.text_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public ConformanceTestMessageData Clone() {
return new ConformanceTestMessageData(this);
}
/// <summary>Field number for the "text" field.</summary>
public const int TextFieldNumber = 1;
private string text_ = "";
/// <summary>
/// Just some text data.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string Text {
get { return text_; }
set {
text_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
return Equals(other as ConformanceTestMessageData);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Equals(ConformanceTestMessageData other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if (Text != other.Text) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
if (Text.Length != 0) hash ^= Text.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
if (Text.Length != 0) {
output.WriteRawTag(10);
output.WriteString(Text);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
if (Text.Length != 0) {
output.WriteRawTag(10);
output.WriteString(Text);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
}
#endif
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
if (Text.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(Text);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(ConformanceTestMessageData other) {
if (other == null) {
return;
}
if (other.Text.Length != 0) {
Text = other.Text;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(pb::CodedInputStream input) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
input.ReadRawMessage(this);
#else
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
Text = input.ReadString();
break;
}
}
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 10: {
Text = input.ReadString();
break;
}
}
}
}
#endif
}
#endregion
}
#endregion Designer generated code

View File

@ -0,0 +1,78 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.UnitTests;
using CloudNative.CloudEvents.UnitTests.ConformanceTestData;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
namespace CloudNative.CloudEvents.SystemTextJson.UnitTests;
public class ConformanceTest
{
private static readonly IReadOnlyList<JsonConformanceTest> allTests =
TestDataProvider.Json.LoadTests(ConformanceTestFile.FromJson, file => file.Tests);
private static JsonConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id);
private static IEnumerable<object[]> SelectTestIds(ConformanceTestType type) =>
allTests
.Where(test => test.TestType == type)
.Select(test => new object[] { test.Id });
public static IEnumerable<object[]> ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent);
public static IEnumerable<object[]> InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent);
public static IEnumerable<object[]> ValidBatchTestIds => SelectTestIds(ConformanceTestType.ValidBatch);
public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch);
[Theory, MemberData(nameof(ValidEventTestIds))]
public void ValidEvent(string testId)
{
var test = GetTestById(testId);
CloudEvent expected = SampleEvents.FromId(test.SampleId);
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
CloudEvent actual = new JsonEventFormatter().ConvertFromJsonElement(test.Event, extensions);
TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidEventTestIds))]
public void InvalidEvent(string testId)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// Hmm... we throw FormatException in some cases, when ArgumentException would be better.
// Changing that would be "somewhat breaking"... it's unclear how much we should worry.
Assert.ThrowsAny<Exception>(() => formatter.ConvertFromJsonElement(test.Event, extensions));
}
[Theory, MemberData(nameof(ValidBatchTestIds))]
public void ValidBatch(string testId)
{
var test = GetTestById(testId);
IReadOnlyList<CloudEvent> expected = SampleBatches.FromId(test.SampleId);
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString();
var body = Encoding.UTF8.GetBytes(json);
IReadOnlyList<CloudEvent> actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions);
TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
}
[Theory, MemberData(nameof(InvalidBatchTestIds))]
public void InvalidBatch(string testId)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString();
var body = Encoding.UTF8.GetBytes(json);
// Hmm... we throw FormatException in some cases, when ArgumentException would be better.
// Changing that would be "somewhat breaking"... it's unclear how much we should worry.
Assert.ThrowsAny<Exception>(() => formatter.DecodeBatchModeMessage(body, contentType: null, extensions));
}
}

View File

@ -0,0 +1,64 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace CloudNative.CloudEvents.SystemTextJson.UnitTests;
#nullable disable
public class ConformanceTestFile
{
private static readonly JsonSerializerOptions serializerOptions = new() { Converters = { new JsonStringEnumConverter() } };
[JsonPropertyName("testType")]
public ConformanceTestType? TestType { get; set; }
// Note: we need a setter here; System.Text.Json doesn't support adding to an existing collection.
// See https://github.com/dotnet/runtime/issues/30258
[JsonPropertyName("tests")]
public List<JsonConformanceTest> Tests { get; set; } = new List<JsonConformanceTest>();
public static ConformanceTestFile FromJson(string json)
{
var testFile = JsonSerializer.Deserialize<ConformanceTestFile>(json, serializerOptions) ?? throw new InvalidOperationException();
foreach (var test in testFile.Tests)
{
test.TestType ??= testFile.TestType;
}
return testFile;
}
}
public class JsonConformanceTest
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("testType")]
public ConformanceTestType? TestType { get; set; }
[JsonPropertyName("sampleId")]
public string SampleId { get; set; }
[JsonPropertyName("event")]
public JsonElement Event { get; set; }
[JsonPropertyName("batch")]
public JsonArray Batch { get; set; }
[JsonPropertyName("sampleExtensionAttributes")]
public bool SampleExtensionAttributes { get; set; }
[JsonPropertyName("extensionConstraints")]
public bool ExtensionConstraints { get; set; }
}
public enum ConformanceTestType
{
ValidSingleEvent,
ValidBatch,
InvalidSingleEvent,
InvalidBatch
}

View File

@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Xunit;
@ -15,6 +17,9 @@ namespace CloudNative.CloudEvents.UnitTests
/// </summary>
internal static class TestHelpers
{
internal static IEqualityComparer<DateTimeOffset> InstantOnlyTimestampComparer => EqualityComparer<DateTimeOffset>.Default;
internal static IEqualityComparer<DateTimeOffset> StrictTimestampComparer => StrictTimestampComparerImpl.Instance;
internal static CloudEventAttribute[] EmptyExtensionArray { get; } = new CloudEventAttribute[0];
internal static IEnumerable<CloudEventAttribute> EmptyExtensionSequence { get; } = new List<CloudEventAttribute>().AsReadOnly();
@ -44,7 +49,7 @@ namespace CloudNative.CloudEvents.UnitTests
/// <summary>
/// The base64 representation of <see cref="SampleBinaryData"/>.
/// </summary>
internal static string SampleBinaryDataBase64 { get; } = Convert.ToBase64String(SampleBinaryData);
internal static string SampleBinaryDataBase64 { get; } = Convert.ToBase64String(SampleBinaryData); // AQID
/// <summary>
/// Arbitrary timestamp to be used for testing.
@ -154,8 +159,12 @@ namespace CloudNative.CloudEvents.UnitTests
}
// TODO: Use this more widely
internal static void AssertCloudEventsEqual(CloudEvent expected, CloudEvent actual)
// TODO: Document handling of timestamps, and potentially parameterize it.
internal static void AssertCloudEventsEqual(CloudEvent expected, CloudEvent actual,
IEqualityComparer<DateTimeOffset>? timestampComparer = null,
IEqualityComparer<object?>? dataComparer = null)
{
timestampComparer ??= StrictTimestampComparer;
Assert.Equal(expected.SpecVersion, actual.SpecVersion);
var expectedAttributes = expected.GetPopulatedAttributes().ToList();
var actualAttributes = actual.GetPopulatedAttributes().ToList();
@ -166,18 +175,62 @@ namespace CloudNative.CloudEvents.UnitTests
var actualAttribute = actualAttributes.FirstOrDefault(actual => actual.Key.Name == expectedAttribute.Key.Name);
Assert.NotNull(actualAttribute.Key);
Assert.Equal(actualAttribute.Key.Type, expectedAttribute.Key.Type);
Assert.Equal(actualAttribute.Value, expectedAttribute.Value);
Assert.Equal(expectedAttribute.Key.Type, actualAttribute.Key.Type);
if (expectedAttribute.Value is DateTimeOffset expectedDto &&
actualAttribute.Value is DateTimeOffset actualDto)
{
Assert.Equal(expectedDto, actualDto, timestampComparer);
}
else
{
Assert.Equal(expectedAttribute.Value, actualAttribute.Value);
}
}
Assert.Equal(expected.Data, actual.Data, dataComparer ?? EqualityComparer<object?>.Default);
}
internal static void AssertBatchesEqual(IReadOnlyList<CloudEvent> expectedBatch, IReadOnlyList<CloudEvent> actualBatch)
internal static void AssertBatchesEqual(IReadOnlyList<CloudEvent> expectedBatch, IReadOnlyList<CloudEvent> actualBatch,
IEqualityComparer<DateTimeOffset>? timestampComparer = null,
IEqualityComparer<object?>? dataComparer = null)
{
Assert.Equal(expectedBatch.Count, actualBatch.Count);
foreach (var pair in expectedBatch.Zip(actualBatch, (x, y) => (x, y)))
{
AssertCloudEventsEqual(pair.x, pair.y);
AssertCloudEventsEqual(pair.x, pair.y, timestampComparer, dataComparer);
}
}
/// <summary>
/// Loads the resource with the given name, copying it into a MemoryStream.
/// (That's often easier to work with when debugging.)
/// </summary>
internal static MemoryStream LoadResource(string resource)
{
using var stream = typeof(TestHelpers).Assembly.GetManifestResourceStream(resource);
if (stream is null)
{
throw new ArgumentException($"Resource {resource} is missing. Known resources: {string.Join(", ", typeof(TestHelpers).Assembly.GetManifestResourceNames())}");
}
var output = new MemoryStream();
stream.CopyTo(output);
output.Position = 0;
return output;
}
private class StrictTimestampComparerImpl : IEqualityComparer<DateTimeOffset>
{
internal static StrictTimestampComparerImpl Instance { get; } = new StrictTimestampComparerImpl();
private StrictTimestampComparerImpl()
{
}
public bool Equals(DateTimeOffset x, DateTimeOffset y) =>
x.UtcDateTime == y.UtcDateTime &&
x.Offset == y.Offset;
public int GetHashCode([DisallowNull] DateTimeOffset obj) =>
obj.UtcDateTime.GetHashCode() ^ obj.Offset.GetHashCode();
}
}
}