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