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-5844-444862845a13e57edd4df4ba0b85f05143059fcb in repository https://gitbox.apache.org/repos/asf/texera.git
commit 65f185757f7d34a583ea0ce9657afb9cc6f85076 Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Jun 21 12:19:33 2026 -0700 test(workflow-operator): add unit test coverage for visualization descriptors (CarpetPlot, DumbbellPlot, ParallelCoordinatesPlot) (#5844) ### What changes were proposed in this PR? Pin behavior of three more previously-untested visualization `PythonOperatorDescriptor`s in `common/workflow-operator/`. No production-code changes. | Spec | Source class | Tests | | --- | --- | --- | | `CarpetPlotOpDescSpec` | `CarpetPlotOpDesc` | 5 | | `DumbbellPlotOpDescSpec` | `DumbbellPlotOpDesc` | 5 | | `ParallelCoordinatesPlotOpDescSpec` | `ParallelCoordinatesPlotOpDesc` | 5 | **Behavior pinned (each descriptor)** | Surface | Contract | | --- | --- | | `operatorInfo` | exact name + visualization group (`Scientific` / `Basic` / `Scientific`); one input / one output | | `getOutputSchemas` | single `html-content` STRING column, asserted as the full map keyed by `operatorInfo.outputPorts.head.id` | | Field defaults | Carpet `a`/`b`/`y == ""`; Dumbbell column fields `== ""` + `showLegends == false`; ParallelCoordinates `dimensions` empty | | `generatePythonCode` | Carpet `go.Carpet(`; Dumbbell `go.Scatter(`; ParallelCoordinates `px.parallel_coordinates(` (structural Python only) | | Round-trip | config fields preserved through the polymorphic base | **Note for reviewers:** `ParallelCoordinatesPlotOpDesc.color` defaults to `null` and `dimensions` to empty — both are null/empty-guarded in `generatePythonCode`, so codegen on a fresh instance is exercised safely. Codegen assertions pin only structural Python, never the base64-encoded `EncodableString` values. ### Any related issues, documentation, discussions? Closes #5839. ### How was this PR tested? - `sbt "WorkflowOperator/testOnly org.apache.texera.amber.operator.visualization.carpetPlot.CarpetPlotOpDescSpec org.apache.texera.amber.operator.visualization.dumbbellPlot.DumbbellPlotOpDescSpec org.apache.texera.amber.operator.visualization.parallelCoordinatesPlot.ParallelCoordinatesPlotOpDescSpec"` — 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]) --- .../carpetPlot/CarpetPlotOpDescSpec.scala | 78 +++++++++++++++++++ .../dumbbellPlot/DumbbellPlotOpDescSpec.scala | 88 ++++++++++++++++++++++ .../ParallelCoordinatesPlotOpDescSpec.scala | 72 ++++++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDescSpec.scala new file mode 100644 index 0000000000..05976ef9c2 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDescSpec.scala @@ -0,0 +1,78 @@ +/* + * 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.carpetPlot + +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 CarpetPlotOpDescSpec extends AnyFlatSpec with Matchers { + + "CarpetPlotOpDesc.operatorInfo" should + "advertise the name and Scientific visualization group" in { + val info = (new CarpetPlotOpDesc).operatorInfo + info.userFriendlyName shouldBe "Carpet Plot" + info.operatorDescription shouldBe "Visualize data in a Carpet Plot" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "CarpetPlotOpDesc" should "default a / b / y to the empty string" in { + val d = new CarpetPlotOpDesc + d.a shouldBe "" + d.b shouldBe "" + d.y shouldBe "" + } + + "CarpetPlotOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new CarpetPlotOpDesc + op.getOutputSchemas(Map.empty) shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "CarpetPlotOpDesc.generatePythonCode" should "emit a Plotly Carpet figure" in { + val d = new CarpetPlotOpDesc + d.a = "ax" + d.b = "bx" + d.y = "yx" + val code = d.generatePythonCode() + code should include("class ProcessTableOperator(UDFTableOperator)") + code should include("go.Carpet(") + } + + "CarpetPlotOpDesc" should "round-trip a / b / y through the polymorphic base" in { + val d = new CarpetPlotOpDesc + d.a = "ax" + d.b = "bx" + d.y = "yx" + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[CarpetPlotOpDesc] + val c = restored.asInstanceOf[CarpetPlotOpDesc] + c.a shouldBe "ax" + c.b shouldBe "bx" + c.y shouldBe "yx" + } +} diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellPlotOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellPlotOpDescSpec.scala new file mode 100644 index 0000000000..104a234500 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellPlotOpDescSpec.scala @@ -0,0 +1,88 @@ +/* + * 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.dumbbellPlot + +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 DumbbellPlotOpDescSpec extends AnyFlatSpec with Matchers { + + "DumbbellPlotOpDesc.operatorInfo" should + "advertise the name and Basic visualization group" in { + val info = (new DumbbellPlotOpDesc).operatorInfo + info.userFriendlyName shouldBe "Dumbbell Plot" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "DumbbellPlotOpDesc" should "default its column fields to empty and showLegends to false" in { + val d = new DumbbellPlotOpDesc + d.categoryColumnName shouldBe "" + d.dumbbellStartValue shouldBe "" + d.dumbbellEndValue shouldBe "" + d.measurementColumnName shouldBe "" + d.comparedColumnName shouldBe "" + d.showLegends shouldBe false + } + + "DumbbellPlotOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new DumbbellPlotOpDesc + op.getOutputSchemas(Map.empty) shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "DumbbellPlotOpDesc.generatePythonCode" should "emit a Plotly Scatter (dumbbell) figure" in { + val d = new DumbbellPlotOpDesc + d.categoryColumnName = "entity" + d.measurementColumnName = "metric" + d.comparedColumnName = "phase" + d.dumbbellStartValue = "before" + d.dumbbellEndValue = "after" + val code = d.generatePythonCode() + code should include("class ProcessTableOperator(UDFTableOperator)") + code should include("go.Scatter(") + } + + "DumbbellPlotOpDesc" should "round-trip its column fields through the polymorphic base" in { + val d = new DumbbellPlotOpDesc + d.categoryColumnName = "entity" + d.measurementColumnName = "metric" + d.comparedColumnName = "phase" + d.dumbbellStartValue = "before" + d.dumbbellEndValue = "after" + d.showLegends = true + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[DumbbellPlotOpDesc] + val dp = restored.asInstanceOf[DumbbellPlotOpDesc] + dp.categoryColumnName shouldBe "entity" + dp.measurementColumnName shouldBe "metric" + dp.comparedColumnName shouldBe "phase" + dp.dumbbellStartValue shouldBe "before" + dp.dumbbellEndValue shouldBe "after" + dp.showLegends shouldBe true + } +} diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/parallelCoordinatesPlot/ParallelCoordinatesPlotOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/parallelCoordinatesPlot/ParallelCoordinatesPlotOpDescSpec.scala new file mode 100644 index 0000000000..61ac9755e7 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/parallelCoordinatesPlot/ParallelCoordinatesPlotOpDescSpec.scala @@ -0,0 +1,72 @@ +/* + * 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.parallelCoordinatesPlot + +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 ParallelCoordinatesPlotOpDescSpec extends AnyFlatSpec with Matchers { + + "ParallelCoordinatesPlotOpDesc.operatorInfo" should + "advertise the name and Scientific visualization group" in { + val info = (new ParallelCoordinatesPlotOpDesc).operatorInfo + info.userFriendlyName shouldBe "Parallel Coordinates Plot" + info.operatorDescription shouldBe "Visualize multivariate data using parallel coordinate axes" + info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP + info.inputPorts should have length 1 + info.outputPorts should have length 1 + } + + "ParallelCoordinatesPlotOpDesc" should "default dimensions to an empty list" in { + (new ParallelCoordinatesPlotOpDesc).dimensions shouldBe empty + } + + "ParallelCoordinatesPlotOpDesc.getOutputSchemas" should + "produce a single html-content STRING column keyed by the declared output port" in { + val op = new ParallelCoordinatesPlotOpDesc + op.getOutputSchemas(Map.empty) shouldBe Map( + op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", AttributeType.STRING) + ) + } + + "ParallelCoordinatesPlotOpDesc.generatePythonCode" should + "emit a Plotly parallel_coordinates figure (even with no dimensions / null color)" in { + // color defaults to null and dimensions to empty; both are guarded in codegen. + val code = (new ParallelCoordinatesPlotOpDesc).generatePythonCode() + code should include("class ProcessTableOperator(UDFTableOperator)") + code should include("px.parallel_coordinates(") + } + + "ParallelCoordinatesPlotOpDesc" should + "round-trip dimensions and color through the polymorphic base" in { + val d = new ParallelCoordinatesPlotOpDesc + d.dimensions = List("d1", "d2") + d.color = "grp" + val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), classOf[LogicalOp]) + restored shouldBe a[ParallelCoordinatesPlotOpDesc] + val p = restored.asInstanceOf[ParallelCoordinatesPlotOpDesc] + p.dimensions shouldBe List("d1", "d2") + p.color shouldBe "grp" + } +}
