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 9032260210 test(workflow-operator): add unit test coverage for
visualization descriptors (GaugeChart, RangeSlider, SankeyDiagram) (#5843)
9032260210 is described below
commit 90322602101d498e7af273ebfe14dcb5f7501566
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Jun 21 12:51:08 2026 -0700
test(workflow-operator): add unit test coverage for visualization
descriptors (GaugeChart, RangeSlider, SankeyDiagram) (#5843)
### What changes were proposed in this PR?
Pin behavior of three previously-untested visualization
`PythonOperatorDescriptor`s in `common/workflow-operator/`. No
production-code changes.
| Spec | Source class | Tests |
| --- | --- | --- |
| `GaugeChartOpDescSpec` | `GaugeChartOpDesc` | 5 |
| `RangeSliderOpDescSpec` | `RangeSliderOpDesc` | 5 |
| `SankeyDiagramOpDescSpec` | `SankeyDiagramOpDesc` | 5 |
**Behavior pinned (each descriptor)**
| Surface | Contract |
| --- | --- |
| `operatorInfo` | exact name + visualization group (`Financial` /
`Basic` / `Basic`); one input / one output |
| `getOutputSchemas` | single `html-content` STRING column, asserted as
the **full map keyed by `operatorInfo.outputPorts.head.id`** (input
ignored — `Map.empty` proves it) |
| Field defaults | Gauge `value`/`delta`/`threshold == ""` + empty
`steps`; RangeSlider `xAxis`/`yAxis == ""`; Sankey
`source`/`target`/`value == ""` |
| `generatePythonCode` | Gauge `go.Indicator(`; RangeSlider
`go.Scatter(`; Sankey `go.Sankey(` (structural Python only) |
| Round-trip | config fields preserved through the polymorphic base |
Codegen assertions check only structural Python (class def / import /
plotly call) — never the interpolated `EncodableString` values, which
are base64-encoded at `.encode` time and do not appear literally.
### Any related issues, documentation, discussions?
Closes #5838.
### How was this PR tested?
- `sbt "WorkflowOperator/testOnly
org.apache.texera.amber.operator.visualization.gaugeChart.GaugeChartOpDescSpec
org.apache.texera.amber.operator.visualization.rangeSlider.RangeSliderOpDescSpec
org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDescSpec"`
— 15 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])
---
.../gaugeChart/GaugeChartOpDescSpec.scala | 85 ++++++++++++++++++++++
.../rangeSlider/RangeSliderOpDescSpec.scala | 77 ++++++++++++++++++++
.../sankeyDiagram/SankeyDiagramOpDescSpec.scala | 79 ++++++++++++++++++++
3 files changed, 241 insertions(+)
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartOpDescSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartOpDescSpec.scala
new file mode 100644
index 0000000000..f01947e61f
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartOpDescSpec.scala
@@ -0,0 +1,85 @@
+/*
+ * 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.gaugeChart
+
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import org.apache.texera.amber.operator.LogicalOp
+import org.apache.texera.amber.operator.metadata.OperatorGroupConstants
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class GaugeChartOpDescSpec extends AnyFlatSpec with Matchers {
+
+ "GaugeChartOpDesc.operatorInfo" should
+ "advertise the name and Financial visualization group" in {
+ val info = (new GaugeChartOpDesc).operatorInfo
+ info.userFriendlyName shouldBe "Gauge Chart"
+ info.operatorGroupName shouldBe
OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP
+ info.inputPorts should have length 1
+ info.outputPorts should have length 1
+ }
+
+ "GaugeChartOpDesc" should "default value/delta/threshold to empty and steps
to an empty list" in {
+ val d = new GaugeChartOpDesc
+ d.value shouldBe ""
+ d.delta shouldBe ""
+ d.threshold shouldBe ""
+ d.steps shouldBe empty
+ }
+
+ "GaugeChartOpDesc.getOutputSchemas" should
+ "produce a single html-content STRING column keyed by the declared output
port" in {
+ val op = new GaugeChartOpDesc
+ op.getOutputSchemas(Map.empty) shouldBe Map(
+ op.operatorInfo.outputPorts.head.id -> Schema().add("html-content",
AttributeType.STRING)
+ )
+ }
+
+ "GaugeChartOpDesc.generatePythonCode" should "emit a Plotly Indicator
figure" in {
+ val d = new GaugeChartOpDesc
+ d.value = "score"
+ val code = d.generatePythonCode()
+ code should include("class ProcessTableOperator(UDFTableOperator)")
+ code should include("plotly.graph_objects")
+ code should include("go.Indicator(")
+ }
+
+ "GaugeChartOpDesc" should
+ "round-trip value/delta/threshold and steps through the polymorphic base"
in {
+ val d = new GaugeChartOpDesc
+ d.value = "v"
+ d.delta = "dl"
+ d.threshold = "th"
+ val step = new GaugeChartSteps
+ step.start = "0"
+ step.end = "50"
+ d.steps = List(step)
+ val restored = objectMapper.readValue(objectMapper.writeValueAsString(d),
classOf[LogicalOp])
+ restored shouldBe a[GaugeChartOpDesc]
+ val g = restored.asInstanceOf[GaugeChartOpDesc]
+ g.value shouldBe "v"
+ g.delta shouldBe "dl"
+ g.threshold shouldBe "th"
+ g.steps should have length 1
+ g.steps.head.start shouldBe "0"
+ g.steps.head.end shouldBe "50"
+ }
+}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderOpDescSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderOpDescSpec.scala
new file mode 100644
index 0000000000..d601572c32
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderOpDescSpec.scala
@@ -0,0 +1,77 @@
+/*
+ * 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.rangeSlider
+
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import org.apache.texera.amber.operator.LogicalOp
+import org.apache.texera.amber.operator.metadata.OperatorGroupConstants
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class RangeSliderOpDescSpec extends AnyFlatSpec with Matchers {
+
+ "RangeSliderOpDesc.operatorInfo" should
+ "advertise the name and Basic visualization group" in {
+ val info = (new RangeSliderOpDesc).operatorInfo
+ info.userFriendlyName shouldBe "Range Slider"
+ info.operatorDescription shouldBe "Visualize data in a Range Slider"
+ info.operatorGroupName shouldBe
OperatorGroupConstants.VISUALIZATION_BASIC_GROUP
+ info.inputPorts should have length 1
+ info.outputPorts should have length 1
+ }
+
+ "RangeSliderOpDesc" should "default xAxis and yAxis to the empty string" in {
+ val d = new RangeSliderOpDesc
+ d.xAxis shouldBe ""
+ d.yAxis shouldBe ""
+ }
+
+ "RangeSliderOpDesc.getOutputSchemas" should
+ "produce a single html-content STRING column keyed by the declared output
port" in {
+ val op = new RangeSliderOpDesc
+ op.getOutputSchemas(Map.empty) shouldBe Map(
+ op.operatorInfo.outputPorts.head.id -> Schema().add("html-content",
AttributeType.STRING)
+ )
+ }
+
+ "RangeSliderOpDesc.generatePythonCode" should "emit a Plotly Scatter figure"
in {
+ val d = new RangeSliderOpDesc
+ d.xAxis = "x"
+ d.yAxis = "y"
+ val code = d.generatePythonCode()
+ code should include("class ProcessTableOperator(UDFTableOperator)")
+ code should include("go.Scatter(")
+ }
+
+ "RangeSliderOpDesc" should
+ "round-trip xAxis, yAxis, and duplicateType through the polymorphic base"
in {
+ val d = new RangeSliderOpDesc
+ d.xAxis = "month"
+ d.yAxis = "sales"
+ d.duplicateType = RangeSliderHandleDuplicateFunction.MEAN
+ val restored = objectMapper.readValue(objectMapper.writeValueAsString(d),
classOf[LogicalOp])
+ restored shouldBe a[RangeSliderOpDesc]
+ val r = restored.asInstanceOf[RangeSliderOpDesc]
+ r.xAxis shouldBe "month"
+ r.yAxis shouldBe "sales"
+ r.duplicateType shouldBe RangeSliderHandleDuplicateFunction.MEAN
+ }
+}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/sankeyDiagram/SankeyDiagramOpDescSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/sankeyDiagram/SankeyDiagramOpDescSpec.scala
new file mode 100644
index 0000000000..d22ac2b489
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/sankeyDiagram/SankeyDiagramOpDescSpec.scala
@@ -0,0 +1,79 @@
+/*
+ * 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.sankeyDiagram
+
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import org.apache.texera.amber.operator.LogicalOp
+import org.apache.texera.amber.operator.metadata.OperatorGroupConstants
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class SankeyDiagramOpDescSpec extends AnyFlatSpec with Matchers {
+
+ "SankeyDiagramOpDesc.operatorInfo" should
+ "advertise the name and Basic visualization group" in {
+ val info = (new SankeyDiagramOpDesc).operatorInfo
+ info.userFriendlyName shouldBe "Sankey Diagram"
+ info.operatorDescription shouldBe "Visualize data using a Sankey diagram"
+ info.operatorGroupName shouldBe
OperatorGroupConstants.VISUALIZATION_BASIC_GROUP
+ info.inputPorts should have length 1
+ info.outputPorts should have length 1
+ }
+
+ "SankeyDiagramOpDesc" should
+ "default sourceAttribute / targetAttribute / valueAttribute to the empty
string" in {
+ val d = new SankeyDiagramOpDesc
+ d.sourceAttribute shouldBe ""
+ d.targetAttribute shouldBe ""
+ d.valueAttribute shouldBe ""
+ }
+
+ "SankeyDiagramOpDesc.getOutputSchemas" should
+ "produce a single html-content STRING column keyed by the declared output
port" in {
+ val op = new SankeyDiagramOpDesc
+ op.getOutputSchemas(Map.empty) shouldBe Map(
+ op.operatorInfo.outputPorts.head.id -> Schema().add("html-content",
AttributeType.STRING)
+ )
+ }
+
+ "SankeyDiagramOpDesc.generatePythonCode" should "emit a Plotly Sankey
figure" in {
+ val d = new SankeyDiagramOpDesc
+ d.sourceAttribute = "src"
+ d.targetAttribute = "dst"
+ d.valueAttribute = "amount"
+ val code = d.generatePythonCode()
+ code should include("class ProcessTableOperator(UDFTableOperator)")
+ code should include("go.Sankey(")
+ }
+
+ "SankeyDiagramOpDesc" should "round-trip its three attributes through the
polymorphic base" in {
+ val d = new SankeyDiagramOpDesc
+ d.sourceAttribute = "src"
+ d.targetAttribute = "dst"
+ d.valueAttribute = "amount"
+ val restored = objectMapper.readValue(objectMapper.writeValueAsString(d),
classOf[LogicalOp])
+ restored shouldBe a[SankeyDiagramOpDesc]
+ val s = restored.asInstanceOf[SankeyDiagramOpDesc]
+ s.sourceAttribute shouldBe "src"
+ s.targetAttribute shouldBe "dst"
+ s.valueAttribute shouldBe "amount"
+ }
+}