Skip to content

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.

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.

Terminal window
make test # tests with the race detector
make coverage # coverage report for the library packages
make soak # chaos and stress tests for SOAKTIME each (default 2m)
make check # format, vet, lint, vulnerability scan, race tests, coverage gate

Randomized 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.

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.

Terminal window
./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/models
make generate # regenerate registry/models_gen.go

A 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.

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.

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.

  1. Start from the models you need, or copy a real device once:

    Terminal window
    sunspecctl template --model 1,103,160 -o device.json
    # or
    sunspecctl capture --url tcp://192.168.1.10:502 -o device.json
  2. Edit the values, then check the file without serving it:

    Terminal window
    sunspecctl serve --file device.json --check
  3. Serve it on loopback and point your code at it:

    Terminal window
    sunspecctl serve --file device.json --url tcp://127.0.0.1:1502
    sunspecctl 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.

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"})
  • 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 sunspec records every model body to an MBCAP capture, and modbusctl server static serves such a recording back at the Modbus level.

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.