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.
79 lines
3.6 KiB
Kotlin
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)
|
|
}
|
|
}
|