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-5697-0f79251c56d36ca9b4c05e53cf2625a6ad49a131
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 612a79cbe48dc915e2975d856541847a918322b4
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Jun 14 23:33:05 2026 -0700

    test(workflow-operator): add unit test coverage for visualization 
chart-config classes (#5697)
    
    ### What changes were proposed in this PR?
    
    Pin behavior of four previously-uncovered Jackson-annotated config
    classes that back visualization operators in
    `common/workflow-operator/operator/visualization/`. No production-code
    changes.
    
    | Spec | Source class | Tests |
    | --- | --- | --- |
    | `TablesConfigSpec` | `TablesConfig` | 7 |
    | `NestedTableConfigSpec` | `NestedTableConfig` | 9 |
    | `FigureFactoryTableConfigSpec` | `FigureFactoryTableConfig` | 7 |
    | `DumbbellDotConfigSpec` | `DumbbellDotConfig` | 9 |
    
    All four spec files follow the `<srcClassName>Spec.scala` one-to-one
    convention.
    
    **Behavior pinned (each class)**
    
    | Surface | Contract |
    | --- | --- |
    | Default field values | empty strings on a fresh instance |
    | Mutability | `var` fields are assignable post-construction |
    | JSON round-trip via `objectMapper.writeValueAsString` + `readValue` |
    preserves every field |
    | `@JsonProperty(required = true)` annotation | present on every
    documented required field — verified via reflection |
    | Distinct instances | no static-field leakage across `new` calls |
    
    **Per-class specifics**
    
    | Spec | Additional pins |
    | --- | --- |
    | `TablesConfigSpec` | `@NotNull` annotation on `attributeName` (jakarta
    validation) |
    | `NestedTableConfigSpec` | `newName` serializes under wire-key `name`
    (per `@JsonProperty(value = "name")`); the field name `newName` MUST NOT
    appear as a JSON key; deserialization from `{"name":...}` re-populates
    `newName` |
    | `FigureFactoryTableConfigSpec` | `@AutofillAttributeName` annotation
    on `attributeName` (UI dropdown contract) |
    | `DumbbellDotConfigSpec` | `dotValue` serializes under wire-key `dot`;
    `@NotNull` + `@AutofillAttributeName` annotations; class-level
    `@JsonSchemaInject` restricts `dot` to integer/long/double attribute
    types |
    
    ### Any related issues, documentation, discussions?
    
    Closes #5694.
    
    ### How was this PR tested?
    
    Pure unit-test additions; verified locally with:
    
    - `sbt "WorkflowOperator/testOnly
    org.apache.texera.amber.operator.visualization.tablesChart.TablesConfigSpec
    
org.apache.texera.amber.operator.visualization.nestedTable.NestedTableConfigSpec
    
org.apache.texera.amber.operator.visualization.figureFactoryTable.FigureFactoryTableConfigSpec
    
org.apache.texera.amber.operator.visualization.dumbbellPlot.DumbbellDotConfigSpec"`
    — 32 tests, all green
    - `sbt scalafmtCheckAll` — clean
    - CI to confirm
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.7 [1M context])
---
 .../dumbbellPlot/DumbbellDotConfigSpec.scala       | 134 +++++++++++++++++++++
 .../FigureFactoryTableConfigSpec.scala             |  99 +++++++++++++++
 .../nestedTable/NestedTableConfigSpec.scala        | 132 ++++++++++++++++++++
 .../tablesChart/TablesConfigSpec.scala             | 108 +++++++++++++++++
 4 files changed, 473 insertions(+)

diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala
new file mode 100644
index 0000000000..d1761817d5
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala
@@ -0,0 +1,134 @@
+/*
+ * 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.dumbbellPlot
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject
+import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+import javax.validation.constraints.NotNull
+
+class DumbbellDotConfigSpec extends AnyFlatSpec {
+
+  // 
---------------------------------------------------------------------------
+  // Default state
+  // 
---------------------------------------------------------------------------
+
+  "DumbbellDotConfig" should "default dotValue to the empty string" in {
+    val c = new DumbbellDotConfig
+    assert(c.dotValue == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Mutability
+  // 
---------------------------------------------------------------------------
+
+  it should "allow dotValue to be assigned post-construction" in {
+    val c = new DumbbellDotConfig
+    c.dotValue = "numeric-col"
+    assert(c.dotValue == "numeric-col")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // JSON round-trip — verify both the Scala field name AND the wire key
+  // 
---------------------------------------------------------------------------
+
+  "DumbbellDotConfig JSON round-trip" should
+    "preserve dotValue via the wire-key `dot` (per @JsonProperty(value = 
\"dot\"))" in {
+    val original = new DumbbellDotConfig
+    original.dotValue = "amount"
+    val json = objectMapper.writeValueAsString(original)
+    // Parse the JSON into a tree and assert on field presence + value
+    // directly — this stays robust to formatting changes (spaces, key
+    // ordering) that pure substring matching would mistake for drift.
+    val tree = objectMapper.readTree(json)
+    assert(tree.has("dot"), s"expected wire-key 'dot' in JSON, got: $json")
+    assert(tree.get("dot").asText() == "amount")
+    assert(
+      !tree.has("dotValue"),
+      s"field name 'dotValue' must NOT appear as a JSON key, got: $json"
+    )
+    val restored = objectMapper.readValue(json, classOf[DumbbellDotConfig])
+    assert(restored.dotValue == "amount")
+  }
+
+  it should "deserialize from the wire-key `dot` back into dotValue" in {
+    val json = """{"dot":"amount"}"""
+    val restored = objectMapper.readValue(json, classOf[DumbbellDotConfig])
+    assert(restored.dotValue == "amount")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Annotations
+  // 
---------------------------------------------------------------------------
+
+  "DumbbellDotConfig#dotValue" should
+    "carry @JsonProperty(value = 'dot', required = true)" in {
+    val jp = classOf[DumbbellDotConfig]
+      .getDeclaredField("dotValue")
+      .getAnnotation(classOf[JsonProperty])
+    assert(jp != null)
+    val actualValue = jp.value
+    assert(actualValue == "dot", s"expected value='dot', got: '$actualValue'")
+    assert(jp.required, "dotValue must be marked required")
+  }
+
+  it should "carry @NotNull (javax.validation contract)" in {
+    val notNull = classOf[DumbbellDotConfig]
+      .getDeclaredField("dotValue")
+      .getAnnotation(classOf[NotNull])
+    assert(notNull != null, "dotValue must carry @NotNull for 
javax.validation")
+  }
+
+  it should "carry @AutofillAttributeName (UI populates the dropdown from the 
input schema)" in {
+    val ann = classOf[DumbbellDotConfig]
+      .getDeclaredField("dotValue")
+      .getAnnotation(classOf[AutofillAttributeName])
+    assert(ann != null)
+  }
+
+  "DumbbellDotConfig (class-level)" should
+    "carry @JsonSchemaInject restricting `dot` to integer/long/double 
attribute types" in {
+    // The class-level @JsonSchemaInject is what tells the UI to filter the
+    // attribute dropdown to numeric columns only. Drift here would silently
+    // accept non-numeric attributes and break the chart at render time.
+    val ann = 
classOf[DumbbellDotConfig].getAnnotation(classOf[JsonSchemaInject])
+    assert(ann != null, "class-level @JsonSchemaInject must be present")
+    val payload = ann.json
+    assert(payload.contains("attributeTypeRules"))
+    assert(payload.contains("\"dot\""))
+    assert(payload.contains("integer"))
+    assert(payload.contains("long"))
+    assert(payload.contains("double"))
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Instance independence
+  // 
---------------------------------------------------------------------------
+
+  it should "construct two independent instances (no static state shared)" in {
+    val a = new DumbbellDotConfig
+    val b = new DumbbellDotConfig
+    a.dotValue = "first"
+    assert(b.dotValue == "")
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala
new file mode 100644
index 0000000000..e7a6329159
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala
@@ -0,0 +1,99 @@
+/*
+ * 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.figureFactoryTable
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class FigureFactoryTableConfigSpec extends AnyFlatSpec {
+
+  // 
---------------------------------------------------------------------------
+  // Default state
+  // 
---------------------------------------------------------------------------
+
+  "FigureFactoryTableConfig" should "default attributeName to the empty 
string" in {
+    val c = new FigureFactoryTableConfig
+    assert(c.attributeName == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Mutability
+  // 
---------------------------------------------------------------------------
+
+  it should "allow attributeName to be assigned post-construction" in {
+    val c = new FigureFactoryTableConfig
+    c.attributeName = "col-z"
+    assert(c.attributeName == "col-z")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // JSON round-trip
+  // 
---------------------------------------------------------------------------
+
+  "FigureFactoryTableConfig JSON round-trip" should "preserve attributeName" 
in {
+    val original = new FigureFactoryTableConfig
+    original.attributeName = "x"
+    val restored = objectMapper.readValue(
+      objectMapper.writeValueAsString(original),
+      classOf[FigureFactoryTableConfig]
+    )
+    assert(restored.attributeName == "x")
+  }
+
+  it should "round-trip the default empty attributeName" in {
+    val restored = objectMapper.readValue(
+      objectMapper.writeValueAsString(new FigureFactoryTableConfig),
+      classOf[FigureFactoryTableConfig]
+    )
+    assert(restored.attributeName == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Annotations
+  // 
---------------------------------------------------------------------------
+
+  "FigureFactoryTableConfig#attributeName" should "carry 
@JsonProperty(required = true)" in {
+    val jp = classOf[FigureFactoryTableConfig]
+      .getDeclaredField("attributeName")
+      .getAnnotation(classOf[JsonProperty])
+    assert(jp != null && jp.required)
+  }
+
+  it should
+    "carry @AutofillAttributeName (UI relies on this to populate a dropdown)" 
in {
+    val ann = classOf[FigureFactoryTableConfig]
+      .getDeclaredField("attributeName")
+      .getAnnotation(classOf[AutofillAttributeName])
+    assert(ann != null, "@AutofillAttributeName must be present so the UI 
auto-populates")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Instance independence
+  // 
---------------------------------------------------------------------------
+
+  it should "construct two independent instances (no static state shared)" in {
+    val a = new FigureFactoryTableConfig
+    val b = new FigureFactoryTableConfig
+    a.attributeName = "first"
+    assert(b.attributeName == "")
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala
new file mode 100644
index 0000000000..199efab00c
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala
@@ -0,0 +1,132 @@
+/*
+ * 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.nestedTable
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class NestedTableConfigSpec extends AnyFlatSpec {
+
+  // 
---------------------------------------------------------------------------
+  // Default state
+  // 
---------------------------------------------------------------------------
+
+  "NestedTableConfig" should "default every field to the empty string" in {
+    val c = new NestedTableConfig
+    assert(c.attributeGroup == "")
+    assert(c.originalName == "")
+    assert(c.newName == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Mutability
+  // 
---------------------------------------------------------------------------
+
+  it should "allow every field to be assigned post-construction" in {
+    val c = new NestedTableConfig
+    c.attributeGroup = "g"
+    c.originalName = "orig"
+    c.newName = "renamed"
+    assert(c.attributeGroup == "g")
+    assert(c.originalName == "orig")
+    assert(c.newName == "renamed")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // JSON round-trip
+  // 
---------------------------------------------------------------------------
+
+  "NestedTableConfig JSON round-trip" should "preserve all three fields" in {
+    val original = new NestedTableConfig
+    original.attributeGroup = "group-1"
+    original.originalName = "src-name"
+    original.newName = "dst-name"
+    val json = objectMapper.writeValueAsString(original)
+    val restored = objectMapper.readValue(json, classOf[NestedTableConfig])
+    assert(restored.attributeGroup == "group-1")
+    assert(restored.originalName == "src-name")
+    assert(restored.newName == "dst-name")
+  }
+
+  it should
+    "serialize newName under the JSON key `name` (per @JsonProperty(value = 
\"name\"))" in {
+    // The wire-key for `newName` is `name`, not `newName` — a regression
+    // that drifted to `newName` would silently break every workflow JSON
+    // that carries this config. Parse the JSON into a tree so the
+    // assertion is robust to Jackson formatting (spaces, key ordering)
+    // and unambiguous about which key carries which value.
+    val c = new NestedTableConfig
+    c.newName = "renamed"
+    val tree = objectMapper.readTree(objectMapper.writeValueAsString(c))
+    assert(tree.has("name"), s"expected wire-key 'name' in JSON tree: $tree")
+    assert(tree.get("name").asText() == "renamed")
+    assert(
+      !tree.has("newName"),
+      s"field name 'newName' must NOT appear as a JSON key, got: $tree"
+    )
+  }
+
+  it should "deserialize from the wire-key `name` back into newName" in {
+    val json = 
"""{"attributeGroup":"g","originalName":"orig","name":"renamed"}"""
+    val restored = objectMapper.readValue(json, classOf[NestedTableConfig])
+    assert(restored.newName == "renamed")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Annotations — required/optional via reflection
+  // 
---------------------------------------------------------------------------
+
+  "NestedTableConfig#attributeGroup" should "carry @JsonProperty(required = 
true)" in {
+    val jp = classOf[NestedTableConfig]
+      .getDeclaredField("attributeGroup")
+      .getAnnotation(classOf[JsonProperty])
+    assert(jp != null && jp.required)
+  }
+
+  "NestedTableConfig#originalName" should "carry @JsonProperty(required = 
true)" in {
+    val jp = classOf[NestedTableConfig]
+      .getDeclaredField("originalName")
+      .getAnnotation(classOf[JsonProperty])
+    assert(jp != null && jp.required)
+  }
+
+  "NestedTableConfig#newName" should
+    "carry @JsonProperty(value = 'name', required = false) — the optional 
renamed field" in {
+    val jp = classOf[NestedTableConfig]
+      .getDeclaredField("newName")
+      .getAnnotation(classOf[JsonProperty])
+    assert(jp != null)
+    val actualValue = jp.value
+    assert(actualValue == "name", s"expected value='name', got: 
'$actualValue'")
+    assert(!jp.required, "newName must NOT be marked required")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Instance independence
+  // 
---------------------------------------------------------------------------
+
+  it should "construct two independent instances (no static state shared)" in {
+    val a = new NestedTableConfig
+    val b = new NestedTableConfig
+    a.attributeGroup = "first"
+    assert(b.attributeGroup == "")
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.scala
new file mode 100644
index 0000000000..48ea3fe136
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.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.tablesChart
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+import javax.validation.constraints.NotNull
+
+class TablesConfigSpec extends AnyFlatSpec {
+
+  // 
---------------------------------------------------------------------------
+  // Default state
+  // 
---------------------------------------------------------------------------
+
+  "TablesConfig" should "default attributeName to the empty string" in {
+    val c = new TablesConfig
+    assert(c.attributeName == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Mutability — the @JsonProperty bag is `var`-based by design
+  // 
---------------------------------------------------------------------------
+
+  it should "allow attributeName to be assigned post-construction" in {
+    val c = new TablesConfig
+    c.attributeName = "col-a"
+    assert(c.attributeName == "col-a")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // JSON round-trip preserves the field
+  // 
---------------------------------------------------------------------------
+
+  "TablesConfig JSON round-trip" should
+    "serialize and deserialize attributeName unchanged" in {
+    val original = new TablesConfig
+    original.attributeName = "my-attr"
+    val json = objectMapper.writeValueAsString(original)
+    val restored = objectMapper.readValue(json, classOf[TablesConfig])
+    assert(restored.attributeName == "my-attr")
+  }
+
+  it should "round-trip the default empty attributeName" in {
+    val original = new TablesConfig
+    val restored = objectMapper.readValue(
+      objectMapper.writeValueAsString(original),
+      classOf[TablesConfig]
+    )
+    assert(restored.attributeName == "")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Independent instances — no static-field leakage
+  // 
---------------------------------------------------------------------------
+
+  it should "construct two independent instances (no static state shared)" in {
+    val a = new TablesConfig
+    val b = new TablesConfig
+    a.attributeName = "first"
+    assert(b.attributeName == "", "second instance must not see first 
instance's mutation")
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Annotations — verified via reflection (the Jackson + validation
+  // contract is what consumers actually depend on)
+  // 
---------------------------------------------------------------------------
+
+  "TablesConfig#attributeName" should "carry @JsonProperty(required = true)" 
in {
+    val field = classOf[TablesConfig].getDeclaredField("attributeName")
+    val jp = field.getAnnotation(classOf[JsonProperty])
+    assert(jp != null, "attributeName must carry @JsonProperty")
+    assert(jp.required, "attributeName must be marked required")
+  }
+
+  it should "carry @NotNull (javax.validation contract)" in {
+    val field = classOf[TablesConfig].getDeclaredField("attributeName")
+    val notNull = field.getAnnotation(classOf[NotNull])
+    assert(notNull != null, "attributeName must carry @NotNull for 
javax.validation")
+  }
+
+  it should
+    "carry @AutofillAttributeName (UI populates the dropdown from the input 
schema)" in {
+    val ann = classOf[TablesConfig]
+      .getDeclaredField("attributeName")
+      .getAnnotation(classOf[AutofillAttributeName])
+    assert(ann != null, "@AutofillAttributeName must be present so the UI 
auto-populates")
+  }
+}

Reply via email to