This is an automated email from the ASF dual-hosted git repository. kpvdr pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-interop-test.git
commit 183efa4603dc95034872307ef30e7d2d3ad7e3b6 Author: QIT Development Team <[email protected]> AuthorDate: Tue Aug 4 12:51:15 2026 -0400 Phase 3b: Add xfail framework for known client library limitations Fix QIT bugs (float comparison, Python float32 decode ordering) and add an expected-failure framework so known upstream issues don't block CI. Framework: - KnownFailure registry (21 entries) in src/qit/core/xfail.py - Diff classification: genuine failures vs xfail vs xpass - Send error xfail handling (Rhea RangeError, .NET/Java ListTypeEncoder NRE) - JUnit XML: xfails emit <skipped>, correct suite counts - --strict flag: exit 1 if any xfails exist (honest failure count) QIT bugs fixed: - Python receiver float32 decode: check Proton type_name before isinstance(float) - Float comparison: normalize both sides to integer bit-patterns - Hex normalization: use prefix strip instead of str.replace("0x", "") Test data restored (previously hidden by workarounds): - binary/timestamp in list indices 3 and 8 - Null-after-non-null list case (index 7) - Nested ubyte array test (array index 7) Result: 550 tests — 479 passed, 0 failed, 71 xfail (known issues) Co-Authored-By: Claude Opus 4.6 <[email protected]> --- shims/dotnet-proton/src/TypeCodec.cs | 5 + shims/python-proton/shim.py | 42 +++----- src/qit/cli/main.py | 12 ++- src/qit/core/comparison.py | 45 ++++---- src/qit/core/orchestrator.py | 177 ++++++++++++++++++++++++++---- src/qit/core/xfail.py | 204 +++++++++++++++++++++++++++++++++++ src/qit/types/composites.py | 23 +++- 7 files changed, 433 insertions(+), 75 deletions(-) diff --git a/shims/dotnet-proton/src/TypeCodec.cs b/shims/dotnet-proton/src/TypeCodec.cs index f92e58d..58e3879 100644 --- a/shims/dotnet-proton/src/TypeCodec.cs +++ b/shims/dotnet-proton/src/TypeCodec.cs @@ -404,6 +404,9 @@ namespace Qit.Shim private static Guid UuidStringToAmqpGuid(string uuid) { + // Parse UUID hex bytes in RFC 4122 order and pass directly to Guid(byte[]). + // Guid(byte[]) interprets groups 1-3 as LE, but ToByteArray() reverses the + // same way, so the wire bytes end up in the original RFC 4122 order. var clean = uuid.Replace("-", ""); var bytes = new byte[16]; for (int i = 0; i < 16; i++) @@ -413,6 +416,8 @@ namespace Qit.Shim private static string AmqpGuidToUuidString(Guid guid) { + // ToByteArray() returns bytes in the same order they were passed to + // the Guid(byte[]) constructor, preserving RFC 4122 byte order. var b = guid.ToByteArray(); return $"{b[0]:x2}{b[1]:x2}{b[2]:x2}{b[3]:x2}-{b[4]:x2}{b[5]:x2}-{b[6]:x2}{b[7]:x2}-{b[8]:x2}{b[9]:x2}-{b[10]:x2}{b[11]:x2}{b[12]:x2}{b[13]:x2}{b[14]:x2}{b[15]:x2}"; } diff --git a/shims/python-proton/shim.py b/shims/python-proton/shim.py index 53ea55d..aa5db2f 100755 --- a/shims/python-proton/shim.py +++ b/shims/python-proton/shim.py @@ -577,45 +577,27 @@ class ReceiverHandler(MessagingHandler): if isinstance(value, bool): return value - # Regular Python floats (check before int, since bool is a subclass of int) - if isinstance(value, float): - # Convert to hex for exact comparison (handles infinity, NaN, etc.) - float_bytes = struct.pack(">d", value) - int_val = struct.unpack(">Q", float_bytes)[0] - return f"0x{int_val:016x}" - - # Integers - if isinstance(value, int): - return value - - # Proton numeric types + # Proton numeric types — check BEFORE isinstance(float/int) since + # Proton types like float32 are float subclasses type_name = type(value).__name__ - # Integer types if type_name in ("ubyte", "ushort", "uint", "ulong", "byte", "short", "int32"): return int(value) - # Floating point - convert to hex for exact comparison if type_name == "float32": - float_val = float(value) - # Handle special float values - if math.isinf(float_val) or math.isnan(float_val): - float_bytes = struct.pack(">f", float_val) - else: - float_bytes = struct.pack(">f", float_val) + float_bytes = struct.pack(">f", float(value)) int_val = struct.unpack(">I", float_bytes)[0] return f"0x{int_val:08x}" - if type_name == "float": - float_val = float(value) - # Handle special double values - if math.isinf(float_val) or math.isnan(float_val): - float_bytes = struct.pack(">d", float_val) - else: - float_bytes = struct.pack(">d", float_val) + # Python float (64-bit double) — Proton's "float" type name is double + if isinstance(value, float): + float_bytes = struct.pack(">d", value) int_val = struct.unpack(">Q", float_bytes)[0] return f"0x{int_val:016x}" + if isinstance(value, int): + return value + # Character if type_name == "char": return ord(str(value)) @@ -629,8 +611,10 @@ class ReceiverHandler(MessagingHandler): return str(value) # Binary - if isinstance(value, bytes): - return value.hex() + if isinstance(value, (bytes, bytearray)): + return bytes(value).hex() + if isinstance(value, memoryview): + return bytes(value).hex() # String if isinstance(value, str): diff --git a/src/qit/cli/main.py b/src/qit/cli/main.py index 03fe9af..266a8b3 100644 --- a/src/qit/cli/main.py +++ b/src/qit/cli/main.py @@ -117,6 +117,11 @@ def test() -> None: is_flag=True, help="Include extended tier test values for complex types", ) [email protected]( + "--strict", + is_flag=True, + help="Treat known failures (xfail) as real failures", +) def test_amqp_types( sender: tuple[str, ...], receiver: tuple[str, ...], @@ -126,6 +131,7 @@ def test_amqp_types( verbose: bool, junit_xml: str | None, extended: bool, + strict: bool, ) -> None: """Test AMQP primitive and complex types interoperability.""" from pathlib import Path @@ -263,11 +269,13 @@ def test_amqp_types( # Generate JUnit XML if requested if junit_xml: - orchestrator.generate_junit_xml(results, junit_xml) + orchestrator.generate_junit_xml(results, junit_xml, strict=strict) click.echo(f"\n✓ JUnit XML report written to: {junit_xml}") # Exit with error if any tests failed - if any(not r.success for r in results): + has_failures = any(not r.success for r in results) + has_xfails = any(r.success and r.xfail_diffs for r in results) + if has_failures or (strict and has_xfails): sys.exit(1) diff --git a/src/qit/core/comparison.py b/src/qit/core/comparison.py index 307ab15..08e47be 100644 --- a/src/qit/core/comparison.py +++ b/src/qit/core/comparison.py @@ -224,36 +224,37 @@ class MessageComparator: return False return self._compare_typed_element(expected["value"], actual["value"]) + def _to_float_bits(self, value: Any) -> int | None: + """Normalize a float/double value to its integer bit-pattern.""" + if isinstance(value, int): + return value + if isinstance(value, str): + s = value.strip() + if s.startswith("0x") or s.startswith("0X"): + return int(s, 16) + try: + return int(s) + except ValueError: + return None + return None + def _compare_float(self, expected: Any, actual: Any) -> bool: - """Compare floating point values using hex representation.""" - # If both are hex strings, compare directly - if isinstance(expected, str) and expected.startswith("0x"): - if isinstance(actual, str) and actual.startswith("0x"): - return expected.lower() == actual.lower() - # Convert actual to hex if it's numeric - return expected.lower() == hex(int(actual)).lower() - - # If both are numeric, compare directly (may have precision issues) - if isinstance(expected, (int, float)) and isinstance(actual, (int, float)): - return expected == actual - - # Mixed types - try conversion - try: - if isinstance(expected, str): - expected = int(expected, 16) - if isinstance(actual, str): - actual = int(actual, 16) - return expected == actual - except (ValueError, TypeError): + """Compare floating point values by normalizing to integer bit-patterns.""" + exp_bits = self._to_float_bits(expected) + act_bits = self._to_float_bits(actual) + if exp_bits is None or act_bits is None: return False + return exp_bits == act_bits def _normalize_hex(self, value: Any) -> str: """Normalize hex string representation.""" if isinstance(value, bytes): return value.hex().lower() if isinstance(value, str): - # Remove any whitespace, 0x prefix, etc. - return value.replace(" ", "").replace("0x", "").lower() + s = value.replace(" ", "").lower() + if s.startswith("0x"): + s = s[2:] + return s return str(value).lower() def format_diff_report(self, diffs: list[MessageDiff]) -> str: diff --git a/src/qit/core/orchestrator.py b/src/qit/core/orchestrator.py index 5c2255f..2ebabab 100644 --- a/src/qit/core/orchestrator.py +++ b/src/qit/core/orchestrator.py @@ -11,6 +11,7 @@ from typing import Any from qit.core.broker import BrokerManager from qit.core.comparison import MessageComparator, MessageDiff from qit.core.shim import Shim +from qit.core.xfail import KnownFailure, find_known_failure, get_applicable_failures @dataclass @@ -32,6 +33,8 @@ class TestResult: diffs: list[MessageDiff] error: str | None = None duration_ms: float = 0.0 + xfail_diffs: list[tuple[MessageDiff, KnownFailure]] | None = None + xpass_entries: list[KnownFailure] | None = None class Orchestrator: @@ -98,8 +101,10 @@ class Orchestrator: result = self.run_test_case(test_case) results.append(result) - if result.success: + if result.success and not result.xfail_diffs: print("✓") + elif result.success and result.xfail_diffs: + print(f"✓ ({len(result.xfail_diffs)} xfail)") else: print("✗") if result.error: @@ -165,11 +170,32 @@ class Orchestrator: ) if not send_result.success: + duration_ms = (time.time() - start_time) * 1000 + applicable = get_applicable_failures( + test_case.sender_shim, + test_case.receiver_shim, + test_case.amqp_type, + ) + if applicable: + return TestResult( + test_case=test_case, + success=True, + diffs=[], + error=f"Send failed (xfail): {send_result.error}", + duration_ms=duration_ms, + xfail_diffs=[( + MessageDiff(index=-1, field="error", + expected="success", actual="send_error", + message=f"Send error: {send_result.error}"), + applicable[0], + )], + ) return TestResult( test_case=test_case, success=False, diffs=[], error=f"Send failed: {send_result.error}", + duration_ms=duration_ms, ) # Receive messages @@ -189,18 +215,25 @@ class Orchestrator: ) # Compare messages - diffs = self.comparator.compare_messages( + all_diffs = self.comparator.compare_messages( send_result.messages, recv_result.messages, ) duration_ms = (time.time() - start_time) * 1000 + # Classify diffs into genuine failures vs expected failures + genuine, xfail_diffs, xpass = self._classify_diffs( + test_case, all_diffs, + ) + return TestResult( test_case=test_case, - success=len(diffs) == 0, - diffs=diffs, + success=len(genuine) == 0, + diffs=genuine, duration_ms=duration_ms, + xfail_diffs=xfail_diffs, + xpass_entries=xpass, ) except Exception as e: @@ -211,11 +244,56 @@ class Orchestrator: error=f"Unexpected error: {e}", ) + def _classify_diffs( + self, + test_case: TestCase, + diffs: list[MessageDiff], + ) -> tuple[list[MessageDiff], list[tuple[MessageDiff, KnownFailure]], list[KnownFailure]]: + """Partition diffs into genuine failures and expected failures. + + Returns: + (genuine_diffs, xfail_diffs, xpass_entries) + """ + genuine: list[MessageDiff] = [] + xfail_diffs: list[tuple[MessageDiff, KnownFailure]] = [] + matched_indices: set[int] = set() + + for diff in diffs: + kf = find_known_failure( + test_case.sender_shim, + test_case.receiver_shim, + test_case.amqp_type, + diff.index, + ) + if kf is not None: + xfail_diffs.append((diff, kf)) + matched_indices.add(diff.index) + else: + genuine.append(diff) + + # Find xpass: registered failures that didn't produce any diffs + xpass: list[KnownFailure] = [] + applicable = get_applicable_failures( + test_case.sender_shim, + test_case.receiver_shim, + test_case.amqp_type, + ) + for kf in applicable: + if kf.message_indices is not None: + if not kf.message_indices & matched_indices: + xpass.append(kf) + elif not matched_indices: + xpass.append(kf) + + return genuine, xfail_diffs, xpass + def generate_report(self, results: list[TestResult]) -> str: """Generate a summary report of test results.""" total = len(results) - passed = sum(1 for r in results if r.success) - failed = total - passed + passed = sum(1 for r in results if r.success and not r.xfail_diffs) + failed = sum(1 for r in results if not r.success) + xfail_count = sum(1 for r in results if r.success and r.xfail_diffs) + xpass_count = sum(1 for r in results if r.xpass_entries) lines = [ "=" * 80, @@ -224,8 +302,12 @@ class Orchestrator: f"Total: {total}", f"Passed: {passed} ({100 * passed / total:.1f}%)" if total > 0 else "Passed: 0", f"Failed: {failed}", - "", ] + if xfail_count > 0: + lines.append(f"XFail: {xfail_count} (known issues)") + if xpass_count > 0: + lines.append(f"XPass: {xpass_count} (known issues that now pass)") + lines.append("") if failed > 0: lines.append("Failed Tests:") @@ -237,47 +319,81 @@ class Orchestrator: if result.error: lines.append(f" Error: {result.error}") if result.diffs: - for diff in result.diffs[:3]: # Show first 3 diffs + for diff in result.diffs[:3]: lines.append(f" {diff.message}") if len(result.diffs) > 3: lines.append(f" ... and {len(result.diffs) - 3} more") lines.append("") + if xfail_count > 0: + lines.append("Expected Failures (known issues):") + lines.append("-" * 80) + for result in results: + if result.success and result.xfail_diffs: + tc = result.test_case + reasons = {kf.reason for _, kf in result.xfail_diffs} + lines.append( + f" {tc.sender_shim} → {tc.receiver_shim} ({tc.amqp_type}): " + f"{len(result.xfail_diffs)} diff(s)" + ) + for reason in sorted(reasons): + lines.append(f" [XFAIL] {reason}") + lines.append("") + + if xpass_count > 0: + lines.append("Unexpected Passes (investigate — known issues now passing):") + lines.append("-" * 80) + for result in results: + if result.xpass_entries: + tc = result.test_case + for kf in result.xpass_entries: + lines.append( + f" {tc.sender_shim} → {tc.receiver_shim} ({tc.amqp_type}): " + f"[XPASS] {kf.reason}" + ) + lines.append("") + lines.append("=" * 80) return "\n".join(lines) - def generate_junit_xml(self, results: list[TestResult], output_path: str) -> None: + def generate_junit_xml( + self, + results: list[TestResult], + output_path: str, + strict: bool = False, + ) -> None: """ Generate JUnit XML report for CI/CD integration. Args: results: Test results to report output_path: Path to write XML file + strict: If True, xfails are reported as failures """ from pathlib import Path from xml.etree.ElementTree import Element, SubElement, tostring from xml.dom import minidom - # Ensure output directory exists Path(output_path).parent.mkdir(parents=True, exist_ok=True) total = len(results) - passed = sum(1 for r in results if r.success) - failed = total - passed + failed = sum(1 for r in results if not r.success) + skipped = sum(1 for r in results if r.success and r.xfail_diffs) + if strict: + failed += skipped + skipped = 0 total_time_sec = sum(r.duration_ms for r in results) / 1000 - # Create root testsuite element testsuite = Element( "testsuite", name="QIT AMQP Interoperability Tests", tests=str(total), failures=str(failed), errors="0", - skipped="0", + skipped=str(skipped), time=f"{total_time_sec:.3f}", ) - # Add test cases for result in results: tc = result.test_case testcase = SubElement( @@ -289,7 +405,6 @@ class Orchestrator: ) if not result.success: - # Add failure element failure_msg = result.error if result.error else f"{len(result.diffs)} message difference(s)" failure_details = [] @@ -298,7 +413,7 @@ class Orchestrator: if result.diffs: failure_details.append(f"\nMessage Differences ({len(result.diffs)} total):") - for diff in result.diffs[:10]: # Include up to 10 diffs + for diff in result.diffs[:10]: failure_details.append(f" - {diff.message}") if len(result.diffs) > 10: failure_details.append(f" ... and {len(result.diffs) - 10} more differences") @@ -311,7 +426,33 @@ class Orchestrator: ) failure.text = "\n".join(failure_details) - # Pretty print and write to file + elif result.xfail_diffs: + if strict: + reasons = {kf.reason for _, kf in result.xfail_diffs} + failure = SubElement( + testcase, + "failure", + message=f"{len(result.xfail_diffs)} known issue(s) (strict mode)", + type="KnownIssue", + ) + failure.text = "\n".join( + f" - {reason}" for reason in sorted(reasons) + ) + else: + reasons = {kf.reason for _, kf in result.xfail_diffs} + skipped_elem = SubElement( + testcase, + "skipped", + message=f"Known: {'; '.join(sorted(reasons))}", + ) + + if result.xpass_entries: + sysout = SubElement(testcase, "system-out") + xpass_lines = [ + f"[XPASS] {kf.reason}" for kf in result.xpass_entries + ] + sysout.text = "\n".join(xpass_lines) + xml_str = minidom.parseString(tostring(testsuite, encoding="unicode")).toprettyxml(indent=" ") with open(output_path, "w", encoding="utf-8") as f: f.write(xml_str) diff --git a/src/qit/core/xfail.py b/src/qit/core/xfail.py new file mode 100644 index 0000000..aac2c7c --- /dev/null +++ b/src/qit/core/xfail.py @@ -0,0 +1,204 @@ +""" +Known failure registry for expected test failures. + +Tracks client library limitations and known bugs that should not block CI. +Each entry documents the specific failure, its root cause, and optionally +a link to the upstream bug tracker. +""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class KnownFailure: + """A known test failure caused by a client library limitation.""" + + sender: str + receiver: str + amqp_type: str + message_indices: frozenset[int] | None + reason: str + bug_url: str = "" + + +def find_known_failure( + sender: str, + receiver: str, + amqp_type: str, + message_index: int, +) -> KnownFailure | None: + """Find a matching known failure entry for a specific test diff. + + Returns the first matching KnownFailure, or None if no match. + """ + for kf in KNOWN_FAILURES: + if kf.amqp_type != amqp_type: + continue + if kf.sender != "*" and kf.sender != sender: + continue + if kf.receiver != "*" and kf.receiver != receiver: + continue + if kf.message_indices is not None and message_index not in kf.message_indices: + continue + return kf + return None + + +def get_applicable_failures( + sender: str, + receiver: str, + amqp_type: str, +) -> list[KnownFailure]: + """Get all known failures that apply to a given test case.""" + result = [] + for kf in KNOWN_FAILURES: + if kf.amqp_type != amqp_type: + continue + if kf.sender != "*" and kf.sender != sender: + continue + if kf.receiver != "*" and kf.receiver != receiver: + continue + result.append(kf) + return result + + +# --------------------------------------------------------------------------- +# Registry of known failures +# +# Each entry documents a client library limitation that causes predictable +# test failures. These are NOT QIT bugs — they are upstream issues. +# --------------------------------------------------------------------------- + +KNOWN_FAILURES: list[KnownFailure] = [ + # --- JavaScript Rhea: parseInt() precision loss for values > 2^53 --- + KnownFailure( + sender="javascript-rhea", receiver="*", amqp_type="ulong", + message_indices=frozenset({6, 7, 8, 9}), + reason="JS number precision loss for ulong values > 2^53", + ), + KnownFailure( + sender="*", receiver="javascript-rhea", amqp_type="ulong", + message_indices=frozenset({6, 7, 8, 9}), + reason="JS number precision loss for ulong values > 2^53", + ), + KnownFailure( + sender="javascript-rhea", receiver="*", amqp_type="long", + message_indices=frozenset({0, 3, 13, 14}), + reason="JS number precision loss for long values with |v| > 2^53", + ), + KnownFailure( + sender="*", receiver="javascript-rhea", amqp_type="long", + message_indices=frozenset({0, 3, 13, 14}), + reason="JS number precision loss for long values with |v| > 2^53", + ), + + # --- Java ProtonJ2: 16-bit char truncates supplementary codepoints --- + KnownFailure( + sender="java-protonj2", receiver="*", amqp_type="char", + message_indices=frozenset({8}), + reason="Java char is 16-bit, truncates codepoints > U+FFFF", + ), + KnownFailure( + sender="*", receiver="java-protonj2", amqp_type="char", + message_indices=frozenset({8}), + reason="Java char is 16-bit, truncates codepoints > U+FFFF", + ), + + # --- .NET Proton: 16-bit char truncates supplementary codepoints --- + KnownFailure( + sender="dotnet-proton", receiver="*", amqp_type="char", + message_indices=frozenset({8}), + reason=".NET char is 16-bit, truncates codepoints > U+FFFF", + ), + KnownFailure( + sender="*", receiver="dotnet-proton", amqp_type="char", + message_indices=frozenset({8}), + reason=".NET char is 16-bit, truncates codepoints > U+FFFF", + ), + + # --- .NET Proton: byte[]/binary ambiguity in lists --- + # byte[] implements IList<byte>, so Proton .NET encodes it as AMQP + # array-of-ubyte instead of binary when it appears inside a list. + KnownFailure( + sender="dotnet-proton", receiver="*", amqp_type="list", + message_indices=frozenset({3, 8}), + reason="Proton .NET encodes byte[] as array-of-ubyte in lists", + ), + + # --- .NET Proton: timestamp decoded as Int64 in lists --- + KnownFailure( + sender="*", receiver="dotnet-proton", amqp_type="list", + message_indices=frozenset({3, 8}), + reason="Proton .NET decodes timestamp as Int64 in list context", + ), + + # --- .NET Proton: ListTypeEncoder NRE with null after non-null --- + KnownFailure( + sender="dotnet-proton", receiver="*", amqp_type="list", + message_indices=frozenset({7}), + reason="Proton .NET ListTypeEncoder NullReferenceException: null after non-null", + ), + + # --- Java ProtonJ2: ListTypeEncoder NRE with null after non-null --- + KnownFailure( + sender="java-protonj2", receiver="*", amqp_type="list", + message_indices=frozenset({7}), + reason="ProtonJ2 ListTypeEncoder NullPointerException: null after non-null", + ), + + # --- Java ProtonJ2: unsigned long values > Long.MAX_VALUE --- + KnownFailure( + sender="*", receiver="java-protonj2", amqp_type="ulong", + message_indices=frozenset({8, 9}), + reason="ProtonJ2 decodes unsigned long values > 2^63 as negative signed long", + ), + + # --- .NET Proton: ubyte arrays decoded as binary --- + KnownFailure( + sender="*", receiver="dotnet-proton", amqp_type="array", + message_indices=frozenset({7}), + reason="Proton .NET byte[]/binary ambiguity for nested ubyte arrays", + ), + + # --- Java ProtonJ2: binary/timestamp decoded incorrectly in lists --- + KnownFailure( + sender="*", receiver="java-protonj2", amqp_type="list", + message_indices=frozenset({3, 8}), + reason="ProtonJ2 decodes binary/timestamp incorrectly in list context", + ), + + # --- Java ProtonJ2: null character (U+0000) encoding --- + KnownFailure( + sender="java-protonj2", receiver="*", amqp_type="char", + message_indices=frozenset({0}), + reason="ProtonJ2 encodes null character (U+0000) incorrectly", + ), + + # --- .NET Proton: empty binary round-trip --- + KnownFailure( + sender="dotnet-proton", receiver="dotnet-proton", amqp_type="binary", + message_indices=frozenset({0}), + reason="Proton .NET empty binary round-trip type mismatch", + ), + + # --- .NET Proton → Java ProtonJ2: binary encoding incompatibility --- + KnownFailure( + sender="dotnet-proton", receiver="java-protonj2", amqp_type="binary", + message_indices=None, + reason="Proton .NET binary encoding incompatible with ProtonJ2 binary decoder", + ), + + # --- .NET Proton: timestamp decoded as Int64 (primitive type) --- + KnownFailure( + sender="*", receiver="dotnet-proton", amqp_type="timestamp", + message_indices=None, + reason="Proton .NET decodes timestamp as Int64 instead of DateTime", + ), + + # --- Java ProtonJ2: timestamp decoded as Long (primitive type) --- + KnownFailure( + sender="*", receiver="java-protonj2", amqp_type="timestamp", + message_indices=None, + reason="ProtonJ2 decodes timestamp as Long instead of Date", + ), +] diff --git a/src/qit/types/composites.py b/src/qit/types/composites.py index a1bca0d..a86d6d6 100644 --- a/src/qit/types/composites.py +++ b/src/qit/types/composites.py @@ -34,6 +34,14 @@ class AmqpComplexTypes: {"element_type": "ushort", "elements": [4, 5, 6]}, ], }, + # Array of ubyte arrays (triggers Proton .NET byte[]/binary ambiguity) + { + "element_type": "array", + "elements": [ + {"element_type": "ubyte", "elements": [1, 2, 3]}, + {"element_type": "ubyte", "elements": [4, 5, 6]}, + ], + }, # Array of lists (nested) { "element_type": "list", @@ -55,14 +63,15 @@ class AmqpComplexTypes: [["string", "hello"], ["string", "world"]], # Homogeneous ints [["int", -1], ["int", 0], ["int", 1]], - # Mixed primitives (null first: works around Proton .NET ListTypeEncoder NRE; - # binary omitted: Proton .NET encodes byte[] as array-of-ubyte in lists) + # Mixed primitives (null first: works around Proton .NET ListTypeEncoder NRE) [ ["null", None], ["string", "hello"], ["int", -42], ["boolean", True], ["float", "0x3f800000"], + ["binary", "deadbeef"], + ["timestamp", 1609459200000], ], # Nested lists [ @@ -80,9 +89,13 @@ class AmqpComplexTypes: ["array", {"element_type": "uint", "elements": [1, 2, 3]}], ["string", "after"], ], + # Null after non-null (triggers Proton .NET ListTypeEncoder NRE) + [ + ["string", "before"], + ["null", None], + ["int", 42], + ], # Kitchen sink: one element per common primitive type - # (timestamp omitted: Proton .NET decodes it as long in list context; - # binary omitted: Proton .NET encodes byte[] as array-of-ubyte in lists) [ ["null", None], ["boolean", True], @@ -98,6 +111,8 @@ class AmqpComplexTypes: ["symbol", "sym"], ["uuid", "550e8400-e29b-41d4-a716-446655440000"], ["char", 65], + ["binary", "ff00"], + ["timestamp", 946684800000], ], ], "description": "Heterogeneous typed list", --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
