Repository navigation
Don't abort the SDR repository walk on OEM or undecodable records #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e6f3487
Don't abort the SDR repository walk on OEM or undecodable records
bertysentry 255b812
Keep the whole payload of vendor-defined SDR record types in OemRecord
bertysentry 7aac625
Add AGENTS.md with project instructions for AI agents, imported by CL…
bertysentry a602881
Fall back to chunked reads on truncated Get SDR replies; add OEM reco…
bertysentry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Instructions for AI Agents | ||
|
|
||
| ## Code format | ||
|
|
||
| The project has not adopted the MetricsHub Eclipse formatter profile yet (see issue #118). Until it does, match the style of the file you are editing: the `org.metricshub.ipmi.client` packages use tabs, the `org.metricshub.ipmi.core` packages (forked from the Verax IPMI library) use 4 spaces. Do not reformat code you are not otherwise changing. Once `formatter-maven-plugin` is configured, simply run `mvn formatter:format` before committing. | ||
|
|
||
| All Java source files under `src/main/java` must include the proper LGPL-3 license header (the `license-maven-plugin` check covers `main/java/**/*.java` only; tests, Markdown and resources carry no header). When you add a new source file, run `mvn license:update-file-header` before committing (and before building, since the build fails if a source file lacks the header). | ||
|
|
||
| All public methods must have proper Javadoc. Check the output of Maven to identify issues with Javadoc and fix these issues. | ||
|
|
||
| The library targets **Java 8** (`maven.compiler.release` is 8): no `var`, records, switch expressions, text blocks or APIs newer than Java 8 in `src/main`. | ||
|
|
||
| ## Build | ||
|
|
||
| The project uses Maven to build. A full build is performed with `mvn verify site` (or `mvn clean verify site` when applicable). CI runs the same on JDK 17. | ||
|
|
||
| @codex, please don't try to use `mvnw` (Maven Wrapper). Maven is already installed and runs perfectly well. | ||
|
|
||
| ## Test | ||
|
|
||
| Whenever required, when you add code or when you modify code that is not covered with unit tests, add the corresponding unit tests. All tests must pass with `mvn test`. Don't use the `-q` (silent) option, as you want to see the result of successful tests. Tests are run with the Maven surefire plugin and results are stored in the ./target/surefire-reports directory. | ||
|
|
||
| Unit tests must not depend on a real BMC. Exercising the RMCP+ session code against real hardware is done with throw-away harnesses outside the repository; never commit hostnames, user names or passwords of test systems. When testing against a real BMC, keep in mind that BMCs drop UDP replies under concurrent sessions: repeat a failing request before blaming the library, set a short per-message timeout with `IpmiConnector.setTimeout(handle, ms)`, and end harness `main` methods with `System.exit(0)` because the library leaves non-daemon threads behind after a timeout. | ||
|
|
||
| ## Code quality reports | ||
|
|
||
| Code quality reports (checkstyle, pmd/cpd, spotbugs) are generated by `mvn verify site` into ./target/checkstyle-result.xml, ./target/pmd.xml, ./target/cpd.xml and ./target/spotbugsXml.xml. They are not yet gated (issues #114, #115, #116, #117 track the clean-up). Do not add new violations: check the reports for the files you changed before committing and submitting your code. On JDK 21+ the SpotBugs plugin version inherited from the parent POM cannot read the JDK class files; run `mvn com.github.spotbugs:spotbugs-maven-plugin:4.10.4.1:spotbugs` instead. | ||
|
|
||
| ## Documentation | ||
|
|
||
| Always make sure that public API changes are properly documented in src/site/markdown/*.md and that README.md is always up-to-date. The published documentation is https://metricshub.org/ipmi-java (see issue #113 for the planned overhaul). | ||
|
|
||
| ## IPMI specifics | ||
|
|
||
| - The protocol implementation lives in `org.metricshub.ipmi.core` (RMCP+, RAKP, SDR/FRU/SEL coders, connection state machine); the MetricsHub-facing API is `org.metricshub.ipmi.client` (`IpmiClient`, `IpmiClientConfiguration`, the runners and `IpmiResultConverter`). | ||
| - Decoders must never abort a whole SDR repository walk or FRU decode on a record they do not model: skip, log and continue (IPMI 2.0 reserves SDR record types 0xC0-0xFF for OEM use and vendors do emit them). | ||
| - Byte-level parsing must follow the IPMI 2.0 specification (section numbers are cited in the code and in the issues); when a finding is "by the spec" but not reproduced on real hardware, say so in the commit or PR. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @AGENTS.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
src/test/java/org/metricshub/ipmi/client/runner/AbstractIpmiRunnerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package org.metricshub.ipmi.client.runner; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.metricshub.ipmi.client.IpmiClientConfiguration; | ||
| import org.metricshub.ipmi.core.coding.commands.sdr.record.FullSensorRecord; | ||
| import org.metricshub.ipmi.core.coding.commands.sdr.record.OemRecord; | ||
|
|
||
| class AbstractIpmiRunnerTest { | ||
|
|
||
| /** | ||
| * Minimal concrete runner: no session, no connector, just the decoding helpers under test. | ||
| */ | ||
| private static final AbstractIpmiRunner<Void> RUNNER = new AbstractIpmiRunner<Void>( | ||
| new IpmiClientConfiguration("bmc", "user", new char[0], null, false, 1)) { | ||
| @Override | ||
| public Void call() { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| private static byte[] record(int type, byte[] payload) { | ||
| byte[] raw = new byte[5 + payload.length]; | ||
| raw[2] = 0x51; | ||
| raw[3] = (byte) type; | ||
| raw[4] = (byte) payload.length; | ||
| System.arraycopy(payload, 0, raw, 5, payload.length); | ||
| return raw; | ||
| } | ||
|
|
||
| @Test | ||
| void decodeRecordReturnsOemRecordForVendorDefinedType() { | ||
| assertInstanceOf(OemRecord.class, RUNNER.decodeRecord(record(0xd0, new byte[] {1, 2, 3}))); | ||
| } | ||
|
|
||
| @Test | ||
| void decodeRecordSkipsRecordsItCannotDecodeInsteadOfThrowing() { | ||
| // reserved (non-OEM) record type | ||
| assertNull(RUNNER.decodeRecord(record(0x05, new byte[] {1}))); | ||
| // shorter than the SDR header | ||
| assertNull(RUNNER.decodeRecord(new byte[] {0x00})); | ||
| // full sensor record whose body is missing: decoder runs out of bytes | ||
| assertNull(RUNNER.decodeRecord(record(0x01, new byte[] {1, 2, 3}))); | ||
| } | ||
|
|
||
| @Test | ||
| void decodeRecordStillDecodesValidRecords() { | ||
| // A Full Sensor Record (type 01h) of minimal valid size: 48 bytes, the last one being the ID string type/length | ||
| byte[] payload = new byte[43]; | ||
| payload[0] = 0x20; // sensor owner: BMC | ||
| payload[7] = 0x01; // sensor type: temperature | ||
| payload[8] = 0x01; // event/reading type: threshold | ||
| payload[16] = 0x01; // base unit: degrees C | ||
| payload[42] = (byte) 0xc0; // 8-bit ASCII, zero-length name | ||
| assertInstanceOf(FullSensorRecord.class, RUNNER.decodeRecord(record(0x01, payload))); | ||
| } | ||
|
|
||
| @Test | ||
| void isTruncatedDetectsWholeRecordResponsesShorterThanDeclared() { | ||
| byte[] complete = record(0xd0, new byte[] {1, 2, 3, 4, 5}); | ||
| assertFalse(AbstractIpmiRunner.isTruncated(complete)); | ||
|
|
||
| byte[] truncated = new byte[complete.length - 2]; | ||
| System.arraycopy(complete, 0, truncated, 0, truncated.length); | ||
| assertTrue(AbstractIpmiRunner.isTruncated(truncated), "declared length 5 but only 3 payload bytes returned"); | ||
|
|
||
| assertTrue(AbstractIpmiRunner.isTruncated(new byte[] {0x00, 0x00, 0x51}), "header itself is incomplete"); | ||
| assertTrue(AbstractIpmiRunner.isTruncated(null)); | ||
| } | ||
| } |
82 changes: 82 additions & 0 deletions
82
src/test/java/org/metricshub/ipmi/core/coding/commands/sdr/record/SensorRecordTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package org.metricshub.ipmi.core.coding.commands.sdr.record; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertArrayEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class SensorRecordTest { | ||
|
|
||
| /** | ||
| * Build a raw SDR record: 2-byte record ID (LE), SDR version 0x51, record type, record length, then the payload. | ||
| */ | ||
| private static byte[] record(int id, int type, byte[] payload) { | ||
| byte[] raw = new byte[5 + payload.length]; | ||
| raw[0] = (byte) (id & 0xff); | ||
| raw[1] = (byte) ((id >> 8) & 0xff); | ||
| raw[2] = 0x51; | ||
| raw[3] = (byte) type; | ||
| raw[4] = (byte) payload.length; | ||
| System.arraycopy(payload, 0, raw, 5, payload.length); | ||
| return raw; | ||
| } | ||
|
|
||
| @Test | ||
| void vendorDefinedRecordTypeIsDecodedAsOemRecordWithWholePayload() { | ||
| // Record type D0h as emitted by GIGABYTE/NVIDIA BMCs: vendor-defined layout, no manufacturer ID field | ||
| byte[] payload = {0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; | ||
|
|
||
| SensorRecord record = SensorRecord.populateSensorRecord(record(0x0004, 0xd0, payload)); | ||
|
|
||
| OemRecord oem = assertInstanceOf(OemRecord.class, record); | ||
| assertEquals(4, oem.getId()); | ||
| assertEquals(0, oem.getManufacturerId(), "manufacturer ID is unknown for vendor-defined record types"); | ||
| assertArrayEquals(payload, oem.getOemData(), "the complete type-specific payload must be preserved"); | ||
| } | ||
|
|
||
| @Test | ||
| void lastOemRecordTypeIsAccepted() { | ||
| byte[] payload = {0x01, 0x02}; | ||
|
|
||
| OemRecord oem = assertInstanceOf(OemRecord.class, SensorRecord.populateSensorRecord(record(0x0010, 0xff, payload))); | ||
|
|
||
| assertArrayEquals(payload, oem.getOemData()); | ||
| } | ||
|
|
||
| @Test | ||
| void standardOemRecordTypeC0ParsesManufacturerIdAndData() { | ||
| // C0h layout: 3-byte manufacturer ID (LE, 0x000157 = 343) followed by OEM data | ||
| byte[] payload = {0x57, 0x01, 0x00, 0x21, 0x22}; | ||
|
|
||
| OemRecord oem = assertInstanceOf(OemRecord.class, SensorRecord.populateSensorRecord(record(0x0020, 0xc0, payload))); | ||
|
|
||
| assertEquals(343, oem.getManufacturerId()); | ||
| assertArrayEquals(new byte[] {0x21, 0x22}, oem.getOemData()); | ||
| } | ||
|
|
||
| @Test | ||
| void shortOemRecordDoesNotThrow() { | ||
| // C0h record without even a complete manufacturer ID: no ID is fabricated and the bytes are kept as payload | ||
| OemRecord oem = assertInstanceOf(OemRecord.class, SensorRecord.populateSensorRecord(record(0x0030, 0xc0, new byte[] {0x57}))); | ||
|
|
||
| assertEquals(0, oem.getManufacturerId()); | ||
| assertArrayEquals(new byte[] {0x57}, oem.getOemData()); | ||
|
|
||
| // header only | ||
| OemRecord empty = assertInstanceOf(OemRecord.class, SensorRecord.populateSensorRecord(record(0x0031, 0xc0, new byte[0]))); | ||
| assertEquals(0, empty.getOemData().length); | ||
| } | ||
|
|
||
| @Test | ||
| void unknownStandardRecordTypeIsRejected() { | ||
| // 05h is reserved by IPMI 2.0 and not an OEM type: callers are expected to skip it | ||
| assertThrows(IllegalArgumentException.class, () -> SensorRecord.populateSensorRecord(record(0x0040, 0x05, new byte[] {0x00}))); | ||
| } | ||
|
|
||
| @Test | ||
| void recordShorterThanHeaderIsRejected() { | ||
| assertThrows(IllegalArgumentException.class, () -> SensorRecord.populateSensorRecord(new byte[] {0x00, 0x00, 0x51})); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.