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-b2886ca8b1bc6658c4bfb362a9d328e668038b3b
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 74fecdfa121fc864c4f5f674157791d75c28725c
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"
+  }
+}

Reply via email to