Skip to content

feat: Recover schema-less payloads and offer TDH's display strings - #85

Merged
m96-chan merged 2 commits into
feat/72-nested-structsfrom
feat/72-wpp-and-formatting
Sep 6, 2026
Merged

m96-chan merged 2 commits into
feat/72-nested-structsfrom
feat/72-wpp-and-formatting

Conversation

@m96-chan

@m96-chan m96-chan commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

The last two things #72 asked for. Stacked on #84 — base is feat/72-nested-structs, so review that first; GitHub will retarget this to main when #84 merges.

With this, every item in #72's proposed approach is either done or has a stated reason it cannot be.

Changes

1. Events TDH cannot describe keep their payload

TdhGetEventInformation has no schema for WPP without a matching TMF, nor for a manifest provider whose manifest is not installed here. Those events came back with an empty dict and nothing else — the payload was read, then dropped.

There are no names to report it under, so it cannot become properties. Keep the bytes instead. EtwEvent.raw_data has existed all along but was never populated, the comment in session.rs explaining that ferrisetw keeps its user_buffer private — but the record handed to TDH carries the same bytes and this module already holds it.

Only for schema-less events; a second copy of every decoded event's data would cost memory for nothing.

Also set_wpp_tmf_search_path, passing TDH a TDH_CONTEXT_WPP_TMFSEARCHPATH so a caller who has .tmf files gets WPP decoded properly instead of falling back to bytes. Unset by default, in which case TDH is passed no context at all.

2. TDH's display strings, opt-in

#72 asked whether typed values or TdhFormatProperty's strings should be the default and whether the other should be available. Typed values stay the default — the Parquet/Arrow exporters need them — and the strings become a second map, formatted_properties, behind set_property_formatting. Off by default, so the normal path pays nothing.

TdhFormatProperty is given one property's bytes, already isolated by TdhGetProperty, rather than the event payload with running offsets. That walk is what this module avoids on purpose, and skipping it costs nothing because TDH has already located the value.

Related Issue

Advances #72 to completion — see the closing section.

Test Plan

Added/Modified Tests

  • 3 unit tests in tdh.rs (formatting default/toggle, TMF path round-trip, no context when unset).
  • 7 tests in test_etl_reader.py over a new no_schema.etl fixture.

The existing fixtures cannot exercise the schema-less path — everything in sample.etl and nested_struct.etl decodes cleanly, which is the point of them. no_schema.etl is the capture that used to be sample.etl before #76 replaced it. It was never a good property-parsing fixture (unrelated telemetry, no Kernel-Process events at all), and that is exactly what makes it right here: 73 of its 149 events have no schema TDH can find.

Test Results

Payload recovery, on that fixture:

no_schema.etl: 149 events
  decoded (properties non-empty): 74
  no properties:                  75
  carrying raw_data:              73
  payload bytes recovered:        77058
  decoded events also holding raw_data (should be 0): 0

  event_id=0 provider_id=9b79ee91-b5fd-41c0-a243-4248e266e9d0
    properties = {}
    raw_data[64] = b'C\x00:\x00\\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00\\x00s\x00y\x00s\x00t\x00e\x00'

77 KB that used to be discarded, and plainly the real thing — UTF-16 C:\Windows\syste..., \Device\Harddisk....

Display strings, on a live DNS-Client capture. These are the properties where the string differs from str() of the typed value, i.e. what typed values cannot express:

with formatting OFF: formatted_properties non-empty on 0 / 34 events
with formatting ON:  formatted_properties non-empty on 33 / 34 events

event_id=1001 provider=Microsoft-Windows-DNS-Client
    DynamicAddress   typed=1                    formatted='dynamic '
    Address          typed=b'\x02\x00\x00\x00\xc0\xa82\x01\x00...'
                                                formatted='192.168.50.1'
event_id=3009
    QueryBlob        typed=2525199111760        formatted='0x24BF1989650'

A value map resolved (1 → dynamic), a raw SOCKADDR rendered as an IP, and pointers in hex.

One bug caught while checking this rather than after: TdhFormatProperty renders a single value, so handing it an array silently described only the first element — a 176-byte TimeZoneInformation came out as "228", the same failure mode as the array bug in #84. Arrays and structs are now excluded from the string view; properties reports them in full.

TMF plumbing:

