This is an automated email from the ASF dual-hosted git repository.
aglinxinyuan 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 16d3c6a169 fix(lineChart): prevent NullPointerException when lines is
not configured (#4919)
16d3c6a169 is described below
commit 16d3c6a169341d1c44f98de0bb84caf89df16f07
Author: Matthew B. <[email protected]>
AuthorDate: Wed May 6 23:08:30 2026 -0700
fix(lineChart): prevent NullPointerException when lines is not configured
(#4919)
### What changes were proposed in this PR?
LineChartOpDesc declared var lines: util.List[LineConfig] = _, which
defaults to null in Scala/Java. Calling generatePythonCode() on a
freshly constructed instance immediately crashed with a
NullPointerException at
createPlotlyFigure because .asScala.map(...) was called on a null
reference.
Two changes:
- Default lines to new util.ArrayList[LineConfig]() instead of null, so
it is always safe to iterate.
- Add assert(lines.asScala.nonEmpty, "At least one line must be
configured") at the top of createPlotlyFigure(), matching the explicit
assertion pattern used by sibling visualizers (HeatMapOpDesc,
BarChartOpDesc).
### Any related issues, documentation, or discussions?
closes: #4811
### How was this PR tested?
Unit tests in LineChartOpDescSpec cover:
- Default-constructed LineChartOpDesc throws AssertionError with a
descriptive message when lines is empty
- Explicitly setting lines to an empty list also throws AssertionError
- A fully configured operator with at least one LineConfig generates
valid Python code containing the expected Plotly structure
### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude opus 4.6 in compliance with ASF
---------
Co-authored-by: Xinyuan Lin <[email protected]>
---
.../visualization/lineChart/LineChartOpDesc.scala | 3 +-
.../lineChart/LineChartOpDescSpec.scala | 36 ++++++++++++++--------
2 files changed, 26 insertions(+), 13 deletions(-)
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala
index 62cabb12a8..bbc4fbfa6a 100644
---
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala
@@ -45,7 +45,7 @@ class LineChartOpDesc extends PythonOperatorDescriptor {
var xLabel: EncodableString = ""
@JsonProperty(value = "lines", required = true)
- var lines: util.List[LineConfig] = _
+ var lines: util.List[LineConfig] = new util.ArrayList[LineConfig]()
override def getOutputSchemas(
inputSchemas: Map[PortIdentity, Schema]
@@ -64,6 +64,7 @@ class LineChartOpDesc extends PythonOperatorDescriptor {
)
def createPlotlyFigure(): PythonTemplateBuilder = {
+ assert(lines != null && !lines.isEmpty, "At least one line must be
configured")
val linesPart = lines.asScala
.map { lineConf =>
val colorPart = if (lineConf.color != "") {
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala
index 2608437098..dc5323538f 100644
---
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala
@@ -96,22 +96,34 @@ class LineChartOpDescSpec extends AnyFlatSpec with Matchers
{
code should not include "Y_LBL_SENT"
}
- it should "raise NullPointerException when lines is left at its null
default" in {
- // Pin: `var lines: util.List[LineConfig] = _` defaults to null, and
- // `createPlotlyFigure` calls `lines.asScala.map(...)` without a null
- // check. Calling `generatePythonCode` on a default-constructed LineChart
- // therefore throws NPE rather than rendering an empty chart or raising
- // an AssertionError. Documenting so a future fix that null-guards lines
- // breaks this spec deliberately.
+ it should "raise AssertionError when lines is left at its default (empty
list)" in {
+ // `var lines: util.List[LineConfig]` defaults to an empty ArrayList.
+ // `createPlotlyFigure` asserts nonEmpty on lines before iterating, so a
+ // default-constructed LineChartOpDesc raises AssertionError with a
+ // descriptive message rather than proceeding with no traces.
val op = new LineChartOpDesc
- assertThrows[NullPointerException](op.generatePythonCode())
+ val ex = intercept[AssertionError](op.generatePythonCode())
+ ex.getMessage should include("At least one line must be configured")
}
- it should "render code with an empty lines list (no NPE, no assertion)" in {
+ it should "raise AssertionError when lines is explicitly set to an empty
list" in {
+ // `createPlotlyFigure` guards against an empty lines list with an
assertion,
+ // matching the fail-fast pattern used by sibling visualizers
+ // (HeatMapOpDesc, BarChartOpDesc).
val op = configured
op.lines = new util.ArrayList[LineConfig]()
- val code = op.generatePythonCode()
- code should include("plotly")
- code should include("fig = go.Figure()")
+ assertThrows[AssertionError](op.generatePythonCode())
+ }
+
+ it should "raise AssertionError rather than NullPointerException when lines
is set to null" in {
+ // `lines` is a public mutable field; Jackson deserializing an explicit
JSON
+ // null or a caller assigning null can set it back to null even after the
+ // non-null default is in place. `createPlotlyFigure` wraps `lines` in
+ // `Option(...).getOrElse(emptyList)` before asserting nonEmpty, so a null
+ // assignment produces the descriptive AssertionError rather than an NPE.
+ val op = configured
+ op.lines = null
+ val ex = intercept[AssertionError](op.generatePythonCode())
+ ex.getMessage should include("At least one line must be configured")
}
}