Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #291 +/- ##
==========================================
+ Coverage 94.62% 95.36% +0.74%
==========================================
Files 49 54 +5
Lines 4203 4812 +609
==========================================
+ Hits 3977 4589 +612
+ Misses 226 223 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues remain in version conversion, metadata round-tripping, string decoding, bounds handling, and defaults.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds XTCE 1.3 support alongside XTCE 1.2, including version-aware namespaces, bundled schemas, validation, serialization, and variable-string parsing.
Changes:
- Adds XTCE version detection, schema resolution, and CLI reporting.
- Updates parsing and serialization for XTCE 1.3 features.
- Adds extensive unit, integration, fixture, and parity coverage.
File summaries
| File | Review summary |
|---|---|
tests/unit/test_xtce/test_versions.py |
Version registry tests. |
tests/unit/test_xtce/test_validation.py |
Versioned schema and structural validation tests. |
tests/unit/test_xtce/test_encodings.py |
Moderate (1 vote): bare UTF-16/UTF-32 cases require endian-specific codec handling. |
tests/unit/test_xtce/test_definitions.py |
Definition conversion and round-trip tests. |
tests/test_data/test_xtce_1_3.xml |
XTCE 1.3 fixture. |
tests/test_data/test_xtce_1_3_only_features.xml |
XTCE 1.3-only feature fixture. |
tests/integration/test_xtce_based_parsing/test_xtce_version_parity.py |
Nit (1 vote): real-packet parity coverage omits SUDA and IDEX. |
tests/integration/test_cli.py |
XTCE 1.3 CLI coverage. |
tests/conftest.py |
Bundled schema fixture. |
space_packet_parser/xtce/validation.py |
Version-aware offline schema and structural validation. |
space_packet_parser/xtce/schemas/SpaceSystem-20180204.xsd |
Bundled XTCE 1.2 schema. |
space_packet_parser/xtce/schemas/README.md |
Schema provenance and checksums. |
space_packet_parser/xtce/encodings.py |
Moderate (1 vote): enforce maxSizeInBits; moderate (1 vote): normalize bare UTF codecs; moderate (1 vote): apply the empty TerminationChar default; nit (1 vote): clarify supported length/delimiter combinations. |
space_packet_parser/xtce/definitions.py |
Moderate (1 vote): cover version-specific units and references; moderate (2 votes): handle 1.3 time-unit serialization; moderate (1 vote): preserve systemType/assetType; nit (2 votes): correct constructor documentation. |
space_packet_parser/xtce/containers.py |
Schema-correct container serialization order. |
space_packet_parser/xtce/__init__.py |
XTCE version and namespace registry. |
space_packet_parser/cli.py |
XTCE and schema version reporting. |
space_packet_parser/__init__.py |
Public XTCE version exports. |
README.md |
XTCE support overview. |
docs/source/user_guide/xtce_validation.md |
Nit (1 vote): correct the invalid claim that spaced parameter names are valid in XTCE 1.2. |
CHANGELOG.md |
Documents XTCE 1.3 support and compatibility changes. |
.pre-commit-config.yaml |
Preserves bundled schema bytes. |
Review details
Suppressed comments (9)
docs/source/user_guide/xtce_validation.md:96
- This says that a parameter name containing spaces is valid in XTCE 1.2, but the 1.2 schema's
NameTypealready excludes spaces (SpaceSystem-20180204.xsd, lines 4989-4995). The version difference is in some reference syntaxes, not in allowing spaced parameter names; please correct this example so users do not construct an invalid 1.2 document based on the guide.
- **Name references.** XTCE 1.3 tightened the characters allowed in a parameter or container
reference, excluding space and tab (and adding array subscript syntax). A definition whose
parameter names contain spaces is valid XTCE 1.2 but not valid XTCE 1.3.
space_packet_parser/xtce/definitions.py:255
- This compatibility check only inspects variable-length string encodings, but the conversion docstring explicitly identifies time-unit enums and name references as version-specific. A 1.2 definition containing
units="picoSeconds"or a reference with a space can be retargeted to 1.3, after which serialization writes the unchanged value and produces schema-invalid XML without any warning. Please either translate/reject these values or include them in the incompatibility warning so the documented conversion contract is enforced.
def _warn_on_version_incompatible_encodings(self) -> None:
"""Warn about content that is valid in one XTCE version but not in the version being written.
Retargeting a definition at another XTCE version rewrites namespaces, not element content, so
a definition can hold a construct the target version's schema rejects. Variable-length strings
are the case that actually bites: XTCE 1.2 requires a declared buffer length (`DynamicValue` or
`DiscreteLookupList`) and treats `LeadingSize`/`TerminationChar` as optional, while XTCE 1.3
makes the buffer length optional and requires exactly one of `LeadingSize`/`TerminationChar`.
Either direction can therefore produce a document that fails schema validation.
"""
version = self.xtce_standard_version
if version not in ("1.2", "1.3"):
return
offenders = []
for parameter_type_name, parameter_type in self.parameter_types.items():
encoding = getattr(parameter_type, "encoding", None)
if not isinstance(encoding, encodings.StringDataEncoding) or encoding.fixed_length:
continue
has_declared_length = bool(encoding.dynamic_length_reference or encoding.discrete_lookup_length)
has_delimiter = bool(encoding.leading_length_size or encoding.termination_character)
if version == "1.3" and not has_delimiter:
offenders.append(parameter_type_name)
elif version == "1.2" and not has_declared_length:
space_packet_parser/xtce/definitions.py:459
from_xtce()captures only the root name and header fields here. XTCE 1.3'ssystemType(andassetType) are meaningful root attributes, butXtcePacketDefinitionhas no fields for them andto_xml_tree()never emits them; round-tripping the supplied 1.3 fixture silently changessystemType="asset"to the schema defaultunknown. Preserve these attributes or explicitly document the metadata loss.
space_system_name=space_system.attrib.get("name", None),
space_packet_parser/xtce/encodings.py:345
- The new LeadingSize-only path uses the reported length directly and never checks
max_size_in_bits, even though the XTCE schema defines that attribute as the upper bound for the variable string buffer. A packet whose size tag exceeds the declared maximum will therefore consume bytes beyond the field (potentially from the next field) instead of rejecting the over-sized value; enforce the bound before reading, ideally through a common check for every sizing branch.
elif self.leading_length_size:
# XTCE 1.3 permits a Variable string with no declared buffer length, in which case the
# leading size tag delimits the buffer: the tag itself, plus the content length it reports.
buflen_bits = self.leading_length_size + self._peek_leading_size(packet)
elif self.termination_character is not None:
# As above, but delimited by a terminator: the buffer runs up to and including it.
buflen_bits = self._scan_to_termination_character(packet)
else:
raise ValueError("No raw length specifier found when decoding a string.")
return int(buflen_bits)
space_packet_parser/xtce/encodings.py:186
- The constructor docstring says only one of
termination_character,fixed_length, orleading_length_sizemay be set, but this class deliberately supports a declared raw length together with a delimiter (the existing tests constructdynamic_length_referenceplustermination_character, and XTCE permits that combination). Please distinguish the raw-buffer length selectors from the optional content delimiter so callers are not told that a supported encoding is nonsensical.
f"""Constructor
Only one of termination_character, fixed_length, or leading_length_size should be set. Setting more than one
is nonsensical.
space_packet_parser/xtce/encodings.py:166
- The newly supported bare UTF-16/UTF-32 spellings are still passed directly to Python's
decode(self.encoding)(including during termination-character validation). Those codecs require a BOM; with XTCE's explicitbyteOrderand the new BOM-less buffers, construction raisesUnicodeDecodeErrorbefore_find_termination_characterruns, and parsing would fail similarly. Normalize to the endian-specific codec selected bybyte_orderand preserve that setting.
"UTF-16": 2,
"UTF-16LE": 2,
"UTF-16BE": 2,
"UTF-32": 4,
"UTF-32LE": 4,
space_packet_parser/xtce/encodings.py:565
- This newly added 1.3 path should also honor the XSD default on
TerminationChar: both bundled schemas define it asdefault="00". A schema-valid empty<TerminationChar/>hastext is None, so the assignment below treats the delimiter as absent and the constructor rejects the valid C-string. Apply the default when the element is empty.
# A Variable element with neither is legal in XTCE 1.3, where the buffer length is derived
# from the LeadingSize or TerminationChar read below. The constructor rejects the case
# where neither of those is present either.
tests/integration/test_xtce_based_parsing/test_xtce_version_parity.py:85
- This parity test claims real-packet decoding coverage for the bundled mission definitions, but the parameter list contains only CTIM and JPSS while the definition parity tests also include SUDA and IDEX. The corresponding SUDA and IDEX packet fixtures are present under
tests/test_data, so two of the four mission definitions in the PR's stated parity claim remain untested on actual packet bytes.
@pytest.mark.parametrize(
("definition_name", "packet_file_name", "root_container_name"),
[
("ctim/ctim_xtce_v1.xml", "ctim/ccsds_2021_155_14_39_51", "CCSDSTelemetryPacket"),
("jpss/jpss1_geolocation_xtce_v1.xml", "jpss/J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1", "CCSDSPacket"),
],
tests/unit/test_xtce/test_encodings.py:685
- These cases construct a bare
UTF-16/UTF-32encoding with an explicitbyte_order, butStringDataEncoding.__init__validates the termination bytes withbytes.decode(encoding). Python's BOM-dependent codecs reject these sequences without a BOM, so every bare case raises before_find_termination_characterruns;parse_valuewould also decode with the bare codec and ignore the requested endianness. Resolve an explicit BE/LE codec frombyte_orderand use it consistently before adding these cases.
encoding = encodings.StringDataEncoding(
encoding=encoding_name,
byte_order="mostSignificantByteFirst",
termination_character="00" * char_width,
- Files reviewed: 21/23 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "Ensure mydef.xtce_ns_prefix is a key in mydef.ns for valid XTCE output.", | ||
| UserWarning, | ||
| ) | ||
| self._warn_on_version_incompatible_encodings() |
There was a problem hiding this comment.
Fixed in 71d6b8b, as a diagnostic rather than a conversion.
_warn_on_version_incompatible_encodings now dispatches to two checks, and the new _warn_on_incompatible_time_units scans TimeParameterType.unit against TIME_UNITS_BY_VERSION, a new per-version table taken from each schema's TimeUnitsType enumeration. The warning names the parameter types, their units, and what the target version permits.
Not translating on the way out is deliberate: a unit is document content, and the PR's stated contract is that element content is passed through unchanged. Silently rewriting picoSeconds to picoseconds would make the output valid while changing what the author wrote. The gap was that the contract was documented but not enforced, which is what this closes.
Compound units (a tuple, from multiple <Unit> elements) are skipped — they are not expressible in the enumeration either way.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| xtce_standard_version : Optional[str] | ||
| Version of the XTCE standard this definition is written against, one of | ||
| {SUPPORTED_XTCE_VERSIONS}. This selects the XML namespace URI used when serializing, since the | ||
| namespace URI is what identifies the XTCE version of a document. Ignored if `ns` is given | ||
| explicitly (in that case the version is inferred from the URI in `ns`). Defaults to | ||
| {DEFAULT_XTCE_VERSION}. |
There was a problem hiding this comment.
Fixed in 71d6b8b. The docstring now states the mutual exclusion — passing both raises ValueError, and when ns is given the version is inferred from the URI it binds to xtce_ns_prefix — instead of saying the argument is ignored.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
medley56
left a comment
There was a problem hiding this comment.
I have no doubt that this works, but I have real concerns about the maintainability of our test suite after these changes. We now have test modules that mix expectations for XTCE 1.3 with expectations for XTCE 1.2 and future developers will have a very rough time parsing through those.
We need a test suite organizational system that handles segregating XTCE 1.3 tests from XTCE 1.2 tests in a way that is obvious to future human developers and AI agents. In other words, we need to keep our test modules human-readable even as we test across multiple XTCE versions.
medley56
left a comment
There was a problem hiding this comment.
The PR does what it says. I verified namespace-based version detection including the legacy https alias, the 1.3 VariableStringType shape, the LinearAdjustment slope default, EntryList/BaseContainer ordering, maxSizeInBits emission, and the bundled-schema checksums against both OMG XSDs and the checked-out head. Nothing blocks merging. The one warning is a performance regression on the existing 1.2 terminated-string path, where a C-level bytes.index became a Python loop. The rest is API tightening (version 1.1 is accepted as a serialization target), a more helpful error for legacy-namespace documents, test hygiene and organization, and CHANGELOG framing. The branch conflicts with main in CHANGELOG.md only, where #290 added a ### Fixed entry under [Unreleased]; a rebase resolves it. Copilot's existing comments on time units, the constructor docstring, systemType round-trip, and the unchecked max_size_in_bits on the LeadingSize path were independently confirmed and are not repeated here.
Severity tally: 0 critical, 1 warning, 7 suggestions, 0 nitpicks posted (2 dropped).
💡 SUGGESTION — 1.2 and 1.3 expectations are interleaved within test modules
Current state: version-awareness tests are a block appended at test_definitions.py:656-944 and test_validation.py:506-645, and the delimiter-derived string tests sit among general encoding tests at test_encodings.py:560-693. Only test_versions.py and test_xtce_version_parity.py are cleanly scoped. A reader cannot tell from a test's location or ID which version it targets, and there is no rule for where the next version-specific test goes.
A scheme that fits the existing layout:
- Version-agnostic behavior (must hold identically in 1.2 and 1.3) stays in the existing modules, parametrized over a shared
xtce_versionfixture intests/conftest.pywithparams=SUPPORTED_XTCE_VERSIONSand idsxtce12/xtce13, yielding the version and a matching definition path. - Version-specific behavior (delimiter-only strings,
systemType, 1.2-only warnings) moves totest_xtce_1_3_features.pyandtest_xtce_1_2_features.pywith a module-levelpytestmark. - Cross-version behavior (retargeting, compatibility warnings, legacy alias repair, schemaLocation rewriting) moves to
test_version_conversion.py;test_versions.pykeeps the registry tests. - Register
xtce12andxtce13markers inpyproject.tomlsopytest -m xtce13runs exactly the 1.3 surface and-m "not xtce13"proves nothing else depends on it. - Version-specific fixtures live under
tests/test_data/xtce_1_3/;test_xtce.xmlstays where it is to avoid churning every existing test. test_encodings.pystays version-neutral:StringDataEncodingdoes not know about XTCE versions, so the delimiter-derived tests belong there with the "XTCE 1.3 form" phrasing kept to docstrings.
This can be a follow-up PR; nothing else in this review depends on it.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| char_width = self._FIXED_CHARACTER_WIDTH_BYTES.get(self.encoding, 1) | ||
| for index in range(0, len(buffer) - len(self.termination_character) + 1, char_width): | ||
| if buffer[index : index + len(self.termination_character)] == self.termination_character: | ||
| return index | ||
| return -1 |
There was a problem hiding this comment.
WARNING — terminator search is now a Python loop on the existing 1.2 path
On main, parse_value found the terminator with raw_string_buffer.index(...), a single C call. _find_termination_character replaces that with a Python loop that slices at every step, and parse_value at encodings.py:463 now routes every terminated string through it, including the pre-existing declared-length path, not just the new 1.3 derived-length scan. For UTF-8 and the single-byte encodings, which cover nearly every real definition, the cost is proportional to string length on every packet.
bytes.find with an alignment check keeps the character-boundary guarantee and is one C call in the width-1 case:
| char_width = self._FIXED_CHARACTER_WIDTH_BYTES.get(self.encoding, 1) | |
| for index in range(0, len(buffer) - len(self.termination_character) + 1, char_width): | |
| if buffer[index : index + len(self.termination_character)] == self.termination_character: | |
| return index | |
| return -1 | |
| char_width = self._FIXED_CHARACTER_WIDTH_BYTES.get(self.encoding, 1) | |
| start = 0 | |
| while (index := buffer.find(self.termination_character, start)) != -1: | |
| if index % char_width == 0: | |
| return index | |
| start = index + 1 | |
| return -1 |
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in 900b581, taking the suggested bytes.find + alignment check.
You were right that this hit the 1.2 path, not just the new one. Measured before and after, UTF-8 with a one-byte terminator:
| buffer | before | after | |
|---|---|---|---|
| 16 B | 0.697 µs | 0.072 µs | 9.7× |
| 64 B | 2.388 µs | 0.075 µs | 31.6× |
| 256 B | 9.063 µs | 0.080 µs | 112.9× |
| 1024 B | 44.827 µs | 0.096 µs | 469.1× |
Both edge cases still hold: the UTF-16BE straddle (b"\x41\x00\x00\x42" → no terminator, b"\x41\x00\x00\x42\x00\x00" → index 4) and the UTF-8 multi-byte terminator that a fixed stride would step over.
Also added test_benchmark__terminated_string_parsing to tests/benchmark/test_fast_benchmarks.py. The repo had no string-parsing benchmark, which is why this was invisible to the suite rather than caught by it.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| if ns is None: | ||
| ns = xtce_nsmap(xtce_standard_version or DEFAULT_XTCE_VERSION, prefix=xtce_ns_prefix) |
There was a problem hiding this comment.
💡 SUGGESTION — xtce_standard_version="1.1" is accepted although 1.1 is not supported
The docstring at definitions.py:88-89 restricts this argument to SUPPORTED_XTCE_VERSIONS, but xtce_uri_for_version resolves any key of XTCE_XMLNS_BY_VERSION, which includes "1.1". Confirmed on the head: XtcePacketDefinition(xtce_standard_version="1.1") succeeds, the setter at definitions.py:217 does too, and to_xml_tree (definitions.py:326) then writes an xsi:schemaLocation pointing at the 1.1 XSD, which is not bundled. Validating that output triggers a network download instead of the offline validation the CHANGELOG promises for serialized documents. Rejecting versions outside SUPPORTED_XTCE_VERSIONS in the constructor and setter, while leaving xtce_uri_for_version permissive for detection, would close this. A test for the rejection is missing either way.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in 71d6b8b.
Added _check_writable_xtce_version, called from the constructor and the setter, which rejects anything outside SUPPORTED_XTCE_VERSIONS. xtce_uri_for_version and XTCE_XMLNS_BY_VERSION stay permissive, so a document in the 1.1 namespace is still detected as 1.1 — which is what the module docstring says that entry is for. Only selecting 1.1 as a write target is refused, and the error says why (no bundled schema, so the output could not be validated offline).
test_cannot_select_a_version_with_no_bundled_schema covers both entry points over 1.0, 1.1 and 9.9, and test_xtce_1_1_namespace_is_still_detected pins the asymmetry.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| if "No matching global declaration available for the validation root." in error.message: | ||
| standard_uris = ", ".join(f"{v}: {XTCE_XMLNS_BY_VERSION[v]}" for v in SUPPORTED_XTCE_VERSIONS) | ||
| result.add_error( | ||
| message="Namespace issue detected. Does the `xmlns[:xtce]=<chosen_xtce_uri>` URI on your document root element match the `targetNamespace` URI in your XSD? Typically this is http://www.omg.org/spec/XTCE/20180204", | ||
| message=( | ||
| "Namespace issue detected. Does the `xmlns[:xtce]=<chosen_xtce_uri>` URI on your " | ||
| "document root element match the `targetNamespace` URI in your XSD? The standard " | ||
| f"XTCE namespace URIs are ({standard_uris}), and the URI must match the XTCE version " | ||
| "of the XSD you are validating against." | ||
| ), | ||
| error_code="INVALID_XTCE_NAMESPACE", | ||
| context={ | ||
| "nsmap": xml_tree.getroot().nsmap, | ||
| "document_xtce_uri": document_xtce_uri, | ||
| "schema_version": result.schema_version, | ||
| }, | ||
| ) | ||
| result.add_error( |
There was a problem hiding this comment.
💡 SUGGESTION — legacy https namespace gets the generic namespace error
_validate_xtce_schema already recognizes the legacy https://www.omg.org/spec/XTCE/20180204 URI and sets xtce_version = "1.2" at validation.py:630-631, but the INVALID_XTCE_NAMESPACE message built here is the generic "does your xmlns match the targetNamespace" text. Running such a document through validate_xtce(level="schema") on the head gives valid=False, xtce_version="1.2", and nothing in the errors says this is the URI written by space_packet_parser <= 6.2 or that loading and re-writing the file repairs it. spp validate is where users will hit this first. A check for document_xtce_uri in LEGACY_XTCE_XMLNS_ALIASES with a one-sentence hint would make the error actionable.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in 71d6b8b.
The INVALID_XTCE_NAMESPACE branch now checks document_xtce_uri in LEGACY_XTCE_XMLNS_ALIASES and, when it matches, replaces the generic text with one that names the URI as written by space_packet_parser 6.2 and earlier, gives the canonical URI, and says that reading the document with from_xtce and writing it back out repairs it. Every other URI keeps the generic message, and the error_code is unchanged so anything filtering on it is unaffected.
test_version_validation.py covers both sides: test_legacy_namespace_gets_a_specific_hint and test_non_legacy_wrong_namespace_gets_the_generic_hint, so an arbitrary wrong namespace does not get blamed on an old release.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| if packet._parsing_pos % 8 != 0: | ||
| raise ValueError( | ||
| "A string whose raw buffer length is delimited by a termination character must begin on a byte " | ||
| f"boundary, but parsing is at bit {packet._parsing_pos}. Declare the buffer length explicitly with " | ||
| "a SizeInBits or DynamicValue element instead." | ||
| ) |
There was a problem hiding this comment.
💡 SUGGESTION — two new error branches have no test
This non-byte-aligned raise is one of the two uncovered lines behind the 99.59% patch figure. It does fire as written (confirmed by setting _parsing_pos = 4 by hand). The other is the xtce_ns_uri is None raise in the xtce_standard_version setter at definitions.py:218-222; the neighbouring "not bound by its namespace mapping" branch is tested at test_definitions.py:939 but this one is not. Each is a one-test addition.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in ddfe4f0. Both branches now have a test:
test_termination_delimited_string_must_start_on_a_byte_boundaryintest_encodings.py, which advances_parsing_posby 3 bits before parsing.test_convert_definition_with_no_namespace_raisesin the newtest_version_conversion.py, next to the "not bound by its namespace mapping" test you mentioned.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| xdef_12 = definitions.XtcePacketDefinition.from_xtce(test_data_dir / "test_xtce.xml") | ||
| xdef_13 = definitions.XtcePacketDefinition.from_xtce(test_data_dir / "test_xtce_1_3.xml") |
There was a problem hiding this comment.
💡 SUGGESTION — test_xtce_1_3.xml is an unguarded hand copy of test_xtce.xml
The two fixtures differ only in the namespace/schema URLs and systemType="asset" on the root (diffed after normalizing the header date). This test, test_validation.py:514-531, and the packet-parity test below all assume they are twins, but nothing enforces it, so the next edit to test_xtce.xml silently makes the 1.3 tests exercise a different definition. test_xtce_version_parity.py:20-30 already derives a 1.3 document in memory with _as_xtce_1_3; a fixture doing the same here would remove the 209-line duplicate. If a checked-in 1.3 file is preferred, a test asserting the two differ only on the expected lines would serve as the drift guard.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in ddfe4f0, with the drift guard rather than the in-memory fixture.
test_xtce_1_3_fixture_tracks_the_1_2_fixture normalizes the 1.3 file — namespace URI, schema URL, and systemType="asset" — and asserts the result equals test_xtce.xml exactly. Deriving it in memory would have meant moving test_validation.py and the CLI integration tests onto a fixture, and those want a real path on disk; and the helper would have had to inject systemType anyway to keep the 1.3-only attribute covered.
The guard moves with the fixture if tests/test_data/xtce_1_3/ happens, which is noted in #294.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
| def test_xtce_1_2_and_1_3_definitions_parse_packets_identically(test_data_dir, jpss_test_data_dir): | ||
| """Packets parse to the same values whether the definition is XTCE 1.2 or 1.3""" | ||
| xdef_12 = definitions.XtcePacketDefinition.from_xtce(test_data_dir / "test_xtce.xml") | ||
| xdef_13 = definitions.XtcePacketDefinition.from_xtce(test_data_dir / "test_xtce_1_3.xml") | ||
|
|
||
| with open(jpss_test_data_dir / "J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1", "rb") as f: | ||
| packet_bytes = list(spp.ccsds_generator(f))[:5] | ||
|
|
||
| assert [dict(xdef_12.parse_bytes(b)) for b in packet_bytes] == [dict(xdef_13.parse_bytes(b)) for b in packet_bytes] |
There was a problem hiding this comment.
💡 SUGGESTION — unit test reads real JPSS packet data
This opens J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1 and parses five real packets. The project instructions (.github/instructions/space_packet_parser.instructions.md:51) reserve real mission data for integration tests, and the same assertion already runs on that file in test_xtce_version_parity.py:80-102. Dropping this copy, or feeding it a synthetic packet from create_ccsds_packet as test_definitions.py:836 does, keeps the unit suite self-contained.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in ddfe4f0. The test now builds its packet with create_ccsds_packet(data=bytes(range(65)), apid=11) — 65 bytes being the JPSS_ATT_EPHEM payload this definition describes — and the jpss_test_data_dir fixture is gone. Varying bytes rather than zeros, so a field read at the wrong offset changes the result.
It also moved to test_version_conversion.py as part of the split, and its docstring points at the integration parity test for the real-bytes version.
While there: that parity test only covered CTIM and JPSS for packet decoding, though it covered all four missions for definition parity. It now covers SUDA (with skip_header_bytes=4) and IDEX too. Still ~5 s.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
|
|
||
| ### Changed | ||
|
|
||
| - **Breaking:** correct the values of `XTCE_1_2_XMLNS` and `XTCE_1_1_XMLNS`, which did not match the |
There was a problem hiding this comment.
💡 SUGGESTION — "Breaking" label commits the next release to 7.0.0
Under the semver policy this file declares, this label means the next release is 7.0.0. Two other behavior changes in the same PR carry no label: XtcePacketDefinition(xtce_ns_prefix=None) now binds a default namespace (CHANGELOG.md:57-58), and from_xtce no longer raises for documents binding multiple XTCE-looking URIs (CHANGELOG.md:90-93, listed under Fixed). Either the old constant values were a bug (they never produced a validatable document, and LEGACY_XTCE_XMLNS_ALIASES keeps reading them working), in which case this belongs under Fixed with the compatibility paragraph kept, or this is 7.0 and the other two entries need the same label.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
There was a problem hiding this comment.
Fixed in 3065af0, taking the first of your two options: next release is 6.3.0.
The constant correction moved to ### Fixed and lost the Breaking label, on the grounds you gave — the old values never produced a schema-validatable document, and LEGACY_XTCE_XMLNS_ALIASES keeps reading documents that use them working. The compatibility paragraph stays with it, and the sentence about imported constants now points at it rather than standing alone.
### Changed keeps the two behavior changes that really are changes: xtce_ns_prefix=None binding a default namespace, and xsi:schemaLocation being rewritten to the canonical OMG URL on serialize. Neither is breaking under semver.
🤖 AI-assisted comment, reviewed and approved by @medley56 before posting.
51a3b4b to
3065af0
Compare
|
Review feedback addressed. Branch rebased onto Each inline thread has a reply. This covers the review-level comment and the nine items Copilot folded into its review body, which have no threads to reply on. Test suite organizationDone partially here, rest in #294, since you noted it could be a follow-up and nothing else depended on it. Version-specific tests are no longer interleaved with the general suite — they moved out of
The parametrized fixture, the Copilot review-body items (no threads)Fixed:
Declined:
Verification589 tests pass (579 plus benchmarks), 🤖 AI-assisted comment, reviewed and approved by @medley56 before posting. |
OMG published XTCE 1.3 in July 2025, in a new namespace (http://www.omg.org/spec/XTCE/20250214). The telemetry half of the schema that this library reads is unchanged from 1.2, so the work is version awareness rather than new parsing: a registry of supported versions, the 1.3 XSD bundled for offline validation, and namespace handling that is derived from the document rather than assumed. Along the way this fixes several defects that the version work exposed: structural validation hardcoded the 1.2 namespace and so silently checked nothing for any other namespace; the XTCE 1.1 and 1.2 namespace constants did not match the targetNamespace of the schemas they name; serialized SequenceContainers put BaseContainer before EntryList, which the XSD rejects; and Header attributes were dropped on round trip. Closes #184 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review found the "telemetry subset is identical between 1.2 and 1.3" claim to be wrong in one place that matters: VariableStringType. XTCE 1.2 requires DynamicValue/DiscreteLookupList to declare the raw buffer length and treats LeadingSize/TerminationChar as optional; 1.3 inverts this, making the declared length optional and one of the delimiters required. A schema-valid 1.3 Pascal string or C string therefore failed to parse. Derive the buffer length from the delimiter when no length is declared, capture and write maxSizeInBits (required by both schemas, and never previously written), and warn when serializing a string encoding the target version's schema would reject. Also from review: recognize the https XTCE 1.2 namespace URI written by space_packet_parser <= 6.2 and repair it on serialize; guard the xtce_standard_version setter against an inconsistent namespace mapping; replace a vacuous 1.3 fixture test with one that cannot pass without the 1.3 schema; reclassify the namespace-constant correction as a breaking change in the changelog. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A multi-byte terminator's byte pattern can occur straddling two characters: b"\x00\x00" appears inside b"\x41\x00\x00\x42", which in UTF-16BE is the two characters U+4100 U+0042 and contains no terminator. Searching byte by byte terminated such a string early, leaving a partial character that then failed to decode. Found while testing the new XTCE 1.3 derived-buffer-length scan, but the existing declared-buffer-length path had it too, so both now share one character-aligned search. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…t slope default Two defects found in second review. The character-aligned terminator scan took the character width from the terminator's own byte length, but UTF-8 is in the supported encoding set and is variable width: a two-byte terminator such as c2a5 (U+00A5) sits at an offset that is not a multiple of two, so the scan stepped over it. Take the width from the encoding instead, and byte-scan the variable-width and single-byte encodings, which is safe because UTF-8 is self synchronizing. Separately, an omitted LinearAdjustment slope defaulted to 0, collapsing the adjustment to a constant. XTCE 1.3 declares a default of 1 and 1.2 declares none, so `<LinearAdjustment intercept="8"/>` means f(x) = x + 8. With a zero slope this silently produced the wrong length for a dynamically sized string or binary field, corrupting every later field. Also documents the (unspecified) bit units of a LeadingSize tag value, and records the two remaining silent behavior changes in the changelog. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The character-width table entries for the bare UTF-16 and UTF-32 spellings, and for UTF-32 generally, were load-bearing but untested: deleting one left the suite green. Parametrize over all six fixed-width encodings, with the expected widths written out in the test rather than read from the table under test. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Restore the C-level scan for a termination character. The character-aligned search introduced with XTCE 1.3 support replaced bytes.index with a Python loop that sliced at every step, and that loop runs on the pre-existing declared-length path too, so it slowed parsing of every terminated string in a 1.2 definition: measured at 10x for a 16-byte string, 113x at 256 bytes and 469x at 1024. Scan with bytes.find and check the alignment of each hit instead; for a single-byte character width every hit is aligned, so the cost is one C call as it was before, and only the fixed-width multi-byte encodings loop again. Bound the XTCE 1.3 leading-size path by maxSizeInBits. The size tag comes from the packet and is the only thing determining how much of it the string consumes, so an oversized tag silently ate bytes belonging to later fields. Scoped to the derived path: applying the same check to the declared-length paths would start rejecting documents that parse today. Read an empty <TerminationChar/> as the null terminator both schemas declare as its default, rather than as an absent delimiter, which rejected a schema-valid C string. Correct the constructor docstring, whose "only one of ..." sentence contradicted the paragraph below it: a declared buffer length may be combined with a delimiter, and it is the two delimiters that are mutually exclusive. Adds a fast benchmark parsing a terminated string, since the repo had none covering string parsing and the regression was invisible to the suite.
Preserve SpaceSystem/@systemtype and @assetType, added in XTCE 1.3, as the new space_system_type and asset_type fields. They were read past and dropped, so round-tripping a 1.3 document silently replaced them with the schema default. Serializing as 1.2, where the attributes do not exist, drops them with a warning rather than losing them silently. Warn when a time parameter type's unit is not in the target version's TimeUnitsType enumeration: 1.2 spells it picoSeconds and 1.3 picoseconds, and 1.3 adds several values, so retargeting could produce a document that fails schema validation with no diagnostic. The unit is document content, so it is reported rather than rewritten. Refuse to select a version whose schema is not bundled as a write target. xtce_uri_for_version resolves any known version, including 1.1, so a definition could be pointed at an XSD that is not shipped, and validating its output would fall back to a network download. Detection stays permissive, so a 1.1 document is still reported as 1.1. Name the cause when a document uses the https 1.2 namespace URI written by space_packet_parser 6.2 and earlier, instead of the generic "does your namespace match your schema?" text, and say that re-serializing repairs it. Also corrects the constructor docstring, which said xtce_standard_version was ignored when ns was supplied while the code raises, and the name-reference note, which claimed 1.2 permitted spaces in parameter names: 1.2's NameType already excluded space, and what 1.3 tightened is the reference patterns.
Version-awareness tests were appended as a trailing block to
test_definitions.py and test_validation.py, interleaving XTCE 1.2 and 1.3
expectations with the general suite. Split them into modules whose remit
is stated in the module docstring:
test_xtce_1_3_features.py constructs that exist only in 1.3
test_version_conversion.py detection, retargeting, version-dependent
serialization
test_version_validation.py version-dependent validation
test_versions.py keeps the registry tests. The parametrized fixture,
markers and versioned test-data directories from the review are left for
the follow-up issue.
Covers the two branches that made up the uncovered patch lines: the
non-byte-aligned terminator scan and the setter on a definition with no
namespace. Adds tests for the empty <TerminationChar/> default, the
maxSizeInBits bound on the leading-size path, rejection of unwritable
version targets, the legacy-namespace diagnostic, the time-unit warnings,
and systemType/assetType round-tripping.
Replaces the real JPSS packet file in the unit suite with a synthetic
packet, per the project rule reserving mission data for integration
tests; the same assertion already runs on those bytes in the integration
parity test. Extends that parity test to SUDA and IDEX, which were
covered for definition parity but not for packet decoding.
Adds a guard that test_xtce_1_3.xml stays a pure version-swap of
test_xtce.xml, which several tests assume and nothing enforced.
Reclassify the XTCE_1_2_XMLNS / XTCE_1_1_XMLNS correction from a breaking change to a fix, targeting 6.3.0 rather than 7.0.0. The old values never produced a schema-validatable document, and LEGACY_XTCE_XMLNS_ALIASES keeps reading documents that use them working, so the compatibility paragraph carries the migration path without a major bump. Correct the name-reference section of the XTCE validation guide, which told users that parameter names containing spaces are valid XTCE 1.2. They are not: 1.2's NameType pattern already excludes space. The 1.2 to 1.3 tightening is in the reference patterns, which is what the section now describes. Documents the SpaceSystem metadata and time-unit behavior added alongside.
3065af0 to
0bd3c37
Compare
greglucas
left a comment
There was a problem hiding this comment.
Reading this from a high-level there is a lot of extra handling for 1.2 vs 1.3 even though there isn't much that changed. I'm wondering if we really need to support writing 1.2 documents versus just being able to read them still which I think is compatible regardless? If someone gives us a 1.2 document we can still parse it equivalently, but if someone gives us a 1.3 document we issue warnings if we try to serialize to a 1.2 version. That seems like an unnecessary guard for all this extra code where we could just add the new features/defaults and explain what changed in the version.
| # The XTCE 1.3 XSD gives slope a default of 1 and intercept a default of 0, i.e. an omitted | ||
| # attribute leaves the value untouched. XTCE 1.2 declares no default for slope, but defaulting | ||
| # it to 0 would collapse the adjustment to a constant, which is never the intent. |
There was a problem hiding this comment.
I don't think you need explanations for 1.3 vs 1.2 differences here. You should just document this as a changed default in this version of space packet parser.
Closes #184.
OMG published XTCE 1.3 in July 2025, in a new namespace (
http://www.omg.org/spec/XTCE/20250214). This adds 1.3 support in addition to 1.2 — nothing about 1.2 is dropped, and 1.2 parse output is byte-identical tomain(verified at the head commit: every decoded and raw value for all four bundled mission definitions over their real packet files, plus the full type/parameter/container inventories — 67,596,155 bytes, identical).The issue also asked to "re-evaluate the way we handle 'standard' XTCE namespacing versus parsing it out of the document," which turned out to be where most of the real bugs were.
Targets 6.3.0. No breaking changes — see Compatibility.
What supporting 1.3 actually required
The two OMG schemas were diffed element by element to find what genuinely affects this library. Most of the 1.2→1.3 delta is on the command/argument side, which we don't parse. Three changes reach the telemetry half:
Header/@versionis not the standard version — per the XSD it's a free-form descriptor for the document itself. That misconception was baked into the code.)VariableStringType. 1.2 requires aDynamicValue/DiscreteLookupListto declare the raw buffer length and treatsLeadingSize/TerminationCharas optional. 1.3 inverts this: the declared length is optional and exactly one delimiter is required — so a Pascal string or C string can be described without inventing a separate length parameter. A schema-valid 1.3 document of that shape previously raisedValueErroron parse.LinearAdjustment/@slopegained a default of1. We defaulted a missing slope to0, collapsing the adjustment to a constant.Two smaller ones also land:
SpaceSystem/@systemTypeand@assetTypeare new in 1.3, andTimeUnitsTyperespellspicoSecondsaspicosecondswhile adding several values.Changes
Version awareness
space_packet_parser.xtce:SUPPORTED_XTCE_VERSIONS,LATEST_XTCE_VERSION,DEFAULT_XTCE_VERSION, per-version namespace/schema-URL/time-unit mappings, andxtce_version_from_uri/xtce_uri_for_version/xtce_nsmap.XtcePacketDefinition.xtce_standard_versionreports the version implied by the namespace URI; assigning to it retargets a definition.ValidationResult.xtce_versionandspp validatereport it too.from_xtcereads the root element's actual namespace and prefix instead of scanning the nsmap for a URI containing"xtce".XTCE 1.3 constructs
LeadingSize(the size tag plus the content length it reports, bounded bymaxSizeInBits) or aTerminationChar(up to and including the terminator).systemTypeandassetTyperound-trip, as the newspace_system_typeandasset_typefields.Where a construct is version-specific
Serializing warns rather than silently producing an invalid document, naming the parameter types involved. This covers variable-length strings in either direction, time units the target version doesn't define, and the 1.3-only
SpaceSystemattributes when writing 1.2. Content is never rewritten — a unit is what the author wrote.Bugs this exposed
All pre-existing, all in the changelog:
_validate_xtce_structurehardcoded the 1.2 namespace in its XPath, so reference-integrity checks matched nothing — and reported no errors — for any other namespace: 1.3, a non-standard mission URI, or none at all. It silently "passed" everything.XTCE_1_2_XMLNSusedhttpswhere the realtargetNamespaceishttp, andXTCE_1_1_XMLNSnamed a URI no XTCE schema declares. A definition built from scratch serialized to a document no schema recognized.SequenceContainerserialization emittedBaseContainerbeforeEntryList; the XSD (both versions) orders it after.maxSizeInBitswas never written, so every variable-length string we serialized was schema-invalid in both versions.b"\x00\x00"matched insideb"\x41\x00\x00\x42"— two ordinary characters — truncating the string. The search is character-aligned now, and still a single C-levelbytes.findcall for the single-byte and UTF-8 encodings that cover nearly every real definition.<TerminationChar/>was read as an absent delimiter rather than the null terminator both schemas declare as its default, rejecting a valid C string.Header/@versionand@validationStatuswere dropped on round trip.Serialized documents now declare
xsi:schemaLocationfor their own version, so a definition written by this library is schema-valid as-is.Compatibility
Nothing here is breaking, so this is a minor release.
The
XTCE_1_2_XMLNS/XTCE_1_1_XMLNScorrection changes two constant values, but the old values never produced a schema-validatable document — they matched no schema'stargetNamespace. Documents written byspace_packet_parser6.2 and earlier carry the oldhttpsURI; they are still recognized as XTCE 1.2 (LEGACY_XTCE_XMLNS_ALIASES), validation names the cause instead of giving a generic namespace error, and re-serializing repairs the URI. Code that string-compares namespace URIs against these constants will see the new values.Two smaller behavior changes are listed under
### Changed:xtce_ns_prefix=Nonenow binds a default namespace rather than producing a document with no namespace, and serializing rewritesxsi:schemaLocationto the canonical OMG URL.Testing
589 tests (108 new), passing on ubuntu/macos/windows across Python 3.10–3.14.
test_xtce_1_3_only_features.xmluses three constructs that exist only in 1.3. Rewritten into the 1.2 namespace it must still fail against the 1.2 schema, and fail on its content rather than on a namespace mismatch — so the test can't pass without the 1.3 schema actually being consulted.test_xtce_1_3.xmlto being a pure version-swap oftest_xtce.xml, which several tests assume.test_xtce_1_3_features.py(1.3-only constructs),test_version_conversion.py(detection, retargeting, version-dependent serialization),test_version_validation.py(version-dependent validation),test_versions.py(the registry).Review
Reviewed adversarially over three rounds before opening, verifying claims against the OMG schemas and the CCSDS green book rather than against commit messages. That found seven substantive defects — including the
VariableStringTypegap and theLinearAdjustmentslope default, both of which had originally been dismissed as validation-only.The PR review then found nine more, all addressed here. The one⚠️ was a real performance regression introduced mid-PR: making the terminator search character-aligned replaced
bytes.indexwith a Python loop that sat on the existing 1.2 path. Fixed and measured — end-to-endparse_valueis within 1–5% ofmainacross a 256× range of string lengths, with a flat ratio.Follow-ups
xtce12/xtce13markers, versioned test-data directories). Split out to keep both diffs reviewable.UTF-16/UTF-32codecs ignore XTCE'sbyteOrderwithout a BOM. Pre-existing onmain; fixing it changes decoded values for existing definitions.Also deliberately not changed:
XtcePacketDefinition.xtce_versionstill meansHeader/@version(the document's version), which reads confusingly next to the newxtce_standard_version. Renaming it is a breaking change worth doing on its own.🤖 Generated with Claude Code