diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 8138f58bcca23..33bb48b3339f3 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -59,6 +59,7 @@ ROOT_ADD_PYUNITTEST(pyroot_pyz_ttree_branch ttree_branch.py PYTHON_DEPS numpy) # TColor-related pythonizations ROOT_ADD_PYUNITTEST(pyroot_pyz_tcolor tcolor.py) +ROOT_ADD_PYUNITTEST(regression_20018 regression_20018.py) # TH1 and subclasses pythonizations ROOT_ADD_PYUNITTEST(pyroot_pyz_th1 th1.py) diff --git a/bindings/pyroot/pythonizations/test/regression_20018.py b/bindings/pyroot/pythonizations/test/regression_20018.py new file mode 100644 index 0000000000000..7a685d95bac04 --- /dev/null +++ b/bindings/pyroot/pythonizations/test/regression_20018.py @@ -0,0 +1,45 @@ +import unittest + +import ROOT + + +class Regression20018(unittest.TestCase): + """ + Regression test for https://github.com/root-project/root/issues/20018 + + Calling ``TColor.DefinedColors(1)`` puts the global color bookkeeping into + the "always store colors" mode (``gLastDefinedColors = -1``). When a canvas + is afterwards serialized to JSON for the web display - which is exactly what + JupyROOT does to show a canvas inline in a notebook - ``TCanvas::Streamer`` + used to unconditionally call ``fPrimitives->Add(...)`` to attach the list of + colors. During the web-canvas snapshot the pad primitives are temporarily + detached (``fPrimitives`` is set to ``nullptr``, see + ``TWebCanvas::CreatePadSnapshot``), so this dereferenced a null pointer and + crashed with a segmentation violation. + """ + + def test_definedcolors_web_canvas_json(self): + # TWebCanvas (and thus CreateCanvasJSON) is only available when ROOT is + # built with the web display (root7/webgui). Without it, JupyROOT uses a + # different code path that is not affected by this issue. + if not hasattr(ROOT, "TWebCanvas") or not hasattr(ROOT.TWebCanvas, "CreateCanvasJSON"): + self.skipTest("ROOT was built without the web display (webgui)") + + ROOT.gROOT.SetBatch(True) + + # This is what triggers the bug: force the "always store colors" mode. + ROOT.TColor.DefinedColors(1) + + c = ROOT.TCanvas("c_regression_20018", "Basic ROOT Plot", 800, 600) + h = ROOT.TH1F("h_regression_20018", "Example Histogram;X axis;Entries", 100, 0, 10) + h.FillRandom("gaus", 1000) + h.Draw() + c.Update() + + # Used to segfault in TCanvas::Streamer due to a null fPrimitives. + json = ROOT.TWebCanvas.CreateCanvasJSON(c, 23, True) + self.assertGreater(len(json.Data()), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/graf2d/gpad/src/TCanvas.cxx b/graf2d/gpad/src/TCanvas.cxx index 916c58397b70e..cd9232ed4fe79 100644 --- a/graf2d/gpad/src/TCanvas.cxx +++ b/graf2d/gpad/src/TCanvas.cxx @@ -2343,7 +2343,13 @@ void TCanvas::Streamer(TBuffer &b) //in the buffer, do not add the list of colors to the list of primitives. TObjArray *colors = nullptr; TObjArray *CurrentColorPalette = nullptr; - if (TColor::DefinedColors()) { + // fPrimitives can be temporarily null while streaming a web-canvas + // snapshot (see TWebCanvas::CreatePadSnapshot). In that case colors and + // palette are delivered separately, so the list of colors must not be + // added here. The guard also avoids a null dereference that crashes when + // colors storage has been forced on via TColor::DefinedColors(1) + // (see https://github.com/root-project/root/issues/20018). + if (fPrimitives && TColor::DefinedColors()) { if (!b.CheckObject(gROOT->GetListOfColors(),TObjArray::Class())) { colors = (TObjArray*)gROOT->GetListOfColors(); fPrimitives->Add(colors); diff --git a/roottest/python/JupyROOT/CMakeLists.txt b/roottest/python/JupyROOT/CMakeLists.txt index 828055119d733..e984810c82d39 100644 --- a/roottest/python/JupyROOT/CMakeLists.txt +++ b/roottest/python/JupyROOT/CMakeLists.txt @@ -49,6 +49,18 @@ if(imt) PYTHON_DEPS jupyter) endif() +# Regression test for https://github.com/root-project/root/issues/20018 : +# TColor.DefinedColors(1) followed by drawing a canvas inline used to segfault. +# This notebook draws a canvas, so its output (unique div ids, random data) is +# not reproducible - just check that it runs without error ("OFF", no compare). +set(TCOLOR_NB tcolor_definedcolors.ipynb) +get_filename_component(NOTEBOOKBASE ${TCOLOR_NB} NAME_WE) +ROOTTEST_ADD_TEST(${NOTEBOOKBASE}_notebook + COPY_TO_BUILDDIR ${TCOLOR_NB} + COMMAND ${Python3_EXECUTABLE} ${NBDIFFUTIL} ${TCOLOR_NB} "OFF" + RUN_SERIAL + PYTHON_DEPS jupyter) + endif() endif() diff --git a/roottest/python/JupyROOT/tcolor_definedcolors.ipynb b/roottest/python/JupyROOT/tcolor_definedcolors.ipynb new file mode 100644 index 0000000000000..8363e165d6e87 --- /dev/null +++ b/roottest/python/JupyROOT/tcolor_definedcolors.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "b8ef0e09", + "metadata": {}, + "outputs": [], + "source": [ + "import ROOT" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac871508", + "metadata": {}, + "outputs": [], + "source": [ + "ROOT.TColor.DefinedColors(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4800d2c7", + "metadata": {}, + "outputs": [], + "source": [ + "c = ROOT.TCanvas(\"c\", \"Basic ROOT Plot\", 800, 600)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3900d8a", + "metadata": {}, + "outputs": [], + "source": [ + "h = ROOT.TH1F(\"h\", \"Example Histogram;X axis;Entries\", 100, 0, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d83cc9da", + "metadata": {}, + "outputs": [], + "source": [ + "for _ in range(10000):\n", + " h.Fill(ROOT.gRandom.Gaus(5, 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1df7d032", + "metadata": {}, + "outputs": [], + "source": [ + "h.Draw()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3743fddc", + "metadata": {}, + "outputs": [], + "source": [ + "c.Draw()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file