Skip to content

Commit 4dd0f93

Browse files
committed
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 <[email protected]>
1 parent 8887ed2 commit 4dd0f93

File tree

15 files changed

+1656
-11
lines changed

15 files changed

+1656
-11
lines changed

CloudEvents.sln

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.29001.49
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.32112.339
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents", "src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj", "{C5DC9F44-7C03-4A70-80EF-7A29696455EB}"
77
EndProject
@@ -35,7 +35,42 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.New
3535
EndProject
3636
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.SystemTextJson", "src\CloudNative.CloudEvents.SystemTextJson\CloudNative.CloudEvents.SystemTextJson.csproj", "{FACB3EF2-F078-479A-A91C-719894CB66BF}"
3737
EndProject
38-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudNative.CloudEvents.Protobuf", "src\CloudNative.CloudEvents.Protobuf\CloudNative.CloudEvents.Protobuf.csproj", "{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}"
38+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.Protobuf", "src\CloudNative.CloudEvents.Protobuf\CloudNative.CloudEvents.Protobuf.csproj", "{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}"
39+
EndProject
40+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "conformance", "conformance", "{8CCC98B3-1776-49FF-96D6-947A9E5DFB0A}"
41+
EndProject
42+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "format", "format", "{A5906FBA-D73A-4A09-8539-CB10D7B586AE}"
43+
ProjectSection(SolutionItems) = preProject
44+
conformance\format\README.md = conformance\format\README.md
45+
EndProjectSection
46+
EndProject
47+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "json", "json", "{D8055631-E6BB-4CD2-8162-F674D6D30E76}"
48+
ProjectSection(SolutionItems) = preProject
49+
conformance\format\json\invalid-batches.json = conformance\format\json\invalid-batches.json
50+
conformance\format\json\invalid-events.json = conformance\format\json\invalid-events.json
51+
conformance\format\json\README.md = conformance\format\json\README.md
52+
conformance\format\json\valid-batches.json = conformance\format\json\valid-batches.json
53+
conformance\format\json\valid-events.json = conformance\format\json\valid-events.json
54+
EndProjectSection
55+
EndProject
56+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "protobuf", "protobuf", "{119AD438-878B-4383-BC9F-779F1605E711}"
57+
ProjectSection(SolutionItems) = preProject
58+
conformance\format\protobuf\conformance_tests.proto = conformance\format\protobuf\conformance_tests.proto
59+
conformance\format\protobuf\invalid-batches.json = conformance\format\protobuf\invalid-batches.json
60+
conformance\format\protobuf\invalid-events.json = conformance\format\protobuf\invalid-events.json
61+
conformance\format\protobuf\README.md = conformance\format\protobuf\README.md
62+
conformance\format\protobuf\valid-batches.json = conformance\format\protobuf\valid-batches.json
63+
conformance\format\protobuf\valid-events.json = conformance\format\protobuf\valid-events.json
64+
EndProjectSection
65+
EndProject
66+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "xml", "xml", "{4012C753-68DE-4737-936F-F5DBC485C51B}"
67+
ProjectSection(SolutionItems) = preProject
68+
conformance\format\xml\invalid-batches.xml = conformance\format\xml\invalid-batches.xml
69+
conformance\format\xml\invalid-events.xml = conformance\format\xml\invalid-events.xml
70+
conformance\format\xml\README.md = conformance\format\xml\README.md
71+
conformance\format\xml\valid-batches.xml = conformance\format\xml\valid-batches.xml
72+
conformance\format\xml\valid-events.xml = conformance\format\xml\valid-events.xml
73+
EndProjectSection
3974
EndProject
4075
Global
4176
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -207,6 +242,12 @@ Global
207242
GlobalSection(SolutionProperties) = preSolution
208243
HideSolutionNode = FALSE
209244
EndGlobalSection
245+
GlobalSection(NestedProjects) = preSolution
246+
{A5906FBA-D73A-4A09-8539-CB10D7B586AE} = {8CCC98B3-1776-49FF-96D6-947A9E5DFB0A}
247+
{D8055631-E6BB-4CD2-8162-F674D6D30E76} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
248+
{119AD438-878B-4383-BC9F-779F1605E711} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
249+
{4012C753-68DE-4737-936F-F5DBC485C51B} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
250+
EndGlobalSection
210251
GlobalSection(ExtensibilityGlobals) = postSolution
211252
SolutionGuid = {F77A454C-CC17-4AD6-823A-64E1A94FDA0A}
212253
EndGlobalSection

