Testing and interop
There is no separate interoperability suite for SunSpec. SunSpec runs on top of Modbus, so the Modbus transport under go-sunspec is the part that is tested against other implementations (see the Modbus overview and its testing page). This page covers how go-sunspec and sunspecctl are tested, and how to test your own SunSpec code without an inverter on the desk.
How go-sunspec is tested
Section titled “How go-sunspec is tested”The go-sunspec test suite needs no hardware. Since v0.5.0 the library can serve SunSpec devices, and its tests use that Server as the device: every read path is exercised over real Modbus TCP against a served chain whose values the test knows.
| Layer | What it covers |
|---|---|
| Unit | One function or type at a time: decoders and encoders per point type, scale factors, groups, the Server, tables of inputs and expected registers, values and errors. |
| Round trip | EncodeModel then DecodeModel for every model of the registry, with generated values for every point type, repeating groups included. |
| End to end | The library’s Server behind a real go-modbus TCP server, read through Detect, Discover, ReadAll, ReadModel, ReadModelByID and ReadPoint: every registry model, every DiscoverOptions field, vendor models, request splitting, live updates, client writes and every documented error. The test file starts with a feature-to-test matrix. |
| Chaos | A fault-injecting TCP proxy between client and device (dropped, reset, truncated, stalled, split, duplicated, delayed and corrupted frames) and misbehaving devices (exceptions, a vanishing marker, a chain that changes or never ends, lying lengths, garbage registers). Asserted: no panic, no hang, no goroutine leak, and never a wrong value returned as valid. |
| Stress | Concurrent discovery and reads through one client, a pooled client and many clients while values are updated and clients write, across server stop and start; a read must never be torn or go back in time. |
| Generated registry check | A test fails if the committed registry/models_gen.go differs from what the generator produces from models/. |
The chaos tests found the two defects fixed in v0.5.0: reads through an existing Device decoded a changed device with its old layout, and an endless model chain looked complete. CI runs the tests with the race detector on Go 1.23 to 1.26, and make check enforces a coverage threshold that Codecov tracks.
make test # tests with the race detectormake coverage # coverage report for the library packagesmake soak # chaos and stress tests for SOAKTIME each (default 2m)make check # format, vet, lint, vulnerability scan, race tests, coverage gateRandomized chaos runs log their seed; SUNSPEC_CHAOS_SEED replays one.
sunspecctl’s own tests run against go-sunspec’s testutil fixture server and against devices that serve runs in the same process, so every command, including capture and serve, is tested without hardware.
Model definitions
Section titled “Model definitions”The model definitions are the other half of correctness: if a definition is wrong, every decoded value from that model is wrong. go-sunspec takes them unchanged from the official sunspec/models repository and generates the registry from them.
./check-models.sh # report what is new, changed or removed upstream (read-only)./check-models.sh --diff # also show which points changed and the JSON diff./sync-models.sh # fetch the latest JSON from sunspec/modelsmake generate # regenerate registry/models_gen.goA scheduled workflow runs check-models.sh every week and opens an issue, with the changed points and the JSON diff, when upstream has new, changed or removed models. The issue closes itself once the local copy is back in sync. The model JSON files must not be edited by hand; a wrong definition is fixed upstream.
Testing your own code
Section titled “Testing your own code”Decode captured registers offline
Section titled “Decode captured registers offline”If you have the raw registers of a model (from a capture, a log or a device manual example), decode them without any Modbus connection:
meta := registry.ByID(103)dm, err := sunspec.DecodeModel(regs, meta, instanceAddr)regs has the same layout as DecodedModel.RawRegisters, which is indexed from the model’s ID register, and instanceAddr is the model’s address on the device. This is the quickest way to write table-driven tests for code that consumes DecodedModel values, including the “not implemented” and scale-factor cases.
Serve a simulated device
Section titled “Serve a simulated device”sunspecctl serve runs a SunSpec device from a JSON file of point values by name. It is the quickest way to give an EMS, a data logger or your own go-sunspec code a device to read, and to see what it does with the writes it sends.
-
Start from the models you need, or copy a real device once:
Terminal window sunspecctl template --model 1,103,160 -o device.json# orsunspecctl capture --url tcp://192.168.1.10:502 -o device.json -
Edit the values, then check the file without serving it:
Terminal window sunspecctl serve --file device.json --check -
Serve it on loopback and point your code at it:
Terminal window sunspecctl serve --file device.json --url tcp://127.0.0.1:1502sunspecctl read --url tcp://127.0.0.1:1502
Serving a capture gives a device that reads the same as the original: the same points, values, scale factors and not-implemented points at the same addresses. With capture --raw the copy is identical register for register. A capture contains the device’s serial number and network addresses, so look before you share it. See Serving a simulated device for the file format.
Serve from your own tests
Section titled “Serve from your own tests”In Go, build the device in the test itself with sunspec.NewServer and serve it on a loopback port with go-modbus. Your test then controls every value, can change them with SetPoint or Update while your code polls, and can watch or refuse writes with ServerOptions.OnWrite:
srv, _ := sunspec.NewServer(&sunspec.ServerOptions{BaseAddress: 40000, UnitID: 1, OnWrite: func(ctx context.Context, w *sunspec.ServerWrite) error { t.Logf("model %d: %v", w.ModelID, w.Points) return nil }}, sunspec.ServerModel{ID: 1, Values: (&sunspec.Values{}).Set("Mn", "Test")}, sunspec.ServerModel{ID: 103, Values: (&sunspec.Values{}).Set("W", 4200).Set("W_SF", 0)},)go srv.Serve(ctx, &modbus.ServerConfig{URL: "tcp://127.0.0.1:1502"})Other simulators
Section titled “Other simulators”- modbus-simulator serves a Modbus device from a JSON fixture of raw registers and codec fields and records a trace you can assert on in CI. It has no SunSpec support, so you lay out the marker, model headers and bodies yourself; use it when you need its fault injection or trace assertions.
modbusctl client scan --algo sunspecrecords every model body to an MBCAP capture, andmodbusctl server staticserves such a recording back at the Modbus level.
What has been validated
Section titled “What has been validated”If go-sunspec decodes a value wrongly on your device, the repository asks for the manufacturer and model, the transport, and the output of sunspecctl detect, sunspecctl models and sunspecctl read --raw in the issue.