This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5797-8582cba2f66cfb0eef8e9669381bfa4876dd5bca
in repository https://gitbox.apache.org/repos/asf/texera.git

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 == "")
+  }
+}

Reply via email to