generate_protos.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,14 @@ $PROTOC \
6363
--csharp_opt=file_extension=.g.cs \
6464
test/CloudNative.CloudEvents.UnitTests/Protobuf/*.proto
6565

66+
# Conformance test protos
67+
$PROTOC \
68+
-I tmp/include \
69+
-I tmp/cloudevents \
70+
-I conformance/format/protobuf \
71+
--csharp_out=test/CloudNative.CloudEvents.UnitTests/Protobuf \
72+
--csharp_opt=file_extension=.g.cs \
73+
conformance/format/protobuf/*.proto
74+
6675
echo "Generated code."
6776
rm -rf tmp

test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpResponseExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ public async Task CopyToHttpResponseAsync_StructuredMode()
9595

9696
var parsed = new JsonEventFormatter().DecodeStructuredModeMessage(content, new ContentType(response.ContentType), extensionAttributes: null);
9797
AssertCloudEventsEqual(cloudEvent, parsed);
98-
Assert.Equal(cloudEvent.Data, parsed.Data);
9998

10099
// We populate headers even though we don't strictly need to; let's validate that.
101100
Assert.Equal("1.0", response.Headers["ce-specversion"]);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2023 Cloud Native Foundation.
2+
// Licensed under the Apache 2.0 license.
3+
// See LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
10+
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
11+
12+
public static class SampleBatches
13+
{
14+
private static ConcurrentDictionary<string, IReadOnlyList<CloudEvent>> batchesById = new ConcurrentDictionary<string, IReadOnlyList<CloudEvent>>();
15+
16+
private static readonly IReadOnlyList<CloudEvent> empty = Register("empty");
17+
private static readonly IReadOnlyList<CloudEvent> minimal = Register("minimal", SampleEvents.Minimal);
18+
private static readonly IReadOnlyList<CloudEvent> minimal2 = Register("minimal2", SampleEvents.Minimal, SampleEvents.Minimal);
19+
private static readonly IReadOnlyList<CloudEvent> minimalAndAllCore = Register("minimalAndAllCore", SampleEvents.Minimal, SampleEvents.AllCore);
20+
private static readonly IReadOnlyList<CloudEvent> minimalAndAllExtensionTypes =
21+
Register("minimalAndAllExtensionTypes", SampleEvents.Minimal, SampleEvents.AllExtensionTypes);
22+
23+
internal static IReadOnlyList<CloudEvent> Empty => Clone(empty);
24+
internal static IReadOnlyList<CloudEvent> Minimal => Clone(minimal);
25+
internal static IReadOnlyList<CloudEvent> Minimal2 => Clone(minimal2);
26+
internal static IReadOnlyList<CloudEvent> MinimalAndAllCore => Clone(minimalAndAllCore);
27+
internal static IReadOnlyList<CloudEvent> MinimalAndAllExtensionTypes => Clone(minimalAndAllExtensionTypes);
28+
29+
internal static IReadOnlyList<CloudEvent> FromId(string id) => batchesById.TryGetValue(id, out var batch)
30+
? Clone(batch)
31+
: throw new ArgumentException($"No such sample batch: '{id}'");
32+
33+
private static IReadOnlyList<CloudEvent> Clone(IReadOnlyList<CloudEvent> cloudEvents) =>
34+
cloudEvents.Select(SampleEvents.Clone).ToList().AsReadOnly();
35+
36+
private static IReadOnlyList<CloudEvent> Register(string id, params CloudEvent[] cloudEvents)
37+
{
38+
var list = new List<CloudEvent>(cloudEvents).AsReadOnly();
39+
batchesById[id] = list;
40+
return list;
41+
}
42+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2023 Cloud Native Foundation.
2+
// Licensed under the Apache 2.0 license.
3+
// See LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
9+
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
10+
11+
internal static class SampleEvents
12+
{
13+
private static ConcurrentDictionary<string, CloudEvent> eventsById = new ConcurrentDictionary<string, CloudEvent>();
14+
15+
private static IReadOnlyList<CloudEventAttribute> allExtensionAttributes = new List<CloudEventAttribute>()
16+
{
17+
CloudEventAttribute.CreateExtension("extinteger", CloudEventAttributeType.Integer),
18+
CloudEventAttribute.CreateExtension("extboolean", CloudEventAttributeType.Boolean),
19+
CloudEventAttribute.CreateExtension("extstring", CloudEventAttributeType.String),
20+
CloudEventAttribute.CreateExtension("exttimestamp", CloudEventAttributeType.Timestamp),
21+
CloudEventAttribute.CreateExtension("exturi", CloudEventAttributeType.Uri),
22+
CloudEventAttribute.CreateExtension("exturiref", CloudEventAttributeType.UriReference),
23+
CloudEventAttribute.CreateExtension("extbinary", CloudEventAttributeType.Binary),
24+
}.AsReadOnly();
25+
26+
private static readonly CloudEvent minimal = new CloudEvent
27+
{
28+
Id = "minimal",
29+
Type = "io.cloudevents.test",
30+
Source = new Uri("https://cloudevents.io")
31+
}.Register();
32+
33+
private static readonly CloudEvent allCore = minimal.With(evt =>
34+
{
35+
evt.Id = "allCore";
36+
evt.DataContentType = "text/plain";
37+
evt.DataSchema = new Uri("https://cloudevents.io/dataschema");
38+
evt.Subject = "tests";
39+
evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero);
40+
}).Register();
41+
42+
private static readonly CloudEvent minimalWithTime = minimal.With(evt =>
43+
{
44+
evt.Id = "minimalWithTime";
45+
evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero);
46+
}).Register();
47+
48+
private static readonly CloudEvent minimalWithRelativeSource = minimal.With(evt =>
49+
{
50+
evt.Id = "minimalWithRelativeSource";
51+
evt.Source = new Uri("#fragment", UriKind.RelativeOrAbsolute);
52+
}).Register();
53+
54+
private static readonly CloudEvent simpleTextData = minimal.With(evt =>
55+
{
56+
evt.Id = "simpleTextData";
57+
evt.Data = "Simple text";
58+
evt.DataContentType = "text/plain";
59+
}).Register();
60+
61+
private static readonly CloudEvent allExtensionTypes = minimal.WithSampleExtensionAttributes().With(evt =>
62+
{
63+
evt.Id = "allExtensionTypes";
64+
65+
evt["extinteger"] = 10;
66+
evt["extboolean"] = true;
67+
evt["extstring"] = "text";
68+
evt["extbinary"] = new byte[] { 77, 97 };
69+
evt["exttimestamp"] = new DateTimeOffset(2023, 3, 31, 15, 12, 0, TimeSpan.Zero);
70+
evt["exturi"] = new Uri("https://cloudevents.io");
71+
evt["exturiref"] = new Uri("//authority/path", UriKind.RelativeOrAbsolute);
72+
}).Register();
73+
74+
internal static CloudEvent Minimal => Clone(minimal);
75+
internal static CloudEvent AllCore => Clone(allCore);
76+
internal static CloudEvent MinimalWithTime => Clone(minimalWithTime);
77+
internal static CloudEvent MinimalWithRelativeSource => Clone(minimalWithRelativeSource);
78+
internal static CloudEvent SimpleTextData => Clone(simpleTextData);
79+
internal static CloudEvent AllExtensionTypes => Clone(allExtensionTypes);
80+
internal static IReadOnlyList<CloudEventAttribute> SampleExtensionAttributes => allExtensionAttributes;
81+
82+
internal static CloudEvent FromId(string id) => eventsById.TryGetValue(id, out var evt)
83+
? Clone(evt)
84+
: throw new ArgumentException($"No such sample event: '{id}'");
85+
86+
// TODO: Make this available somewhere else?
87+
internal static CloudEvent Clone(CloudEvent evt)
88+
{
89+
var clone = new CloudEvent(evt.SpecVersion, evt.ExtensionAttributes);
90+
foreach (var attr in evt.GetPopulatedAttributes())
91+
{
92+
clone[attr.Key] = attr.Value;
93+
}
94+
// TODO: Deep copy where appropriate?
95+
clone.Data = evt.Data;
96+
return clone;
97+
}
98+
99+
private static CloudEvent With(this CloudEvent evt, Action<CloudEvent> action)
100+
{
101+
var clone = Clone(evt);
102+
action(clone);
103+
return clone;
104+
}
105+
106+
/// <summary>
107+
/// Returns a clone of the given CloudEvent, with all attributes in <see cref="allExtensionAttributes"/>
108+
/// registered but without values.
109+
/// </summary>
110+
private static CloudEvent WithSampleExtensionAttributes(this CloudEvent evt) => evt.With(clone =>
111+
{
112+
foreach (var attribute in allExtensionAttributes)
113+
{
114+
clone[attribute] = null;
115+
}
116+
});
117+
118+
private static CloudEvent Register(this CloudEvent evt)
119+
{
120+
eventsById[evt.Id ?? throw new InvalidOperationException("No ID in sample event")] = evt;
121+
return evt;
122+
}
123+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2023 Cloud Native Foundation.
2+
// Licensed under the Apache 2.0 license.
3+
// See LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Linq;
9+
10+
namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData;
11+
12+
internal class TestDataProvider
13+
{
14+
private static readonly string ConformanceTestDataRoot = Path.Combine(FindRepoRoot(), "conformance", "format");
15+
16+
public static TestDataProvider Json { get; } = new TestDataProvider("json", "*.json");
17+
public static TestDataProvider Protobuf { get; } = new TestDataProvider("protobuf", "*.json");
18+
public static TestDataProvider Xml { get; } = new TestDataProvider("xml", "*.xml");
19+
20+
21+
private readonly string testDataDirectory;
22+
private readonly string searchPattern;
23+
24+
private TestDataProvider(string relativeDirectory, string searchPattern)
25+
{
26+
testDataDirectory = Path.Combine(ConformanceTestDataRoot, relativeDirectory);
27+
this.searchPattern = searchPattern;
28+
}
29+
30+
public IEnumerable<string> ListTestFiles() => Directory.EnumerateFiles(testDataDirectory, searchPattern);
31+
32+
/// <summary>
33+
/// Loads all tests, assuming multiple tests per file, to be loaded based on textual file content.
34+
/// </summary>
35+
/// <typeparam name="TFile">The deserialized test file type.</typeparam>
36+
/// <typeparam name="TTest">The deserialized test type.</typeparam>
37+
/// <param name="fileParser">A function to parse the content of the file (provided as a string) to a test file.</param>
38+
/// <param name="testExtractor">A function to extract all the tests within the given test file.</param>
39+
public IReadOnlyList<TTest> LoadTests<TFile, TTest>(Func<string, TFile> fileParser, Func<TFile, IEnumerable<TTest>> testExtractor) =>
40+
ListTestFiles()
41+
.Select(file => fileParser(File.ReadAllText(file)))
42+
.SelectMany(testExtractor)
43+
.ToList()
44+
.AsReadOnly();
45+
46+
private static string FindRepoRoot()
47+
{
48+
var currentDirectory = Path.GetFullPath(".");
49+
var directory = new DirectoryInfo(currentDirectory);
50+
while (directory != null &&
51+
(!File.Exists(Path.Combine(directory.FullName, "LICENSE"))
52+
|| !File.Exists(Path.Combine(directory.FullName, "CloudEvents.sln"))))
53+
{
54+
directory = directory.Parent;
55+
}
56+
if (directory == null)
57+
{
58+
throw new Exception("Unable to determine root directory. Please run within the sdk-csharp repository.");
59+
}
60+
return directory.FullName;
61+
}
62+
}

test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ public async Task CopyToHttpListenerResponseAsync_StructuredMode()
260260

261261
var parsed = new JsonEventFormatter().DecodeStructuredModeMessage(bytes, MimeUtilities.ToContentType(content.Headers.ContentType), extensionAttributes: null);
262262
AssertCloudEventsEqual(cloudEvent, parsed);
263-
Assert.Equal(cloudEvent.Data, parsed.Data);
264263

265264
// We populate headers even though we don't strictly need to; let's validate that.
266265
Assert.Equal("1.0", response.Headers.GetValues("ce-specversion").Single());

0 commit comments

Comments
 (0)