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-5843-74fecdfa121fc864c4f5f674157791d75c28725c in repository https://gitbox.apache.org/repos/asf/texera.git
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" + } +}
