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

commit 6d31f46ecfe894fb04f783972c8f9566afb5a16b
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat Jun 20 20:58:52 2026 -0700

    test(workflow-operator): add unit test coverage for visualization operator 
descriptors (#5827)
    
    ### What changes were proposed in this PR?
    
    Pin behavior of three previously-untested visualization descriptors in
    `common/workflow-operator/`. No production-code changes.
    
    | Spec | Source class | Tests |
    | --- | --- | --- |
    | `HtmlVizOpDescSpec` | `HtmlVizOpDesc` | 4 |
    | `CandlestickChartOpDescSpec` | `CandlestickChartOpDesc` | 5 |
    | `Histogram2DOpDescSpec` | `Histogram2DOpDesc` | 6 |
    
    All three spec files follow the `<srcClassName>Spec.scala` one-to-one
    convention.
    
    **Behavior pinned**
    
    | Surface | Contract |
    | --- | --- |
    | `operatorInfo` | exact name + visualization group (`MEDIA` /
    `FINANCIAL` / `STATISTICAL`); one input / one output |
    | Output schema | all three emit a single `html-content` STRING column
    (`HtmlViz` via `getExternalOutputSchemas`; charts via
    `getOutputSchemas`) |
    | `getPhysicalOp` wiring (`HtmlViz`) |
    `OpExecWithClassName("…htmlviz.HtmlVizOpExec")`; port **identities**
    carried forward |
    | Field defaults | `Candlestick` OHLC columns default `""`;
    `Histogram2D` `xBins`/`yBins == 10`, `normalize == DENSITY` |
    | `generatePythonCode` | `Candlestick` emits a Plotly `go.Candlestick(`
    figure; `Histogram2D` emits `px.density_heatmap(` and **rejects a
    non-positive bin count** (`AssertionError`) |
    | Round-trip | all config fields preserved through the polymorphic base
    |
    
    The specs pin the stable contract (operatorInfo + output schema +
    codegen guards) rather than the full Plotly template, and never assert
    on interpolated `EncodableString` values (which are decoded at runtime,
    not embedded raw).
    
    ### Any related issues, documentation, discussions?
    
    Closes #5824.
    
    ### How was this PR tested?
    
    Pure unit-test additions; verified locally with:
    
    - `sbt "WorkflowOperator/testOnly
    org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpDescSpec
    
org.apache.texera.amber.operator.visualization.candlestickChart.CandlestickChartOpDescSpec
    
org.apache.texera.amber.operator.visualization.histogram2d.Histogram2DOpDescSpec"`
    — 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])
---
 .../CandlestickChartOpDescSpec.scala               | 85 +++++++++++++++++++
 .../histogram2d/Histogram2DOpDescSpec.scala        | 93 +++++++++++++++++++++
 .../visualization/htmlviz/HtmlVizOpDescSpec.scala  | 94 ++++++++++++++++++++++
 3 files changed, 272 insertions(+)

diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/candlestickChart/CandlestickChartOpDescSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/candlestickChart/CandlestickChartOpDescSpec.scala
new file mode 100644
index 0000000000..cdaabd3bc3
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/candlestickChart/CandlestickChartOpDescSpec.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.candlestickChart
+
+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 CandlestickChartOpDescSpec extends AnyFlatSpec with Matchers {
+
+  private def configured(): CandlestickChartOpDesc = {
+    val d = new CandlestickChartOpDesc
+    d.date = "day"
+    d.open = "o"
+    d.high = "h"
+    d.low = "l"
+    d.close = "c"
+    d
+  }
+
+  "CandlestickChartOpDesc.operatorInfo" should
+    "advertise the name and Financial visualization group" in {
+    val info = (new CandlestickChartOpDesc).operatorInfo
+    info.userFriendlyName shouldBe "Candlestick Chart"
+    info.operatorGroupName shouldBe 
OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP
+    info.inputPorts should have length 1
+    info.outputPorts should have length 1
+  }
+
+  "CandlestickChartOpDesc" should "default all OHLC column fields to the empty 
string" in {
+    val d = new CandlestickChartOpDesc
+    d.date shouldBe ""
+    d.open shouldBe ""
+    d.high shouldBe ""
+    d.low shouldBe ""
+    d.close shouldBe ""
+  }
+
+  "CandlestickChartOpDesc.getOutputSchemas" should
+    "produce a single html-content STRING column keyed by the declared output 
port" in {
+    val op = new CandlestickChartOpDesc
+    val out = op.getOutputSchemas(Map(op.operatorInfo.inputPorts.head.id -> 
Schema()))
+    out shouldBe Map(
+      op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", 
AttributeType.STRING)
+    )
+  }
+
+  "CandlestickChartOpDesc.generatePythonCode" should "emit a Plotly 
Candlestick figure" in {
+    val code = configured().generatePythonCode()
+    code should include("go.Candlestick(")
+    code should include("html-content")
+  }
+
+  "CandlestickChartOpDesc" should "round-trip its OHLC fields through the 
polymorphic base" in {
+    val restored =
+      objectMapper.readValue(objectMapper.writeValueAsString(configured()), 
classOf[LogicalOp])
+    restored shouldBe a[CandlestickChartOpDesc]
+    val c = restored.asInstanceOf[CandlestickChartOpDesc]
+    c.date shouldBe "day"
+    c.open shouldBe "o"
+    c.high shouldBe "h"
+    c.low shouldBe "l"
+    c.close shouldBe "c"
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/histogram2d/Histogram2DOpDescSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/histogram2d/Histogram2DOpDescSpec.scala
new file mode 100644
index 0000000000..bc56ab72b1
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/histogram2d/Histogram2DOpDescSpec.scala
@@ -0,0 +1,93 @@
+/*
+ * 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.histogram2d
+
+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 Histogram2DOpDescSpec extends AnyFlatSpec with Matchers {
+
+  private def configured(): Histogram2DOpDesc = {
+    val d = new Histogram2DOpDesc
+    d.xColumn = "x"
+    d.yColumn = "y"
+    d
+  }
+
+  "Histogram2DOpDesc.operatorInfo" should
+    "advertise the name and Statistical visualization group" in {
+    val info = (new Histogram2DOpDesc).operatorInfo
+    info.userFriendlyName shouldBe "Histogram2D"
+    info.operatorGroupName shouldBe 
OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP
+    info.inputPorts should have length 1
+    info.outputPorts should have length 1
+  }
+
+  "Histogram2DOpDesc" should "default bins to 10 and normalization to DENSITY" 
in {
+    val d = new Histogram2DOpDesc
+    d.xBins shouldBe 10
+    d.yBins shouldBe 10
+    d.normalize shouldBe NormalizationType.DENSITY
+    d.xColumn shouldBe ""
+    d.yColumn shouldBe ""
+  }
+
+  "Histogram2DOpDesc.getOutputSchemas" should
+    "produce a single html-content STRING column keyed by the declared output 
port" in {
+    val op = new Histogram2DOpDesc
+    val out = op.getOutputSchemas(Map(op.operatorInfo.inputPorts.head.id -> 
Schema()))
+    out shouldBe Map(
+      op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", 
AttributeType.STRING)
+    )
+  }
+
+  "Histogram2DOpDesc.generatePythonCode" should "reject a non-positive bin 
count" in {
+    val d = configured()
+    d.xBins = 0
+    intercept[AssertionError] {
+      d.generatePythonCode()
+    }
+  }
+
+  it should "emit a Plotly density-heatmap figure for a valid config" in {
+    val code = configured().generatePythonCode()
+    code should include("px.density_heatmap(")
+    code should include("html-content")
+  }
+
+  "Histogram2DOpDesc" should "round-trip its fields through the polymorphic 
base" in {
+    val d = configured()
+    d.xBins = 20
+    d.yBins = 5
+    d.normalize = NormalizationType.PROBABILITY
+    val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), 
classOf[LogicalOp])
+    restored shouldBe a[Histogram2DOpDesc]
+    val h = restored.asInstanceOf[Histogram2DOpDesc]
+    h.xColumn shouldBe "x"
+    h.yColumn shouldBe "y"
+    h.xBins shouldBe 20
+    h.yBins shouldBe 5
+    h.normalize shouldBe NormalizationType.PROBABILITY
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpDescSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpDescSpec.scala
new file mode 100644
index 0000000000..09ceafc3ce
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpDescSpec.scala
@@ -0,0 +1,94 @@
+/*
+ * 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.htmlviz
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import javax.validation.constraints.NotNull
+import org.apache.texera.amber.core.executor.OpExecWithClassName
+import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema}
+import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, 
WorkflowIdentity}
+import org.apache.texera.amber.operator.LogicalOp
+import org.apache.texera.amber.operator.metadata.OperatorGroupConstants
+import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class HtmlVizOpDescSpec extends AnyFlatSpec with Matchers {
+
+  private val workflowId = WorkflowIdentity(1L)
+  private val executionId = ExecutionIdentity(1L)
+
+  "HtmlVizOpDesc.operatorInfo" should "advertise the name and Media 
visualization group" in {
+    val info = (new HtmlVizOpDesc).operatorInfo
+    info.userFriendlyName shouldBe "HTML Visualizer"
+    info.operatorGroupName shouldBe 
OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP
+    info.inputPorts should have length 1
+    info.outputPorts should have length 1
+  }
+
+  "HtmlVizOpDesc.htmlContentAttrName" should "default to the empty string" in {
+    (new HtmlVizOpDesc).htmlContentAttrName shouldBe ""
+  }
+
+  it should "carry @JsonProperty(required = true) + @AutofillAttributeName + 
@NotNull" in {
+    val field = classOf[HtmlVizOpDesc].getDeclaredField("htmlContentAttrName")
+    val jp = field.getAnnotation(classOf[JsonProperty])
+    jp should not be null
+    jp.required shouldBe true
+    field.getAnnotation(classOf[AutofillAttributeName]) should not be null
+    val notNull = field.getAnnotation(classOf[NotNull])
+    notNull should not be null
+    notNull.message shouldBe "HTML content cannot be empty"
+  }
+
+  "HtmlVizOpDesc" should "round-trip htmlContentAttrName through the 
polymorphic base" in {
+    val op = new HtmlVizOpDesc
+    op.htmlContentAttrName = "myCol"
+    val restored = objectMapper.readValue(objectMapper.writeValueAsString(op), 
classOf[LogicalOp])
+    restored shouldBe a[HtmlVizOpDesc]
+    restored.asInstanceOf[HtmlVizOpDesc].htmlContentAttrName shouldBe "myCol"
+  }
+
+  "HtmlVizOpDesc.getPhysicalOp" should "wire HtmlVizOpExec and carry port 
identities" in {
+    val op = new HtmlVizOpDesc
+    op.htmlContentAttrName = "html"
+    val physical = op.getPhysicalOp(workflowId, executionId)
+    physical.opExecInitInfo match {
+      case OpExecWithClassName(className, descString) =>
+        className shouldBe 
"org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpExec"
+        descString should not be empty
+      case other => fail(s"expected OpExecWithClassName, got $other")
+    }
+    physical.inputPorts.keySet shouldBe 
op.operatorInfo.inputPorts.map(_.id).toSet
+    physical.outputPorts.keySet shouldBe 
op.operatorInfo.outputPorts.map(_.id).toSet
+  }
+
+  "HtmlVizOpDesc schema propagation" should
+    "emit a single html-content STRING column keyed by the declared output 
port" in {
+    val op = new HtmlVizOpDesc
+    op.htmlContentAttrName = "html"
+    val input = Schema().add(new Attribute("html", AttributeType.STRING))
+    val out = 
op.getExternalOutputSchemas(Map(op.operatorInfo.inputPorts.head.id -> input))
+    out shouldBe Map(
+      op.operatorInfo.outputPorts.head.id -> Schema().add("html-content", 
AttributeType.STRING)
+    )
+  }
+}

Reply via email to