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"
+  }
+}

Reply via email to