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
The following commit(s) were added to refs/heads/main by this push:
new 8be174308d test(workflow-operator): guard visualization output schemas
(#7768)
8be174308d is described below
commit 8be174308d4e597f120253cc68dbd4f21c0af1ed
Author: Eugene Gu <[email protected]>
AuthorDate: Wed Aug 19 04:38:51 2026 +0000
test(workflow-operator): guard visualization output schemas (#7768)
### What changes were proposed in this PR?
The frontend renders a chart by reading a single `html-content` STRING
column from a visualization operator's one output port. All 48 Python
visualization descriptors re-declare that contract in identical
three-line `getOutputSchemas` overrides, but 24 of them never had the
method called by any test, and no spec swept the operator registry for
the contract as a whole — so a new chart, or an edit to an existing one,
could silently ship a schema the frontend cannot render.
This PR adds one new guard spec, `VisualizationOutputSchemaSpec`,
instead of 24 per-operator tests. It walks
`OperatorMetadataGenerator.operatorTypeMap`, selects the visualization
descriptors by package, instantiates each, and asserts the contract, so
the next chart operator is covered the moment it is registered. Four
tests: a floor assertion (at least 48 swept) so a package rename cannot
turn the sweep into a vacuous pass; an assertion that the only
visualization-package registrations outside `PythonOperatorDescriptor`
are exactly `HtmlVizOpDesc` and `UrlVizOpDesc`, which derive their
schemas through `SchemaPropagationFunc` and are covered by their own
specs, so a new non-Python visualization operator must be consciously
added to the exclusion list; the main per-descriptor sweep asserting
exactly one output port whose schema is a single `html-content` STRING
attribute, with a per-operator `withClue` for diagnosis; and an
assertion that the derived schema is independent of the input schemas.
No production code changed; all 48 descriptors satisfy the contract
today. The registry-wide-guard shape follows the precedents in the same
package, `OutputPortReuseFlagSpec` and `AttributeTypeRuleTargetSpec`
(#7249).
### Any related issues, documentation, discussions?
Resolves #7766
### How was this PR tested?
This PR is test-only. The new suite passes locally: `sbt
"WorkflowOperator/testOnly *VisualizationOutputSchemaSpec"` runs 4 tests
sweeping all 48 descriptors, all green, and
`WorkflowOperator/Test/scalafmtCheck` passes. The full
`WorkflowOperator/test` suite was also run locally to confirm the new
spec introduces no cross-test interference.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Fable 5)
---
.../metadata/VisualizationOutputSchemaSpec.scala | 108 +++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/metadata/VisualizationOutputSchemaSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/metadata/VisualizationOutputSchemaSpec.scala
new file mode 100644
index 0000000000..d1f5ee04a0
--- /dev/null
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/metadata/VisualizationOutputSchemaSpec.scala
@@ -0,0 +1,108 @@
+/*
+ * 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.metadata
+
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import org.apache.texera.amber.core.workflow.PortIdentity
+import org.apache.texera.amber.operator.PythonOperatorDescriptor
+import org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpDesc
+import org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpDesc
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+/**
+ * Guard for the visualization output-schema contract, over every registered
+ * visualization descriptor.
+ *
+ * The frontend locates the chart HTML a visualization returns by reading a
+ * single `html-content` STRING column from the operator's one output port.
+ * Every Python visualization descriptor re-declares that contract in its own
+ * `getOutputSchemas` override, so a new chart (or an edit to an existing one)
+ * can silently ship a schema the frontend cannot render, and only that
+ * operator's own spec -- if it asserts the schema at all -- would notice.
+ *
+ * Sweeping the registry rather than testing the descriptors one by one is
+ * deliberate: the contract lives in 48 identical three-line overrides, so the
+ * mistake is as easy to make in the next chart as in these, and a
per-operator
+ * test would not exist for the next chart until someone remembers to write
it.
+ */
+class VisualizationOutputSchemaSpec extends AnyFlatSpec with Matchers {
+
+ // HtmlViz and UrlViz are Scala map operators: they do not extend
+ // PythonOperatorDescriptor and derive their output schema through
+ // getPhysicalOp's SchemaPropagationFunc instead, covered by their own specs.
+ // They are excluded by explicit class so that any NEW visualization
+ // descriptor is swept by default and must be consciously added here to
+ // escape the contract.
+ private val nonPythonVisualizations: Set[Class[_]] =
+ Set(classOf[HtmlVizOpDesc], classOf[UrlVizOpDesc])
+
+ private val visualizationClasses =
+ OperatorMetadataGenerator.operatorTypeMap.keys.toSeq
+
.filter(_.getName.startsWith("org.apache.texera.amber.operator.visualization."))
+ .sortBy(_.getSimpleName)
+
+ private val sweptClasses =
+ visualizationClasses.filterNot(nonPythonVisualizations.contains)
+
+ private def instantiate(opClass: Class[_]): PythonOperatorDescriptor =
+
opClass.getConstructor().newInstance().asInstanceOf[PythonOperatorDescriptor]
+
+ "the registry" should "contain the visualization descriptors this sweep
guards" in {
+ // If the selection ever comes back empty (say, the package is renamed),
+ // the per-operator assertions below would vacuously pass; pin a floor.
+ sweptClasses.size should be >= 48
+ }
+
+ it should "register no visualization descriptor outside the Python contract
except the known two" in {
+ val nonPython =
+
visualizationClasses.filterNot(classOf[PythonOperatorDescriptor].isAssignableFrom)
+ nonPython.toSet shouldBe nonPythonVisualizations
+ }
+
+ "every Python visualization descriptor" should "declare a single
html-content STRING column on its one output port" in {
+ sweptClasses.foreach { opClass =>
+ withClue(s"${opClass.getSimpleName}: ") {
+ val op = instantiate(opClass)
+ val outputPortIds = op.operatorInfo.outputPorts.map(_.id)
+ outputPortIds should have length 1
+
+ val outputSchemas = op.getOutputSchemas(Map.empty[PortIdentity,
Schema])
+ outputSchemas.keySet shouldBe Set(outputPortIds.head)
+
+ val schema = outputSchemas(outputPortIds.head)
+ schema.getAttributeNames shouldBe List("html-content")
+ schema.getAttribute("html-content").getType shouldBe
AttributeType.STRING
+ }
+ }
+ }
+
+ it should "derive that schema independently of the input schemas" in {
+ val populatedInput =
+ Map(PortIdentity() -> Schema().add("x", AttributeType.INTEGER))
+ sweptClasses.foreach { opClass =>
+ withClue(s"${opClass.getSimpleName}: ") {
+ val op = instantiate(opClass)
+ op.getOutputSchemas(Map.empty[PortIdentity, Schema]) shouldBe
+ op.getOutputSchemas(populatedInput)
+ }
+ }
+ }
+}