Skip to content

Commit b5227e4

Browse files
committed
The commit message:
Vendor conformance suite and add Artemis conformance tests
1 parent e9ac581 commit b5227e4

11 files changed

Lines changed: 526 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,23 @@ jobs:
3131
- name: Test with coverage gate (>=90% line)
3232
run: dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Threshold=90 /p:ThresholdType=line /p:ThresholdStat=total
3333

34+
conformance:
35+
name: Conformance suite in sync
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v5
39+
- name: Verify vendored conformance matches the canonical suite
40+
run: |
41+
git clone --depth 1 https://github.com/BabelQueue/conformance.git "$RUNNER_TEMP/conformance"
42+
diff -ru "$RUNNER_TEMP/conformance/manifest.json" "tests/BabelQueue.Artemis.Tests/conformance/manifest.json"
43+
diff -ru "$RUNNER_TEMP/conformance/fixtures" "tests/BabelQueue.Artemis.Tests/conformance/fixtures"
44+
diff -ru "$RUNNER_TEMP/conformance/schema" "tests/BabelQueue.Artemis.Tests/conformance/schema"
45+
echo "Vendored conformance is in sync with the canonical suite."
46+
3447
ci-green:
3548
name: CI green
3649
runs-on: ubuntu-latest
37-
needs: [test]
50+
needs: [test, conformance]
3851
if: ${{ always() }}
3952
steps:
4053
- name: Fail if any required job did not pass
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text.Json;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Amqp;
8+
using Amqp.Framing;
9+
using Amqp.Types;
10+
using BabelQueue;
11+
using BabelQueue.Artemis;
12+
using Moq;
13+
using Xunit;
14+
15+
namespace BabelQueue.Artemis.Tests;
16+
17+
/// <summary>
18+
/// Apache ActiveMQ Artemis binding conformance against the vendored canonical suite's
19+
/// <c>artemis</c> block: the §7 AMQP projection (the <c>x-opt-jms-type</c> annotation /
20+
/// <c>correlation-id</c> + the <c>bq-</c> application properties) and the
21+
/// <c>attempts = max(body, delivery-count)</c> reconciliation (the AMQP counter is 0-based, no
22+
/// −1). No Artemis, no network.
23+
/// </summary>
24+
public sealed class ArtemisConformanceTests
25+
{
26+
private static readonly string Dir = Path.Combine(AppContext.BaseDirectory, "conformance");
27+
28+
private static JsonElement Artemis()
29+
{
30+
using var doc = JsonDocument.Parse(File.ReadAllText(Path.Combine(Dir, "manifest.json")));
31+
return doc.RootElement.GetProperty("artemis").Clone();
32+
}
33+
34+
[Fact]
35+
public void PropertyProjectionMatchesGolden()
36+
{
37+
var projection = Artemis().GetProperty("property_projection");
38+
var body = File.ReadAllText(Path.Combine(Dir, projection.GetProperty("envelope_file").GetString()!));
39+
var message = AmqpProperties.ToMessage(EnvelopeCodec.Decode(body));
40+
41+
Assert.Equal(
42+
projection.GetProperty("jms_type").GetString(),
43+
message.MessageAnnotations.Map[new Symbol("x-opt-jms-type")] as string);
44+
Assert.Equal(
45+
projection.GetProperty("correlation_id").GetString(),
46+
message.Properties.CorrelationId as string);
47+
48+
foreach (var golden in projection.GetProperty("properties").EnumerateObject())
49+
{
50+
Assert.Equal(golden.Value.GetString(), message.ApplicationProperties.Map[golden.Name] as string);
51+
}
52+
}
53+
54+
[Fact]
55+
public async Task AttemptsReconciliationMatchesGolden()
56+
{
57+
foreach (var testCase in Artemis().GetProperty("attempts_reconciliation").GetProperty("cases").EnumerateArray())
58+
{
59+
var bodyAttempts = testCase.GetProperty("body_attempts").GetInt32();
60+
var deliveryCount = (uint)testCase.GetProperty("delivery_count").GetInt32();
61+
var expected = testCase.GetProperty("expected_attempts").GetInt32();
62+
var env = EnvelopeCodec.Make("urn:babel:orders:created", new Dictionary<string, object?> { ["x"] = 1 }, "orders")
63+
with { Attempts = bodyAttempts };
64+
65+
var message = new Message(EnvelopeCodec.Encode(env))
66+
{
67+
MessageAnnotations = new MessageAnnotations(),
68+
};
69+
message.MessageAnnotations.Map[new Symbol("x-opt-jms-type")] = "urn:babel:orders:created";
70+
if (deliveryCount > 0)
71+
{
72+
message.Header = new Header { DeliveryCount = deliveryCount };
73+
}
74+
75+
var receiver = new Mock<IAmqpReceiver>();
76+
receiver.Setup(r => r.ReceiveAsync(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>())).ReturnsAsync(message);
77+
receiver.Setup(r => r.AcceptAsync(It.IsAny<Message>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
78+
receiver.Setup(r => r.ReleaseAsync(It.IsAny<Message>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
79+
80+
var seen = -1;
81+
var handlers = new Dictionary<string, BabelHandler>
82+
{
83+
["urn:babel:orders:created"] = (e, _, _) => { seen = e.Attempts; return Task.CompletedTask; },
84+
};
85+
await new ArtemisConsumer(receiver.Object, handlers, new ArtemisConsumerOptions { MaxTries = 99 }).PollAsync();
86+
87+
Assert.Equal(expected, seen);
88+
}
89+
}
90+
}

tests/BabelQueue.Artemis.Tests/BabelQueue.Artemis.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,9 @@
2424
<ProjectReference Include="../../src/BabelQueue.Artemis/BabelQueue.Artemis.csproj" />
2525
</ItemGroup>
2626

27+
<ItemGroup>
28+
<!-- Vendored canonical conformance suite (synced from conformance/); copied next to the test assembly. -->
29+
<None Include="conformance/**/*.json" CopyToOutputDirectory="PreserveNewest" LinkBase="conformance" />
30+
</ItemGroup>
31+
2732
</Project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"job": "urn:babel:orders:created",
3+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4+
"data": {
5+
"order_id": 1042
6+
},
7+
"meta": {
8+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9+
"queue": "orders",
10+
"lang": "php",
11+
"schema_version": 1,
12+
"created_at": 1749132727000
13+
},
14+
"attempts": 3,
15+
"dead_letter": {
16+
"reason": "failed",
17+
"error": "Payment gateway timeout",
18+
"exception": "App\\Exceptions\\GatewayTimeout",
19+
"failed_at": 1749132730000,
20+
"original_queue": "orders",
21+
"attempts": 3,
22+
"lang": "php"
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
3+
"data": {
4+
"order_id": 1042
5+
},
6+
"meta": {
7+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
8+
"queue": "orders",
9+
"lang": "php",
10+
"schema_version": 1,
11+
"created_at": 1749132727000
12+
},
13+
"attempts": 0
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"job": "urn:babel:orders:created",
3+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4+
"data": {
5+
"order_id": 1042
6+
},
7+
"meta": {
8+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9+
"queue": "orders",
10+
"lang": "php",
11+
"schema_version": 2,
12+
"created_at": 1749132727000
13+
},
14+
"attempts": 0
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"job": "urn:babel:orders:created",
3+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
4+
"data": {
5+
"order_id": 1042
6+
},
7+
"meta": {
8+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
9+
"queue": "orders",
10+
"lang": "php",
11+
"schema_version": 1,
12+
"created_at": 1749132727000
13+
},
14+
"attempts": 0
15+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"job": "urn:babel:catalog:item.indexed",
3+
"trace_id": "3f7a1d2e-9b4c-4a8d-bc1e-0f5a6b7c8d90",
4+
"data": {
5+
"title": "Café — naïve ☕",
6+
"qty": 7,
7+
"price_cents": 1299,
8+
"ratio": 0.5,
9+
"active": true,
10+
"note": null
11+
},
12+
"meta": {
13+
"id": "b2c3d4e5-f607-4890-a1b2-c3d4e5f60718",
14+
"queue": "catalog",
15+
"lang": "python",
16+
"schema_version": 1,
17+
"created_at": 1749132727000
18+
},
19+
"attempts": 2
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"urn": "urn:babel:orders:created",
3+
"trace_id": "9c1e0b44-7a2d-4e6f-8a10-2b3c4d5e6f70",
4+
"data": {
5+
"order_id": 1042
6+
},
7+
"meta": {
8+
"id": "a1b2c3d4-e5f6-4789-90ab-cdef01234567",
9+
"queue": "orders",
10+
"lang": "go",
11+
"schema_version": 1,
12+
"created_at": 1749132727000
13+
},
14+
"attempts": 0
15+
}

0 commit comments

Comments
 (0)