From e78680e491905abe6a53aeddb23972a8518b3948 Mon Sep 17 00:00:00 2001 From: jadamcrain Date: Thu, 15 Oct 2026 11:03:12 -0700 Subject: [PATCH] Correct three texts a tester reads: 8.20.2a's IIN check, 10.4.4 step 12's sequence, and the capture adapter option --- .../dnp3c/conformance/api/options/ChannelOptions.scala | 7 ++++++- .../conformance/application/pcap/PacketCapture.scala | 3 ++- .../stepfunc/dnp3c/conformance/common/Validations.scala | 8 ++++++-- .../stepfunc/dnp3c/conformance/v31r1/OutputEvents.scala | 4 +++- .../io/stepfunc/dnp3c/conformance/v31r1/PointRanges.scala | 2 +- .../stepfunc/dnp3c/conformance/v31r1/StaticInputs.scala | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/api/options/ChannelOptions.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/api/options/ChannelOptions.scala index 6c5d6b2..be79916 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/api/options/ChannelOptions.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/api/options/ChannelOptions.scala @@ -13,7 +13,12 @@ object ChannelOptions extends OptionGroup { val host: StringOption = OptionTypes.string(this, "device.address", "127.0.0.1", "IP address of the DUT") private val port = OptionTypes.integer(this, "device.port", 20000, "IP port of the DUT", Some(1), Some(65535)) private val captureNIC = - OptionTypes.string(this, "capture.adapter", "", "IP address of the network adapter on which to capture pcaps") + OptionTypes.string( + this, + "capture.adapter", + "", + "Address of the network adapter on which to capture pcaps, as -adapters prints it, e.g. /192.168.1.10; empty for no capture" + ) def prefix: String = "tcp" def description: String = "TCP options" diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/application/pcap/PacketCapture.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/application/pcap/PacketCapture.scala index 06d7ca0..a259afc 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/application/pcap/PacketCapture.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/application/pcap/PacketCapture.scala @@ -40,7 +40,8 @@ object PacketCapture { val interface: PcapNetworkInterface = { interfaces.find(d => matches(nic, d.getAddresses.asScala.toList)).getOrElse { - throw new Exception("Unable to find network adapter: " + nic) + throw new Exception( + s"Unable to find network adapter: $nic (give its address as -adapters prints it, e.g. /192.168.1.10)") } } diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/common/Validations.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/common/Validations.scala index 3bfd82b..c595af3 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/common/Validations.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/common/Validations.scala @@ -50,8 +50,12 @@ object Validations { IIN((IIN.errors.lsb & ~allowed.lsb).toByte, (IIN.errors.msb & ~allowed.msb).toByte) // 3.1r1's Null Error Response: no objects, and at least one of the error bits allowed, but no other error bit - def isNullErrorResponse(allowed: IIN): MessageValidation = - isEmpty && containingAnyOf(allowed) && without(errorsExcept(allowed)) + def isNullErrorResponse(allowed: IIN): MessageValidation = { + val others = errorsExcept(allowed) + // with every error bit allowed there's nothing to rule out, and an empty check would print as "IIN()" + if (others.lsb == 0 && others.msb == 0) isEmpty && containingAnyOf(allowed) + else isEmpty && containingAnyOf(allowed) && without(others) + } // Null Response, or an Error Response whose error bits are all in `allowed` (an Error Response may contain data) def isNullOrErrorResponse(allowed: IIN): MessageValidation = new MessageValidation { diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/OutputEvents.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/OutputEvents.scala index 02dacd6..48cb447 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/OutputEvents.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/OutputEvents.scala @@ -474,7 +474,9 @@ object OutputEvents { driver.writeMessage(Message.request(s, AppFunction.select, List(header))) }, 12 --> new TestStep { - def description: List[Comment] = expectSingleResponse(Sequence.zero, echoes(header)).description + // the sequence is the Select's, known only when it runs + def description: List[Comment] = + "Read a single fragment response" :: expectSingleResponse(Sequence.zero, echoes(header)).description.tail def run(driver: TestDriver): List[DeferredCheck] = expectSingleResponse(s, echoes(header)).run(driver) }, 13 --> delay(timeoutMs + 1000), diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/PointRanges.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/PointRanges.scala index 2bd4994..dd242a4 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/PointRanges.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/PointRanges.scala @@ -136,7 +136,7 @@ object PointRanges { Option.when(t.escapeStep)(OptionCondition(SupportOptions.rangeReads)).toList)*) // a level 1 or 2 device without range reads answers with IIN2.0, 2.1 or 2.2 - private val unsupported = isEmpty && containingAnyOf(IIN.errors) + private val unsupported = isNullErrorResponse(IIN.errors) /** x.2: a range of points the DUT has gets those points, and only those */ def validRequest(t: RangedPoints): ProcedureDefinition = new ProcedureDefinition { diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/StaticInputs.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/StaticInputs.scala index d786a90..b745190 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/StaticInputs.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/v31r1/StaticInputs.scala @@ -61,7 +61,7 @@ object StaticInputs { } private def declined(seq: Sequence, variation: Variation): TestStep = new TestStep { - private val check = isEmpty && containingAnyOf(IIN.errors) + private val check = isNullErrorResponse(IIN.errors) def description: List[Comment] = check.description :+ Comment("Note the error IIN bits set (IIN2.1 is preferred)") def run(driver: TestDriver): List[DeferredCheck] = {