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
commit e87bc0b4285d2314b66c59ae8a1321baf12dbcc1 Author: Xinyuan Lin <[email protected]> AuthorDate: Sat Jun 20 20:58:49 2026 -0700 test(workflow-operator): add unit test coverage for visualization descriptors (ContourPlot, PolarChart, StripChart) (#5831) ### 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 | | --- | --- | --- | | `ContourPlotOpDescSpec` | `ContourPlotOpDesc` | 4 | | `PolarChartOpDescSpec` | `PolarChartOpDesc` | 5 | | `StripChartOpDescSpec` | `StripChartOpDesc` | 5 | **Behavior pinned** | Surface | Contract | | --- | --- | | `operatorInfo` | exact name + visualization group (Scientific / Scientific / Statistical); one input / one output | | `getOutputSchemas` | single `html-content` STRING column, asserted as the **full map keyed by `operatorInfo.outputPorts.head.id`** (input map ignored — `Map.empty` proves it) | | Field defaults | ContourPlot `x/y/z/gridSize == ""`, `connectGaps == false`; Polar `r/theta == ""`; Strip `x/y/colorBy/facetColumn == ""` | | `generatePythonCode` | Polar emits `go.Scatterpolargl(`; Strip emits `px.strip(` (structural Python only) | | Round-trip | all column fields preserved through the polymorphic base | **Notes for reviewers** - `ContourPlotOpDesc.generatePythonCode` is intentionally **not** exercised: a freshly-constructed instance has a `null` `coloringMethod` var, so codegen throws an NPE (the `@NotNull` is validation-layer only, not enforced at codegen). The spec pins the clean, deterministic contracts instead. - Codegen assertions check only structural Python (class def / import / plotly call) — never the interpolated `EncodableString` column values, which are base64-encoded at `.encode` time and do not appear literally. ### Any related issues, documentation, discussions? Closes #5828. ### How was this PR tested? - `sbt "WorkflowOperator/testOnly org.apache.texera.amber.operator.visualization.contourPlot.ContourPlotOpDescSpec org.apache.texera.amber.operator.visualization.polarChart.PolarChartOpDescSpec org.apache.texera.amber.operator.visualization.stripChart.StripChartOpDescSpec"` — 14 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]) --- .../contourPlot/ContourPlotOpDescSpec.scala | 76 ++++++++++++++++++++ .../polarChart/PolarChartOpDescSpec.scala | 75 ++++++++++++++++++++ .../stripChart/StripChartOpDescSpec.scala | 81 ++++++++++++++++++++++ 3 files changed, 232 insertions(+) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala new file mode 100644 index 0000000000..8464b1f520 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala @@ -0,0 +1,76 @@ +/* + * 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.contourPlot + +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 ContourPlotOpDescSpec extends AnyFlatSpec with Matchers { + + "ContourPlotOpDesc.operatorInfo" should + "advertise the name and Scientific visualization group" in { + val info = (new ContourPlotOpDesc).operatorInfo + info.userFriendlyName shouldBe "Contour Plot" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "ContourPlotOpDesc" should + "default the x/y/z/gridSize columns to empty and connectGaps to false" in { + val d = new ContourPlotOpDesc + d.x shouldBe "" + d.y shouldBe "" + d.z shouldBe "" + d.gridSize shouldBe "" + d.connectGaps shouldBe false + } + + "ContourPlotOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new ContourPlotOpDesc + // getOutputSchemas ignores its input; pass empty to prove that. + val out = op.getOutputSchemas(Map.empty) + out shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "ContourPlotOpDesc" should "round-trip its column fields through the polymorphic base" in { + val d = new ContourPlotOpDesc + d.x = "lon" + d.y = "lat" + d.z = "elev" + d.gridSize = "20" + d.connectGaps = true + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[ContourPlotOpDesc] + val c = restored.asInstanceOf[ContourPlotOpDesc] + c.x shouldBe "lon" + c.y shouldBe "lat" + c.z shouldBe "elev" + c.gridSize shouldBe "20" + c.connectGaps shouldBe true + } +} diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/polarChart/PolarChartOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/polarChart/PolarChartOpDescSpec.scala new file mode 100644 index 0000000000..79770d6ddd --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/polarChart/PolarChartOpDescSpec.scala @@ -0,0 +1,75 @@ +/* + * 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.polarChart + +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 PolarChartOpDescSpec extends AnyFlatSpec with Matchers { + + "PolarChartOpDesc.operatorInfo" should + "advertise the name and Scientific visualization group" in { + val info = (new PolarChartOpDesc).operatorInfo + info.userFriendlyName shouldBe "Polar Chart" + info.operatorDescription shouldBe "Displays data points in a polar scatter plot" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "PolarChartOpDesc" should "default r and theta to the empty string" in { + val d = new PolarChartOpDesc + d.r shouldBe "" + d.theta shouldBe "" + } + + "PolarChartOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new PolarChartOpDesc + op.getOutputSchemas(Map.empty) shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "PolarChartOpDesc.generatePythonCode" should "emit a Plotly Scatterpolargl figure" in { + val d = new PolarChartOpDesc + d.r = "radius" + d.theta = "angle" + val code = d.generatePythonCode() + code should include("class ProcessTableOperator(UDFTableOperator)") + code should include("plotly.graph_objects") + code should include("go.Scatterpolargl(") + } + + "PolarChartOpDesc" should "round-trip r and theta through the polymorphic base" in { + val d = new PolarChartOpDesc + d.r = "radius" + d.theta = "angle" + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[PolarChartOpDesc] + val p = restored.asInstanceOf[PolarChartOpDesc] + p.r shouldBe "radius" + p.theta shouldBe "angle" + } +} diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/stripChart/StripChartOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/stripChart/StripChartOpDescSpec.scala new file mode 100644 index 0000000000..4869289eb5 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/stripChart/StripChartOpDescSpec.scala @@ -0,0 +1,81 @@ +/* + * 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.stripChart + +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 StripChartOpDescSpec extends AnyFlatSpec with Matchers { + + "StripChartOpDesc.operatorInfo" should + "advertise the name and Statistical visualization group" in { + val info = (new StripChartOpDesc).operatorInfo + info.userFriendlyName shouldBe "Strip Chart" + info.operatorDescription shouldBe "Visualize distribution of data points as a strip plot" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "StripChartOpDesc" should "default x / y / colorBy / facetColumn to the empty string" in { + val d = new StripChartOpDesc + d.x shouldBe "" + d.y shouldBe "" + d.colorBy shouldBe "" + d.facetColumn shouldBe "" + } + + "StripChartOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new StripChartOpDesc + op.getOutputSchemas(Map.empty) shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "StripChartOpDesc.generatePythonCode" should "emit a Plotly px.strip figure" in { + val d = new StripChartOpDesc + d.x = "category" + d.y = "value" + val code = d.generatePythonCode() + code should include("class ProcessTableOperator(UDFTableOperator)") + code should include("plotly.express") + code should include("px.strip(") + } + + "StripChartOpDesc" should "round-trip all four column fields through the polymorphic base" in { + val d = new StripChartOpDesc + d.x = "cat" + d.y = "val" + d.colorBy = "grp" + d.facetColumn = "panel" + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[StripChartOpDesc] + val s = restored.asInstanceOf[StripChartOpDesc] + s.x shouldBe "cat" + s.y shouldBe "val" + s.colorBy shouldBe "grp" + s.facetColumn shouldBe "panel" + } +}
