This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new c8a4cd9f3a test(workflow-operator): add unit test coverage for small
visualization config bags (BulletChartStepDefinition + HierarchySection +
BandConfig) (#5797)
c8a4cd9f3a is described below
commit c8a4cd9f3a8ee5a832929767e8e720195af918b7
Author: Xinyuan Lin <[email protected]>
AuthorDate: Fri Jun 19 19:26:14 2026 -0700
test(workflow-operator): add unit test coverage for small visualization
config bags (BulletChartStepDefinition + HierarchySection + BandConfig) (#5797)
### What changes were proposed in this PR?
Pin defaults, mutability, JSON round-trip, and annotation surface for
three small Jackson-deserializable config classes in the visualization
operator subtree. Each one lives on a descriptor's wire-format (frontend
↔ backend JSON), so any drift in defaults, wire-key names, or annotation
values silently breaks the workflow saved-state round-trip. No
production-code changes.
| Spec | Source class | Tests |
| --- | --- | --- |
| `BulletChartStepDefinitionSpec` | `BulletChartStepDefinition` | 7 |
| `HierarchySectionSpec` | `HierarchySection` | 7 |
| `BandConfigSpec` | `BandConfig` | 9 |
All three spec files follow the `<srcClassName>Spec.scala` one-to-one
convention.
**Behavior pinned — `BulletChartStepDefinition`**
| Surface | Contract |
| --- | --- |
| Construction | stores both `@JsonCreator` constructor arguments |
| Mutability | both `var` fields are reassignable |
| Wire keys | serializes under `start` / `end` (tree-API verified) |
| JSON round-trip | preserves both fields |
| Annotations | `@JsonProperty(\"start\")` and `@JsonProperty(\"end\")`
live on the **constructor parameters** (Scala's default for `var` ctor
params unless `@meta.field` is used); verified via
`Constructor.getParameterAnnotations` |
| Instance independence | no static state shared |
**Behavior pinned — `HierarchySection`**
| Surface | Contract |
| --- | --- |
| Defaults | `attributeName == \"\"` |
| Mutability | `attributeName` is reassignable |
| JSON round-trip | preserves the field, including the default-empty
case |
| Annotations | `@JsonProperty(required = true)` +
`@AutofillAttributeName` + `@NotNull(\"Attribute Name cannot be
empty\")` |
| Instance independence | no static state shared |
**Behavior pinned — `BandConfig`**
| Surface | Contract |
| --- | --- |
| Inheritance | extends `LineConfig` (compile-time enforced) |
| Defaults | `yUpper`, `yLower`, `fillColor` all default to `\"\"` |
| Mutability | all three fields are reassignable |
| JSON round-trip | preserves all three fields |
| `yUpper` annotations | `@JsonProperty(required = true)` +
`@NotNull(\"Y-Axis Upper Bound cannot be empty\")` +
`@AutofillAttributeName` |
| `yLower` annotations | `@JsonProperty(required = true)` +
`@NotNull(\"Y-Axis Lower Bound cannot be empty\")` +
`@AutofillAttributeName` |
| `fillColor` annotations | `@JsonProperty(required = false)`, **no**
`@NotNull` |
| Instance independence | no static state shared |
### Any related issues, documentation, discussions?
Closes #5794.
### How was this PR tested?
Pure unit-test additions; verified locally with:
- `sbt \"WorkflowOperator/testOnly
org.apache.texera.amber.operator.visualization.bulletChart.BulletChartStepDefinitionSpec
org.apache.texera.amber.operator.visualization.hierarchychart.HierarchySectionSpec
org.apache.texera.amber.operator.visualization.continuousErrorBands.BandConfigSpec\"`
— 23 tests, all green
- `sbt \"WorkflowOperator/Test/scalafmtCheck\"` — clean
- CI to confirm
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.7 [1M context])
---
.../BulletChartStepDefinitionSpec.scala | 117 +++++++++++++++++++
.../continuousErrorBands/BandConfigSpec.scala | 130 +++++++++++++++++++++
.../hierarchychart/HierarchySectionSpec.scala | 108 +++++++++++++++++
3 files changed, 355 insertions(+)
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartStepDefinitionSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartStepDefinitionSpec.scala
new file mode 100644
index 0000000000..984c1fd8b0
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartStepDefinitionSpec.scala
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.bulletChart
+
+import com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty}
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class BulletChartStepDefinitionSpec extends AnyFlatSpec {
+
+ //
---------------------------------------------------------------------------
+ // Construction — @JsonCreator constructor accepts both fields
+ //
---------------------------------------------------------------------------
+
+ "BulletChartStepDefinition" should "store both constructor arguments" in {
+ val d = new BulletChartStepDefinition("10", "90")
+ assert(d.start == "10")
+ assert(d.end == "90")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Mutability
+ //
---------------------------------------------------------------------------
+
+ it should "allow both fields to be reassigned post-construction" in {
+ val d = new BulletChartStepDefinition("0", "1")
+ d.start = "low"
+ d.end = "high"
+ assert(d.start == "low")
+ assert(d.end == "high")
+ }
+
+ //
---------------------------------------------------------------------------
+ // JSON round-trip — wire keys are `start` / `end`
+ //
---------------------------------------------------------------------------
+
+ "BulletChartStepDefinition JSON round-trip" should
+ "serialize start and end under the canonical wire keys" in {
+ val d = new BulletChartStepDefinition("alpha", "omega")
+ val tree = objectMapper.readTree(objectMapper.writeValueAsString(d))
+ assert(tree.has("start"))
+ assert(tree.get("start").asText() == "alpha")
+ assert(tree.has("end"))
+ assert(tree.get("end").asText() == "omega")
+ }
+
+ it should "round-trip both fields cleanly" in {
+ val d = new BulletChartStepDefinition("33", "66")
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(d),
+ classOf[BulletChartStepDefinition]
+ )
+ assert(restored.start == "33")
+ assert(restored.end == "66")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Annotations — on the @JsonCreator constructor parameters (Scala places
+ // annotations on `var` ctor params on the parameter, not the synthesized
+ // field, unless `@(JsonProperty @meta.field)` is used).
+ //
---------------------------------------------------------------------------
+
+ // Select the @JsonCreator-annotated constructor by its annotation rather
than
+ // by reflection order (`getDeclaredConstructors.head`), so the test stays
+ // deterministic if an auxiliary constructor is ever added.
+ private val jsonCreatorCtor =
+ classOf[BulletChartStepDefinition].getDeclaredConstructors
+ .find(_.isAnnotationPresent(classOf[JsonCreator]))
+ .getOrElse(
+ fail("expected a @JsonCreator constructor on
BulletChartStepDefinition")
+ )
+
+ private def ctorParamJsonProperty(paramIndex: Int): JsonProperty = {
+ val annotations = jsonCreatorCtor.getParameterAnnotations()(paramIndex)
+ annotations.collectFirst { case jp: JsonProperty => jp }.orNull
+ }
+
+ "BulletChartStepDefinition ctor param[0] (start)" should "carry
@JsonProperty(\"start\")" in {
+ val jp = ctorParamJsonProperty(0)
+ assert(jp != null)
+ assert(jp.value == "start")
+ }
+
+ "BulletChartStepDefinition ctor param[1] (end)" should "carry
@JsonProperty(\"end\")" in {
+ val jp = ctorParamJsonProperty(1)
+ assert(jp != null)
+ assert(jp.value == "end")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Instance independence
+ //
---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new BulletChartStepDefinition("a-start", "a-end")
+ val b = new BulletChartStepDefinition("b-start", "b-end")
+ a.start = "mutated"
+ assert(b.start == "b-start")
+ }
+}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/BandConfigSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/BandConfigSpec.scala
new file mode 100644
index 0000000000..c33289e276
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/BandConfigSpec.scala
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.continuousErrorBands
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import javax.validation.constraints.NotNull
+import
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.operator.visualization.lineChart.LineConfig
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class BandConfigSpec extends AnyFlatSpec {
+
+ //
---------------------------------------------------------------------------
+ // Inheritance
+ //
---------------------------------------------------------------------------
+
+ "BandConfig" should "extend LineConfig (compile-time enforced)" in {
+ val lc: LineConfig = new BandConfig
+ assert(lc != null)
+ }
+
+ //
---------------------------------------------------------------------------
+ // Defaults
+ //
---------------------------------------------------------------------------
+
+ it should "default yUpper, yLower, and fillColor to the empty string" in {
+ val c = new BandConfig
+ assert(c.yUpper == "")
+ assert(c.yLower == "")
+ assert(c.fillColor == "")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Mutability
+ //
---------------------------------------------------------------------------
+
+ it should "allow all three fields to be reassigned post-construction" in {
+ val c = new BandConfig
+ c.yUpper = "upper"
+ c.yLower = "lower"
+ c.fillColor = "#ff0000"
+ assert(c.yUpper == "upper")
+ assert(c.yLower == "lower")
+ assert(c.fillColor == "#ff0000")
+ }
+
+ //
---------------------------------------------------------------------------
+ // JSON round-trip
+ //
---------------------------------------------------------------------------
+
+ "BandConfig JSON round-trip" should "preserve yUpper, yLower, and fillColor"
in {
+ val c = new BandConfig
+ c.yUpper = "u"
+ c.yLower = "l"
+ c.fillColor = "rgba(0,0,255,0.5)"
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(c),
+ classOf[BandConfig]
+ )
+ assert(restored.yUpper == "u")
+ assert(restored.yLower == "l")
+ assert(restored.fillColor == "rgba(0,0,255,0.5)")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Annotations — required=true on yUpper/yLower, required=false on fillColor
+ //
---------------------------------------------------------------------------
+
+ "BandConfig#yUpper" should "carry @JsonProperty(required = true) + @NotNull
+ @AutofillAttributeName" in {
+ val cls = classOf[BandConfig]
+ val jp =
cls.getDeclaredField("yUpper").getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ assert(jp.required)
+ val notNull =
cls.getDeclaredField("yUpper").getAnnotation(classOf[NotNull])
+ assert(notNull != null)
+ assert(notNull.message == "Y-Axis Upper Bound cannot be empty")
+ val autofill =
cls.getDeclaredField("yUpper").getAnnotation(classOf[AutofillAttributeName])
+ assert(autofill != null)
+ }
+
+ "BandConfig#yLower" should "carry @JsonProperty(required = true) + @NotNull
+ @AutofillAttributeName" in {
+ val cls = classOf[BandConfig]
+ val jp =
cls.getDeclaredField("yLower").getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ assert(jp.required)
+ val notNull =
cls.getDeclaredField("yLower").getAnnotation(classOf[NotNull])
+ assert(notNull != null)
+ assert(notNull.message == "Y-Axis Lower Bound cannot be empty")
+ val autofill =
cls.getDeclaredField("yLower").getAnnotation(classOf[AutofillAttributeName])
+ assert(autofill != null)
+ }
+
+ "BandConfig#fillColor" should "carry @JsonProperty(required = false) and no
@NotNull" in {
+ val cls = classOf[BandConfig]
+ val jp =
cls.getDeclaredField("fillColor").getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ assert(!jp.required)
+ val notNull =
cls.getDeclaredField("fillColor").getAnnotation(classOf[NotNull])
+ assert(notNull == null)
+ }
+
+ //
---------------------------------------------------------------------------
+ // Instance independence
+ //
---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new BandConfig
+ val b = new BandConfig
+ a.yUpper = "mutated"
+ assert(b.yUpper == "")
+ }
+}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchySectionSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchySectionSpec.scala
new file mode 100644
index 0000000000..cb40df93f8
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchySectionSpec.scala
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.hierarchychart
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import javax.validation.constraints.NotNull
+import
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class HierarchySectionSpec extends AnyFlatSpec {
+
+ //
---------------------------------------------------------------------------
+ // Defaults
+ //
---------------------------------------------------------------------------
+
+ "HierarchySection" should "default attributeName to the empty string" in {
+ val s = new HierarchySection
+ assert(s.attributeName == "")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Mutability
+ //
---------------------------------------------------------------------------
+
+ it should "allow attributeName to be reassigned post-construction" in {
+ val s = new HierarchySection
+ s.attributeName = "country"
+ assert(s.attributeName == "country")
+ }
+
+ //
---------------------------------------------------------------------------
+ // JSON round-trip
+ //
---------------------------------------------------------------------------
+
+ "HierarchySection JSON round-trip" should "preserve attributeName" in {
+ val s = new HierarchySection
+ s.attributeName = "region"
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(s),
+ classOf[HierarchySection]
+ )
+ assert(restored.attributeName == "region")
+ }
+
+ it should "round-trip default (empty) values cleanly" in {
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(new HierarchySection),
+ classOf[HierarchySection]
+ )
+ assert(restored.attributeName == "")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Annotations — required=true + @AutofillAttributeName + @NotNull
+ //
---------------------------------------------------------------------------
+
+ "HierarchySection#attributeName" should "carry @JsonProperty(required =
true)" in {
+ val jp = classOf[HierarchySection]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ assert(jp.required)
+ }
+
+ it should "carry @AutofillAttributeName" in {
+ val ann = classOf[HierarchySection]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[AutofillAttributeName])
+ assert(ann != null)
+ }
+
+ it should "carry @NotNull with the canonical error message" in {
+ val ann = classOf[HierarchySection]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[NotNull])
+ assert(ann != null)
+ assert(ann.message == "Attribute Name cannot be empty")
+ }
+
+ //
---------------------------------------------------------------------------
+ // Instance independence
+ //
---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new HierarchySection
+ val b = new HierarchySection
+ a.attributeName = "first"
+ assert(b.attributeName == "")
+ }
+}