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

commit 8582cba2f66cfb0eef8e9669381bfa4876dd5bca
Author: Xinyuan Lin <[email protected]>
AuthorDate: Fri Jun 19 19:19:03 2026 -0700

    test(workflow-operator): add unit test coverage for operator config bags 
(LineConfig, UiUDFParameter, SortCriteriaUnit) (#5815)
    
    ### What changes were proposed in this PR?
    
    Pin behavior of three previously-untested Jackson config-bag classes in
    `common/workflow-operator/` — plain `@JsonProperty` holders (no
    `LogicalOp`) that sit on a descriptor's wire-format, so drift in
    defaults, wire-key names, enum mapping, or annotations silently breaks
    the workflow saved-state round-trip. No production-code changes.
    
    | Spec | Source class | Tests |
    | --- | --- | --- |
    | `LineConfigSpec` | `LineConfig` | 7 |
    | `UiUDFParameterSpec` | `UiUDFParameter` | 3 |
    | `SortCriteriaUnitSpec` | `SortCriteriaUnit` | 6 |
    
    All three spec files follow the `<srcClassName>Spec.scala` one-to-one
    convention.
    
    **Behavior pinned — `LineConfig`**
    
    | Surface | Contract |
    | --- | --- |
    | Defaults | `yValue`/`xValue`/`name`/`color == ""`, `mode ==
    LineMode.LINE_WITH_DOTS` |
    | Wire-keys | serializes under `y` / `x` / `mode` / `name` / `color`
    (not `yValue`/`xValue`) |
    | `LineMode` `@JsonValue` | `LINE → "line"`, `DOTS → "dots"`,
    `LINE_WITH_DOTS → "line with dots"` |
    | `LineMode.fromString` | case-insensitive (`"LINE WITH DOTS" →
    LINE_WITH_DOTS`); unknown string throws `IllegalArgumentException` |
    | `LineMode.getModeInPlotly` | `lines` / `markers` / `lines+markers` |
    | Round-trip | all five fields preserved |
    
    **Behavior pinned — `UiUDFParameter`**
    
    | Surface | Contract |
    | --- | --- |
    | Default | `value == ""` (the `attribute` var is left null on a fresh
    instance and is not dereferenced) |
    | Round-trip | `attribute` (`Attribute`) + `value` preserved through
    JSON |
    | `value` defaulting | deserializing JSON that omits `value` yields `""`
    |
    
    **Behavior pinned — `SortCriteriaUnit`**
    
    | Surface | Contract |
    | --- | --- |
    | Deserialize | `{"attribute":...,"sortPreference":"ASC"/"DESC"}` →
    `attributeName` + `SortPreference` |
    | Wire-key | serializes under `attribute` (not `attributeName`);
    `sortPreference` key present |
    | Round-trip | both fields preserved |
    | Annotations | `@JsonProperty("attribute", required = true)` and
    `@JsonProperty("sortPreference", required = true)` |
    
    ### Any related issues, documentation, discussions?
    
    Closes #5808.
    
    ### How was this PR tested?
    
    Pure unit-test additions; verified locally with:
    
    - `sbt "WorkflowOperator/testOnly
    org.apache.texera.amber.operator.visualization.lineChart.LineConfigSpec
    org.apache.texera.amber.operator.udf.python.UiUDFParameterSpec
    org.apache.texera.amber.operator.sort.SortCriteriaUnitSpec"` — 16 tests,
    all green
    - `sbt "WorkflowOperator/Test/scalafmtCheck"` and `sbt
    "WorkflowOperator/Test/scalafix --check"` — clean
    - CI to confirm
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../amber/operator/sort/SortCriteriaUnitSpec.scala | 87 ++++++++++++++++++++
 .../operator/udf/python/UiUDFParameterSpec.scala   | 51 ++++++++++++
 .../visualization/lineChart/LineConfigSpec.scala   | 92 ++++++++++++++++++++++
 3 files changed, 230 insertions(+)

diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/SortCriteriaUnitSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/SortCriteriaUnitSpec.scala
new file mode 100644
index 0000000000..1ace7417df
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/SortCriteriaUnitSpec.scala
@@ -0,0 +1,87 @@
+/*
+ * 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.sort
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class SortCriteriaUnitSpec extends AnyFlatSpec with Matchers {
+
+  "SortCriteriaUnit" should "deserialize an ASC criterion (wire-key 
'attribute')" in {
+    val u = objectMapper.readValue(
+      """{"attribute":"col","sortPreference":"ASC"}""",
+      classOf[SortCriteriaUnit]
+    )
+    u.attributeName shouldBe "col"
+    u.sortPreference shouldBe SortPreference.ASC
+  }
+
+  it should "deserialize a DESC criterion" in {
+    val u = objectMapper.readValue(
+      """{"attribute":"age","sortPreference":"DESC"}""",
+      classOf[SortCriteriaUnit]
+    )
+    u.attributeName shouldBe "age"
+    u.sortPreference shouldBe SortPreference.DESC
+  }
+
+  "SortCriteriaUnit JSON" should
+    "serialize attributeName under the wire-key 'attribute' (not 
'attributeName')" in {
+    val u = new SortCriteriaUnit
+    u.attributeName = "city"
+    u.sortPreference = SortPreference.ASC
+    val tree = objectMapper.readTree(objectMapper.writeValueAsString(u))
+    tree.has("attribute") shouldBe true
+    tree.get("attribute").asText shouldBe "city"
+    tree.has("attributeName") shouldBe false
+    tree.get("sortPreference").asText shouldBe "ASC"
+  }
+
+  it should "round-trip both fields" in {
+    val u = new SortCriteriaUnit
+    u.attributeName = "score"
+    u.sortPreference = SortPreference.DESC
+    val restored =
+      objectMapper.readValue(objectMapper.writeValueAsString(u), 
classOf[SortCriteriaUnit])
+    restored.attributeName shouldBe "score"
+    restored.sortPreference shouldBe SortPreference.DESC
+  }
+
+  "SortCriteriaUnit#attributeName" should "carry @JsonProperty(\"attribute\", 
required = true)" in {
+    val jp = classOf[SortCriteriaUnit]
+      .getDeclaredField("attributeName")
+      .getAnnotation(classOf[JsonProperty])
+    jp should not be null
+    jp.value shouldBe "attribute"
+    jp.required shouldBe true
+  }
+
+  "SortCriteriaUnit#sortPreference" should
+    "carry @JsonProperty(\"sortPreference\", required = true)" in {
+    val jp = classOf[SortCriteriaUnit]
+      .getDeclaredField("sortPreference")
+      .getAnnotation(classOf[JsonProperty])
+    jp should not be null
+    jp.value shouldBe "sortPreference"
+    jp.required shouldBe true
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameterSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameterSpec.scala
new file mode 100644
index 0000000000..1bb09e04f0
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameterSpec.scala
@@ -0,0 +1,51 @@
+/*
+ * 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.udf.python
+
+import org.apache.texera.amber.core.tuple.{Attribute, AttributeType}
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class UiUDFParameterSpec extends AnyFlatSpec with Matchers {
+
+  "UiUDFParameter" should "default value to the empty string" in {
+    // `attribute` is an uninitialized `var` (null) on a fresh instance; only
+    // `value` has a default and is safe to read.
+    (new UiUDFParameter).value shouldBe ""
+  }
+
+  it should "round-trip attribute and value through JSON" in {
+    val p = new UiUDFParameter
+    p.attribute = new Attribute("col", AttributeType.STRING)
+    p.value = "x"
+    val restored =
+      objectMapper.readValue(objectMapper.writeValueAsString(p), 
classOf[UiUDFParameter])
+    restored.attribute shouldBe new Attribute("col", AttributeType.STRING)
+    restored.value shouldBe "x"
+  }
+
+  it should "default value to the empty string when the JSON omits it" in {
+    val json = 
"""{"attribute":{"attributeName":"col","attributeType":"string"}}"""
+    val restored = objectMapper.readValue(json, classOf[UiUDFParameter])
+    restored.value shouldBe ""
+    restored.attribute shouldBe new Attribute("col", AttributeType.STRING)
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineConfigSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineConfigSpec.scala
new file mode 100644
index 0000000000..dd2d65aaa2
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineConfigSpec.scala
@@ -0,0 +1,92 @@
+/*
+ * 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.lineChart
+
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class LineConfigSpec extends AnyFlatSpec with Matchers {
+
+  "LineConfig" should "default string fields to empty and mode to 
LINE_WITH_DOTS" in {
+    val c = new LineConfig
+    c.yValue shouldBe ""
+    c.xValue shouldBe ""
+    c.name shouldBe ""
+    c.color shouldBe ""
+    c.mode shouldBe LineMode.LINE_WITH_DOTS
+  }
+
+  "LineConfig JSON" should "serialize fields under their wire-keys y / x / 
mode / name / color" in {
+    val c = new LineConfig
+    c.yValue = "sales"
+    c.xValue = "month"
+    c.name = "trend"
+    c.color = "#fff"
+    c.mode = LineMode.LINE
+    val tree = objectMapper.readTree(objectMapper.writeValueAsString(c))
+    tree.get("y").asText shouldBe "sales"
+    tree.get("x").asText shouldBe "month"
+    tree.get("name").asText shouldBe "trend"
+    tree.get("color").asText shouldBe "#fff"
+    // mode is a Java enum with @JsonValue -> its lowercase phrase
+    tree.get("mode").asText shouldBe "line"
+  }
+
+  it should "serialize LINE_WITH_DOTS via its @JsonValue string" in {
+    val c = new LineConfig
+    c.mode = LineMode.LINE_WITH_DOTS
+    objectMapper
+      .readTree(objectMapper.writeValueAsString(c))
+      .get("mode")
+      .asText shouldBe "line with dots"
+  }
+
+  it should "round-trip all five fields" in {
+    val c = new LineConfig
+    c.yValue = "y1"
+    c.xValue = "x1"
+    c.name = "n"
+    c.color = "red"
+    c.mode = LineMode.DOTS
+    val restored = objectMapper.readValue(objectMapper.writeValueAsString(c), 
classOf[LineConfig])
+    restored.yValue shouldBe "y1"
+    restored.xValue shouldBe "x1"
+    restored.name shouldBe "n"
+    restored.color shouldBe "red"
+    restored.mode shouldBe LineMode.DOTS
+  }
+
+  "LineMode.fromString" should "map mode strings case-insensitively" in {
+    LineMode.fromString("line") shouldBe LineMode.LINE
+    LineMode.fromString("dots") shouldBe LineMode.DOTS
+    LineMode.fromString("LINE WITH DOTS") shouldBe LineMode.LINE_WITH_DOTS
+  }
+
+  it should "throw IllegalArgumentException for an unknown mode" in {
+    an[IllegalArgumentException] should be thrownBy 
LineMode.fromString("wiggly")
+  }
+
+  "LineMode.getModeInPlotly" should "map each mode to its plotly trace mode" 
in {
+    LineMode.LINE.getModeInPlotly shouldBe "lines"
+    LineMode.DOTS.getModeInPlotly shouldBe "markers"
+    LineMode.LINE_WITH_DOTS.getModeInPlotly shouldBe "lines+markers"
+  }
+}

Reply via email to