Files
MicrOBU/app/src/test/java/com/hawhamburg/micr0bu/CamEncodeGoldenTest.kt
T
Ashin Walpola b2b57fa39e Vendor the ASN.1 modules the codecs are verified against; untrack IDE churn
asn1/
Three tests assert exact bytes - CamEncodeGoldenTest, DenmAirReceiveTest and
SpatemUperCodecTest - and their expected values came from asn1tools compiled
against ETSI modules that existed only as an untracked working copy on one
machine. A golden-byte fixture nobody else can regenerate is a fixture nobody
can safely touch, so the modules are now in the repo.

Only the seven .asn files those tests need are copied, 576 KB of a 4.2 MB
checkout; the upstream Rust parser is not used by this project at all. Verified
sufficient in isolation: copied into an empty directory, all three specs compile
and reproduce the committed golden CAM bytes byte-identically.

Source is consider it GmbH's C-ITS-Parser (github.com/consider-it/C-ITS-Parser)
at f457426, MIT licensed - LICENSE is retained alongside as that requires. The
schemas themselves are ETSI's standard definitions; upstream's contribution is
assembling them into a compilable set. asn1/README.md records the provenance,
which module pairs with which message, and the rule that matters: never
regenerate a golden fixture from this project's own encoder, because sharing a
mistake between encoder and decoder is exactly the failure these files exist to
catch.

Doc references in the codecs and tests now point at asn1/ instead of the
untracked checkout, and C-ITS-Parser/ is gitignored so the working copy beside
the project is never picked up.

Untracked local state
- .idea/deploymentTargetSelector.xml rewrites itself on every deploy, so it has
  been showing as modified in essentially every commit. Along with
  deviceManager.xml, appInsightsSettings.xml and studiobot.xml it is per-machine
  state, not project configuration.
- obu-firmware/sdkconfig.old is ESP-IDF build output - it is the previous
  sdkconfig, rewritten on every build. sdkconfig.defaults remains tracked, since
  that is the configuration actually chosen.

All five stay on disk; only the tracking is removed. Also ignores
.claude/settings.local.json, which is per-machine, while leaving the skills
beside it committable as project knowledge.
2026-08-21 14:28:57 +02:00

79 lines
3.6 KiB
Kotlin

package com.hawhamburg.micr0bu
import com.hawhamburg.micr0bu.domain.asn1.CamUperCodec
import com.hawhamburg.micr0bu.domain.cam.Cam
import com.hawhamburg.micr0bu.domain.cam.StationType
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Golden-byte test for the CAM this app transmits.
*
* ## Why a byte-for-byte fixture
* This project has now shipped the same class of bug three times: a field encoded with the wrong
* number of bits, which both ends of this codebase then read back with the *same* wrong number.
* Phone and ESP32 agree perfectly with each other and with nothing else, so every internal test
* passes while the frames on air are malformed. It cost a hardware session each time
* (`CurvatureCalculationMode`, the GeoNetworking reserved bytes, and `yawRateConfidence`).
*
* A round-trip test through this codebase's own decoder cannot catch that - it shares the
* mistake. Only an independent implementation can. So the expected bytes below were produced by
* `asn1tools` compiled from the real ETSI modules in `asn1/`: it decoded this
* encoder's output and re-encoded it, and the result was byte-identical to what is asserted here.
* That is stronger than "it parses" - it means this encoder emits exactly what the reference
* encoder emits.
*
* If a field width is ever "tidied up", this test fails. Do not regenerate the expected value from
* this encoder's own output - regenerate it through asn1tools, or the test is worthless.
*/
class CamEncodeGoldenTest {
/**
* asn1tools-verified encoding of [referenceCam]. The trap this pins down: `YawRateConfidence`
* has nine enumerands (0..8), so it needs 4 bits and `unavailable` is 8 - not 3 bits and 7.
*/
private val expectedHex =
"0202000f423f3700402ab215af6e286477dffffffc23b7743e0027ffc0d0fe0118329337feebfff6000000"
private val referenceCam = Cam(
stationId = 999_999L,
stationType = StationType.CYCLIST,
latitude = 53.5544955,
longitude = 10.0225470,
speedMps = 4.17,
headingDeg = 63.9,
yawRateDps = null,
driveDirection = 0,
vehicleLengthM = 1.8,
vehicleWidthM = 0.7,
accelerationMps2 = 0.4,
timestamp = 1_787_100_000_000L,
isOwn = true,
)
@Test
fun `encodes a CAM exactly as the ETSI reference encoder does`() {
val encoded = CamUperCodec.encode(referenceCam)
.joinToString("") { "%02x".format(it) }
assertEquals(expectedHex, encoded)
}
@Test
fun `own decoder agrees with the encoder on every field it reads`() {
// Self-consistency is necessary but NOT sufficient - see the class KDoc. This guards the
// decoder against drifting away from the encoder, while the golden bytes above are what
// guards both of them against drifting away from the standard.
val round = CamUperCodec.decode(CamUperCodec.encode(referenceCam), referenceCam.timestamp)
requireNotNull(round)
assertEquals(referenceCam.stationId, round.stationId)
assertEquals(referenceCam.stationType, round.stationType)
assertEquals(referenceCam.latitude, round.latitude, 1e-7)
assertEquals(referenceCam.longitude, round.longitude, 1e-7)
assertEquals(referenceCam.speedMps, round.speedMps, 1e-9)
assertEquals(referenceCam.headingDeg, round.headingDeg, 1e-9)
assertEquals(referenceCam.vehicleLengthM!!, round.vehicleLengthM!!, 1e-9)
assertEquals(referenceCam.vehicleWidthM!!, round.vehicleWidthM!!, 1e-9)
assertEquals(referenceCam.accelerationMps2!!, round.accelerationMps2!!, 1e-9)
}
}