From c0bd70fa30d85fbffa3595e2aa70c1f54f934322 Mon Sep 17 00:00:00 2001 From: jadamcrain Date: Wed, 14 Oct 2026 14:50:00 -0700 Subject: [PATCH] Validate only the procedures that apply at the run's subset level Revision.validate collected the configuration problems of every selected procedure that's implemented and enabled, including those that don't apply at the configured subset level, which a run skips. A 3.1r1 run at level 2 could be refused for what 8.26.3, a level 3 test, needs. It now checks the procedures the run will run: implemented, applicable at the level, and enabled. Part of #59. --- .../stepfunc/dnp3c/conformance/Revision.scala | 6 ++++-- .../dnp3c/conformance/CleanupTest.scala | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/scala/io/stepfunc/dnp3c/conformance/Revision.scala b/src/main/scala/io/stepfunc/dnp3c/conformance/Revision.scala index 87a6a71..00e1184 100644 --- a/src/main/scala/io/stepfunc/dnp3c/conformance/Revision.scala +++ b/src/main/scala/io/stepfunc/dnp3c/conformance/Revision.scala @@ -81,7 +81,8 @@ final class Revision private[conformance] ( /** * Check that the configuration is one this revision can test: it gives a subset level the revision tests, and it has - * what the revision needs configured, whatever procedures run, and what the procedures selected need. + * what the revision needs configured, whatever procedures run, and what each procedure selected needs, if it will run: + * it's implemented, applies at that level and is enabled. * * @param selected the procedures a run will run; all of them unless given * @return the subset level @@ -90,7 +91,8 @@ final class Revision private[conformance] ( def validate(options: OptionSource, selected: Iterable[TestProcedure] = flatMapProcedures(Some(_))): Int = { val level = subsetLevel(options) // what the procedures that will run need, each problem once - val procedures = selected.filter(p => !p.incomplete && p.disabledBy(options).isEmpty) + val procedures = + selected.filter(p => !p.incomplete && levelsOf(p).contains(level) && p.disabledBy(options).isEmpty) val problems = (configurationProblems(options) ++ procedures.flatMap(_.definition.configurationProblems(options))).distinct if (problems.nonEmpty) throw new ConfigurationException("configuration", problems) diff --git a/src/test/scala/io/stepfunc/dnp3c/conformance/CleanupTest.scala b/src/test/scala/io/stepfunc/dnp3c/conformance/CleanupTest.scala index 72d4b98..2733a70 100644 --- a/src/test/scala/io/stepfunc/dnp3c/conformance/CleanupTest.scala +++ b/src/test/scala/io/stepfunc/dnp3c/conformance/CleanupTest.scala @@ -169,4 +169,21 @@ class CleanupTest extends AnyFunSuite with Matchers { problems(Map(), needing(required = false), needing(required = false)) should equal(List(problem)) problems(Map(Inputs.binary.points.id -> "false"), needing(required = true)) should equal(Nil) } + + test("a run doesn't check the configuration problems of a procedure that doesn't apply at its level") { + val problem = ConfigProblem("some.option", "", "needed") + val levelTwo = TestProcedure( + "a", + "a", + new ProcedureDefinition { + override protected def steps(options: OptionSource): Iterable[LabeledStep] = Nil + override def configurationProblems(options: OptionSource): List[ConfigProblem] = List(problem) + }, + levels = Some(Set(2)) + ) + val revision = new Revision("t", "t", "2000-01-01", List(1, 2), List(section("s", "s")(levelTwo))) + def at(level: Int) = Options.from(Map(OutstationOptions.subsetLevel.id -> level.toString)) + revision.validate(at(1)) should equal(1) + intercept[ConfigurationException](revision.validate(at(2))).problems should equal(List(problem)) + } }