default search path: None
after set:           'C:\Windows\Temp'
manifest events unchanged:  True (34 events)
no-schema events unchanged: True (149 events)
nonexistent path, manifest events still fine: True

Suite:

pytest        215 passed, 102 skipped     (the 102 are #77, fixed in #82)
cargo test     63 passed,   0 failed
cargo clippy --all-targets -- -D warnings  clean
cargo fmt --check / ruff / black           clean

Checklist

  • Tests have been added/updated
  • All tests pass
  • Ready for code review
  • Documentation updated (if applicable)
  • CHANGELOG.md updated (if applicable)

Where #72 stands after this

#72 item Status
Enumerate properties via TDH instead of a guess list #76
EventValue variants for SID / FILETIME / GUID / binary / pointer #76
Arrays #84
Nested structures #84
MOF (classic) as well as manifest Works — MSNT_SystemTrace decodes fully
PropertyParamLength / PropertyParamCount #84
Providers with no manifest installed This PR — payload preserved
WPP fallback This PR — TMF search path, see caveat
Typed vs TdhFormatProperty strings, other opt-in This PR — typed default, strings opt-in
Schema cache per (provider, event id, version) #76

The one caveat, stated plainly. The TMF search path is not verified end to end. This machine has no .tmf files and none of the WPP tooling that produces them (tracefmt, tracepdb, traceview are all absent, and public Microsoft PDBs do not carry WPP data). What is verified is that the context reaches TDH, that setting a path leaves all other decoding byte-identical, and that a path matching nothing is harmless. Actual WPP decoding needs someone with TMFs to confirm. I would rather say so than let it look verified.

Two smaller things also not done, both from #84's notes:

  • No fixture for arrays of structs. I tried hard: no ambient provider emits one reliably in a short window (Kernel-Processor-Power event 181 fired once in a 5-second capture and never in ~20 shorter ones; Kernel-Pep, Kernel-Power, Kernel-General, Direct3D12 produced none), and .NET's EventSource refuses to serialise an array of structs at all — it throws TargetInvocationException and writes an EventSourceException string instead of the event. Covered by unit tests and the tracerpt comparison in feat: Decode nested structure properties #84.
  • Nested structs inside the string view are skipped, as described above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

@m96-chan

m96-chan commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Holding this one. Two blockers before the content is even the question.

1. This PR has never been tested by CI. Not once.

$ gh run list --branch feat/72-wpp-and-formatting
(no runs)

ci.yml triggers on pull_request: branches: [main], and this targets feat/72-nested-structs. A stacked PR therefore gets no CI at all — not a partial run, none. Every green tick you might expect here is simply absent, so nothing in the diff has been compiled, linted or tested by anything but your machine.

That is worth fixing in the workflow independently of this PR, because the failure mode is silent: a stacked PR looks unremarkable in the UI while being completely unverified. Adding pull_request: without a branches filter, or listing the feature branches, would cover it.

2. Blocked behind #84, which is red. Its two nested-struct tests fail on the runner, and my read is that the fixture's provider has no manifest on Windows Server rather than that the code is wrong — details in that PR. This cannot be evaluated on its merits until the base is green.


On the content, from reading rather than running, because I have no Windows and this has no CI:

Recovering the payload for schema-less events is clearly right. Reading the bytes and dropping them was the worst of both worlds, and confining raw_data to events TDH cannot describe is the correct trade — populating it always would double the memory for every decoded event to no benefit. Using the old sample.etl as no_schema.etl is a nice reuse: a fixture that was bad at proving decoding works is genuinely good at proving the fallback works, and 73 of 149 events is real coverage rather than a contrived case.

Typed values staying the default is the right call, and I agree formatted_properties belongs beside them rather than replacing them. Handing TdhFormatProperty a single property's bytes that TdhGetProperty already located, instead of walking the payload with running offsets, keeps the safety property this module was built around.

The design question I would want settled before merge: set_property_formatting and set_wpp_tmf_search_path are process-global — an AtomicBool and a Mutex<Option<Vec<u16>>> in statics. That means two EtwSessions in one process cannot disagree, a library that flips formatting changes behaviour for its caller's sessions, and the setting is invisible at the point where events are consumed. TMF search path is at least arguably machine-global; formatting is a per-consumer preference and reads more naturally as a session or reader option.

Not necessarily a blocker — a global is defensible for something set once at startup — but it is a public API that will be awkward to narrow later, so it deserves a deliberate decision rather than arriving as an implementation detail. What made you choose global over per-session?

Once #84 is green and this is retargeted at main so CI actually runs, I will review the diff properly against real results.

@m96-chan
m96-chan force-pushed the feat/72-wpp-and-formatting branch from 5b0ec2f to dfd0ee2 Compare September 6, 2026 02:35
@m96-chan m96-chan mentioned this pull request Sep 6, 2026
3 of 5 tasks
m96-chan and others added 2 commits September 6, 2026 11:39
The last two gaps #72 listed for the TDH parser.

**Events TDH cannot describe.** `TdhGetEventInformation` has no schema for WPP
events without a matching TMF, nor for manifest providers whose manifest is not
installed on this machine. Those events came back with an empty property dict
and nothing else: their payload was read and then dropped. There are no names
to report it under, so it cannot become properties -- keep the bytes instead.
`EtwEvent.raw_data` has existed all along but was never filled in, the comment
in session.rs explaining that ferrisetw keeps its `user_buffer` private. The
record handed to TDH carries the same bytes and this module already holds it,
so read `UserData` from there. Only for events with no schema: carrying a
second copy of every decoded event's data would cost memory for nothing.

`set_wpp_tmf_search_path` passes TDH a TDH_CONTEXT_WPP_TMFSEARCHPATH so a
caller holding .tmf files can have WPP events decoded properly rather than
falling back to bytes. Unset by default, in which case TDH is passed no
context at all, exactly as before.

**TDH's display strings.** #72 asked whether typed values or
`TdhFormatProperty`'s strings should be the default and whether the other
should be available. Typed values stay the default -- the Parquet/Arrow
exporters need them -- and the strings become an opt-in second map,
`formatted_properties`, behind `set_property_formatting`. Off by default, so
the normal path pays nothing.

They earn their place by expressing what typed values cannot, because TDH
applies the manifest's value maps and output types:

    DynamicAddress   typed=1                     formatted='dynamic '
    Address          typed=b'\x02\x00\x00\x00\xc0\xa82\x01...'
                                                 formatted='192.168.50.1'
    QueryBlob        typed=2525199111760         formatted='0x24BF1989650'

`TdhFormatProperty` is given one property's bytes, already isolated by
`TdhGetProperty`, rather than the event payload with running offsets. That
walk is what the rest of this module avoids on purpose, and skipping it costs
nothing because TDH has already located the value. Arrays and structs are left
out of the string view: `TdhFormatProperty` renders a single value, so handing
it an array would silently describe only the first element -- a 176-byte
TimeZoneInformation rendered as "228". `properties` reports those in full.

The TMF search path is not verified end to end. This machine has no .tmf files
and none of the WPP tooling that produces them. What is checked is that the
context reaches TDH, that setting a path leaves all other decoding
byte-identical, and that a path matching nothing is harmless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn
The existing fixtures cannot exercise the schema-less path: everything in
sample.etl and nested_struct.etl decodes cleanly, which is the whole point of
them. Add no_schema.etl -- the capture that used to be sample.etl before #76
replaced it. It was never a good property-parsing fixture, holding unrelated
telemetry and no Kernel-Process events at all, but that is exactly what makes
it the right one here: 73 of its 149 events have no schema TDH can find.

Tests assert the payload survives for those, that decoded events do not also
carry a redundant copy, that display strings appear only when switched on and
leave the typed values untouched, and that a TMF search path matching nothing
leaves all other decoding byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn
@m96-chan
m96-chan force-pushed the feat/72-nested-structs branch from f089361 to 036723e Compare September 6, 2026 02:39
@m96-chan
m96-chan force-pushed the feat/72-wpp-and-formatting branch from dfd0ee2 to 53c011c Compare September 6, 2026 02:40
@m96-chan

m96-chan commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Heads-up on CI for this one: it will not run until #84 merges.

ci.yml triggers on pull_request: branches: [main], and this PR targets feat/72-nested-structs so the workflow never fires. gh pr checks 85 reports "no checks reported". That is a property of the stacking, not something wrong with the branch — once #84 merges GitHub retargets this to main and CI runs normally.

Rebased onto the current main (now including #82 and #83) and onto the updated #84. Local run against that state:

pytest        318 passed, 0 skipped
cargo test     66 passed, 0 failed
cargo clippy --all-targets -- -D warnings   clean
cargo fmt --check / ruff / black            clean

Worth noting the skip count is now genuinely zero locally, because #82 landed and pandas is installed here; CI will show 2 skipped for the pandas-gated to_dataframe tests until the export extra is installed there, which is the open question I left on #82.

@m96-chan
m96-chan merged commit c6c5f35 into feat/72-nested-structs Sep 6, 2026
m96-chan added a commit that referenced this pull request Sep 6, 2026
* feat: Decode nested structure properties

Struct-typed properties were skipped outright, so their contents were
unreachable from Python. On this machine 1512 properties across 904 providers
are declared as structs, and a single Kernel-Processor-Power core-parking
event carries ten of them, all previously absent from the property dict.

TDH addresses a struct member with a chain of PROPERTY_DATA_DESCRIPTORs -- the
struct with the index of the instance, then the member -- so `property_bytes`
grows into `property_bytes_at`, taking the whole path. Struct values are read
recursively into `EventValue::Struct`, and a struct that is itself an array
into an `Array` of them. Recursion is capped by MAX_STRUCT_DEPTH: a schema
whose members pointed back at an enclosing struct would otherwise not
terminate.

Two supporting changes fall out of this.

`read_layout` now walks all `PropertyCount` properties rather than only the
top-level ones, because struct members live past `TopLevelPropertyCount` and
`structType` addresses them by their index into that same array. Only the
top-level entries become keys of the returned map; the rest are reached
through the struct that owns them.

Keeping that vector positionally aligned with `EventPropertyInfoArray` also
fixes #81. A property with no name used to be dropped from the vector, which
shifted every later index, so `PropertyParamCount` resolved its count from the
wrong sibling. Unnamed properties now occupy their slot as a placeholder that
is skipped when values are read. The struct work needs this alignment to be
correct at all, so it is not separable.

Verified against tracerpt on live Kernel-Processor-Power events. For the same
core-parking event tracerpt reports Group {Number 0, Affinity 0xFFFF} and
OldPark/NewPark/NewSoftPark {Number 0, Affinity 0x50C0}, and those are exactly
the values that now come through. For an array of structs -- HardwareTable,
64 elements of {LogicalProcessorIndex, WorkLoadClassIndex, PerformanceClass,
EfficiencyClass} -- every element matches too, and unlike tracerpt, which
flattens all 64 into one repeated list because XML cannot express the
grouping, the elements stay grouped.

Closes #81

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* test: Cover nested structure decoding with a fixture that has some

sample.etl cannot exercise the struct path: Microsoft-Windows-Kernel-Process
declares no struct-typed property, so the feature would have had no automated
coverage at all and would only ever have been checked by hand on a live
session.

Add nested_struct.etl, 40 KB of Kernel-Processor-Power core-parking events
carrying seventeen distinct struct properties, plus the script that produces
it. The Algorithm keyword (0x20) is what selects those events; wider masks add
hundreds of KB of unrelated telemetry without adding a struct, which is what
keeps the fixture the same order of size as sample.etl.

The tests assert the members are reachable, that they do not also leak in
beside their struct at the top level, that structs survive into to_dict(), and
that nested values are Python-native recursively.

Arrays of structs are not in the fixture. The events carrying them fire rarely
enough that capturing one reliably would have meant a multi-megabyte file;
that case is covered by the bounds tests in tdh.rs and was verified by hand
against tracerpt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* test: Make the nested-struct fixture decode on machines other than mine

CI failed on all five Python versions: the fixture produced its events, but
not one struct-valued property, so both struct assertions fired.

The cause is the fixture, not the parser. It was a capture of
Kernel-Processor-Power, a manifest provider, and decoding a manifest event
needs that manifest installed *and of a matching version*. It decoded on the
machine that recorded it and yielded nothing on the CI runner's different
Windows build. A fixture that only decodes where it was made is worse than no
fixture, because it looks like coverage.

Record from a TraceLogging provider defined in the generator script instead.
Such events embed their own schema, so the ETL decodes identically anywhere,
which is a property of the format rather than something to hope for. Windows
ships the C# compiler the script uses, so this still needs nothing beyond a
stock machine, and the fixture drops from 40 KB to 16 KB.

Each event now carries two { Number, Affinity } structs with known values, so
the tests can assert the contents outright rather than just their shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* fix: Drop the length prefix from counted strings and blobs

TDH returns the two-byte count as part of the value for the counted in-types,
so it was being decoded as content. The new fixture's `Label` holds
"core-state" and came back as "\u{14}core-state", the `\u{14}` being its own
20-byte length. The same was visible earlier in a TraceLogging capture, where
"hello-from-32bit" read as " hello-from-32bit" -- the leading space was the
32-byte count, quiet enough to miss.

Affects win:CountedString, win:CountedAnsiString, win:CountedBinary and their
MOF equivalents.

The prefix is only removed when it actually describes the rest of the buffer.
Stripping unconditionally would eat the first character of any value that TDH
had already stripped, and this way a provider or TDH version that behaves
differently loses nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* feat: Recover schema-less payloads and offer TDH's display strings (#85)

* feat: Recover schema-less payloads and offer TDH's display strings

The last two gaps #72 listed for the TDH parser.

**Events TDH cannot describe.** `TdhGetEventInformation` has no schema for WPP
events without a matching TMF, nor for manifest providers whose manifest is not
installed on this machine. Those events came back with an empty property dict
and nothing else: their payload was read and then dropped. There are no names
to report it under, so it cannot become properties -- keep the bytes instead.
`EtwEvent.raw_data` has existed all along but was never filled in, the comment
in session.rs explaining that ferrisetw keeps its `user_buffer` private. The
record handed to TDH carries the same bytes and this module already holds it,
so read `UserData` from there. Only for events with no schema: carrying a
second copy of every decoded event's data would cost memory for nothing.

`set_wpp_tmf_search_path` passes TDH a TDH_CONTEXT_WPP_TMFSEARCHPATH so a
caller holding .tmf files can have WPP events decoded properly rather than
falling back to bytes. Unset by default, in which case TDH is passed no
context at all, exactly as before.

**TDH's display strings.** #72 asked whether typed values or
`TdhFormatProperty`'s strings should be the default and whether the other
should be available. Typed values stay the default -- the Parquet/Arrow
exporters need them -- and the strings become an opt-in second map,
`formatted_properties`, behind `set_property_formatting`. Off by default, so
the normal path pays nothing.

They earn their place by expressing what typed values cannot, because TDH
applies the manifest's value maps and output types:

    DynamicAddress   typed=1                     formatted='dynamic '
    Address          typed=b'\x02\x00\x00\x00\xc0\xa82\x01...'
                                                 formatted='192.168.50.1'
    QueryBlob        typed=2525199111760         formatted='0x24BF1989650'

`TdhFormatProperty` is given one property's bytes, already isolated by
`TdhGetProperty`, rather than the event payload with running offsets. That
walk is what the rest of this module avoids on purpose, and skipping it costs
nothing because TDH has already located the value. Arrays and structs are left
out of the string view: `TdhFormatProperty` renders a single value, so handing
it an array would silently describe only the first element -- a 176-byte
TimeZoneInformation rendered as "228". `properties` reports those in full.

The TMF search path is not verified end to end. This machine has no .tmf files
and none of the WPP tooling that produces them. What is checked is that the
context reaches TDH, that setting a path leaves all other decoding
byte-identical, and that a path matching nothing is harmless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* test: Cover schema-less payloads, display strings, and the TMF path

The existing fixtures cannot exercise the schema-less path: everything in
sample.etl and nested_struct.etl decodes cleanly, which is the whole point of
them. Add no_schema.etl -- the capture that used to be sample.etl before #76
replaced it. It was never a good property-parsing fixture, holding unrelated
telemetry and no Kernel-Process events at all, but that is exactly what makes
it the right one here: 73 of its 149 events have no schema TDH can find.

Tests assert the payload survives for those, that decoded events do not also
carry a redundant copy, that display strings appear only when switched on and
leave the typed values untouched, and that a TMF search path matching nothing
leaves all other decoding byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
m96-chan added a commit that referenced this pull request Sep 6, 2026
* fix: Actually decode WPP events, through a decoding handle

The TMF search path added in #85 never worked. It was reasoned from the
documentation and only checked for not disturbing anything, which it did not,
because it did nothing at all. Building a real WPP provider and capturing its
events showed why.

`TdhGetEventInformation` and `TdhGetProperty` accept a `TDH_CONTEXT` array and
ignore it for WPP. Verified against a live capture: the TMF search path, a
single TMF file, both together, a PDB path, and the TRACE_FORMAT_SEARCH_PATH
environment variable all leave `FormattedString` reading "No Format
Information found", while tracefmt decodes the same trace from the same TMF
directory without complaint.

Format information only reaches TDH through a decoding handle:
`TdhOpenDecodingHandle`, then `TdhSetDecodingParameter` for each source, then
`TdhGetWppMessage`. All three source kinds work that way, so all three are
offered -- and `set_wpp_pdb_path` is the pick of them, since TDH reads the
format information straight out of the PDB with no `tracepdb` step.

Two things only real events could have shown:

`TdhGetWppMessage` reports ERROR_SUCCESS from its sizing call, not
ERROR_INSUFFICIENT_BUFFER as the rest of TDH does. Treating the usual protocol
as the only valid one turned a working decode back into "no format
information", and cost an hour of looking in the wrong place.

It also returns a message for events that are not WPP at all, so a
`FormattedString` began appearing on Kernel-Process events as soon as any
source was configured. The layout now records `DecodingSource`, and only
events TDH decodes as WPP are asked.

Decoded messages replace the placeholder `FormattedString` rather than landing
somewhere new, so there is one place to look instead of a property that says
"No Format Information found" beside a field holding the answer. Nothing is
asked of TDH at all until a source is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* test: Cover WPP decoding with a real WPP capture and its TMF

Nothing in the repository could exercise WPP: no fixture contained a WPP
event, and none of the machines involved had a `.tmf`. That is precisely why
#85 shipped a WPP feature that did not work.

Add `wpp.etl`, three events from a purpose-built WPP provider, together with
the `.tmf` that decodes them. Both are needed -- a WPP capture without its
format information is undecodable by construction -- and both are small (8 KB
and 344 bytes). The expected strings in the test are what tracefmt, Microsoft's
own formatter, produces from the same pair, so the tests are pinned to an
independent answer rather than to our own output.

The generator script is committed for reproducibility but, unlike the other
fixture scripts, cannot run on a bare machine: it needs `tracewpp` and
`tracepdb` from the Windows SDK and `cl` from the MSVC build tools. It ends by
running tracefmt over what it produced, so the expected values can be
re-derived rather than trusted.

The tests assert the placeholder appears without a source, the real messages
appear with one, that removing the source goes back to the placeholder, and
that a source matching nothing leaves every other event's properties
byte-identical -- the last of which caught the `FormattedString` leaking onto
non-WPP events.

The PDB route is not covered here: shipping a PDB as a fixture to test one
extra TDH_CONTEXT value is not worth its size. It shares the code path and was
verified by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

* docs: Document event property decoding, and cover the PDB route in CI

Two loose ends from the WPP work.

**Documentation.** Nothing in docs/ described property decoding at all -- not
arrays, not nested structures, not raw payloads, and not WPP. Add an Event
Properties page covering the lot, with `set_wpp_pdb_path` given the prominence
it earns: it is the only route that needs no SDK tooling, and it was previously
discoverable only by reading the API. The page also records that WPP derives a
trace GUID from the source file's full path, which is not obvious and bites
anyone trying to decode a capture with a PDB built elsewhere.

**CI.** The committed wpp.etl covers the two TMF routes but cannot cover the
PDB one, for exactly that reason: a PDB built anywhere other than where the
capture was made has a different trace GUID. Committing a PDB instead is not
worth it -- ours is 7 MB, which would be the largest file in the repository by
two orders of magnitude, to cover one extra TDH context value.

So the new job builds the provider and captures fresh events on the runner,
where the GUIDs match by construction, and checks all three routes. Both the
Windows SDK (tracewpp, tracepdb, tracefmt) and MSVC are already on
windows-latest, so this needs nothing installed.

`create_wpp_etl.ps1` gained -OutputDir and -IncludePdb so CI can reuse it, and
the provider source moved out of a here-string into wpp_provider/wpptest.c so
both the fixture and CI build the same file. The build has to happen in that
fixed directory rather than a temp one, or the committed .tmf would stop
matching the committed capture; its artifacts are gitignored.

The verification is a script rather than a pytest module: it depends on
artifacts only CI produces, and expressing that as a test which skips unless
some environment variables are set is precisely the kind of quiet skip that let
the first attempt at WPP ship broken. It exits non-zero instead.

The fixture is regenerated, since moving the build changed the trace GUID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhPcm483PrDmuQEUSALBFn

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant