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

Reply via email to