Salome HOME
Copyright update 2022
[modules/gui.git] / tools / CurvePlot / src / python / test / PlotTestBase.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2007-2022  CEA/DEN, EDF R&D
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19 #
20 # Author : A. Bruneton
21 #
22 import unittest, sys, os, filecmp, shutil, tempfile
23 from pyqtside.QtWidgets import QApplication
24 from pyqtside.QtGui import QPixmap, QPainter
25 from pyqtside.QtCore import QTimer
26
27 from curveplot.PlotController import PlotController
28 from curveplot.XYView import XYView
29 from PyQt5.Qt import QMainWindow
30
31 def runOnly(func):
32   func.__runOnly__ = True
33   return func
34
35 def processDecorator(mod_name):
36   """ Little trick to be able to mark a test with the decorator
37     @runOnly
38       If one or more tests bear this decorator, only them will be run
39   """
40   import inspect
41   someRunOnly = False
42   for name, obj in inspect.getmembers(sys.modules[mod_name]):
43     if name == "PlotTest" and inspect.isclass(obj):
44       for p in dir(obj):
45         if p.startswith("test") and hasattr(obj.__dict__[p], "__runOnly__"):
46           someRunOnly = True
47           break
48   if someRunOnly:
49     for name, obj in inspect.getmembers(sys.modules[mod_name]):
50       if name == "PlotTest" and inspect.isclass(obj):
51         for p in dir(obj):
52           # Note the "not":
53           if p.startswith("test") and not hasattr(obj.__dict__[p], "__runOnly__"):
54             delattr(obj, p)
55
56 class PlotTestBase(unittest.TestCase):
57   """ Unit test suite for the curve plotter. This class deals with the set up and the screenshot generation/
58   comparison. The tests themselves are stored in the derived class PlotTest below.
59    
60   The class variable below can be turned on to regenerate base line files (reference images).
61   All baselines start with the name of the corresponding test, plus a possible suffix.
62   The baselines directory is set relative to the path of this script.
63   """
64   REBUILD_BASELINES = False
65
66   __BASE_LINE_DIR = "baselines"
67   __FORMAT = "png"
68
69   def __init__(self, methodName):
70     unittest.TestCase.__init__(self, methodName)
71
72     if self.REBUILD_BASELINES:
73       self.tmpBaselineDir = os.path.join(tempfile.gettempdir(), "curveplot_baselines")
74       if not os.path.isdir(self.tmpBaselineDir):
75         os.mkdir(self.tmpBaselineDir)
76       print("### Rebuilding base lines. Reference files will be saved to '%s'" % self.tmpBaselineDir)
77
78     PlotController.WITH_CURVE_BROWSER = True
79     XYView._DEFAULT_LEGEND_STATE = True   # always show legend by default
80     self._this_dir = os.path.dirname(os.path.realpath(__file__))
81
82   def setUp(self):
83     import sys
84     from curveplot.SalomePyQt_MockUp import SalomePyQt
85     from curveplot.TableModel import TableModel
86     from curveplot.CurveModel import CurveModel
87     from curveplot.XYPlotSetModel import XYPlotSetModel
88     from curveplot.TestDesktop import TestDesktop
89
90     self.qpixmap = None
91     self.keepDir = False
92     if not self.REBUILD_BASELINES:
93       self.tmpDir = tempfile.mkdtemp(prefix="curveplot_tests")
94     else:
95       self.tmpDir = None
96
97     # Minimal UI setup:
98     self.app = QApplication(sys.argv)
99     desktop = TestDesktop(None)
100     self.sgPyQt = SalomePyQt(desktop)
101     desktop._sgPyQt = self.sgPyQt
102     desktop.initialize()
103     self.plotController = PlotController.GetInstance(self.sgPyQt)
104     desktop.resize(800, 600)
105     desktop.show()
106     self._execQtWasCalled = False # Used to automatically finish Qt execution loop on tests not doing a screenshot
107
108     # Reset some class var to make sure IDs appearing in screenshots do not depend on test seq order:
109     CurveModel.START_ID = -1
110     TableModel.START_ID = -1
111     XYPlotSetModel.START_ID = -1
112
113   def tearDown(self):
114     if not self.REBUILD_BASELINES:
115       # Clean up temp dir where the file comparison has been made:
116       if not self.keepDir:
117         shutil.rmtree(self.tmpDir, False)
118     if not self._execQtWasCalled:
119       self._execQt(withShot=False)
120     PlotController.Destroy()
121
122   def getTestName(self):
123     return self.id().split(".")[-1]
124
125   def saveCurrentPix(self, direct, suffix):
126     fileName = os.path.join(direct, self.getTestName() + suffix + "." + self.__FORMAT)
127     self.qpixmap.save(fileName, self.__FORMAT)
128     return fileName
129
130   def _execQt(self, withShot=False):
131     if withShot:
132       QTimer.singleShot(50, self._shotEvent)  # take picture
133     QTimer.singleShot(200, self.app.quit)    # quit
134     self.app.exec_()  # will hang till quit is fired
135
136     # Important make sure previous app is properly killed before launching next test!
137     # Qt doesn't like multiple running instance
138     import gc
139     gc.collect()
140
141   def areScreenshotEqual(self, widget):
142     """ Finish the launching of the Qt application so that the widgets and the curve have a chance to display
143      and trigger snapshot comparison """
144     self.saveW = widget
145     self._execQtWasCalled = True
146     self._execQt(withShot=True)
147     return self.retValue
148
149   def _shotEvent(self):
150     self.retValue = self._snapAndCompare(self.saveW)
151
152   def _snapAndCompare(self, widget, suffix=""):
153     """ Test equality between a reference file saved in the baseline directory, and whose name is built as
154           "<test_name><suffix>.png"
155         and the file generated on the fly by taking a snapshot of the widget provided in argument.
156         The comparison is made in a temp dir which is kept if the file differ.
157     """
158     import glob
159     # Smiiiile :-)
160     self.qpixmap = QPixmap(widget.size())
161     widget.repaint()
162     widget.render(self.qpixmap)
163     #self.qpixmap = widget.grab()
164
165     # Nothing to compare if rebuilding base lines, just saving file:
166     if self.REBUILD_BASELINES:
167       self.saveCurrentPix(self.tmpBaselineDir, suffix)
168       return True
169
170     gen_path = self.saveCurrentPix(self.tmpDir, suffix)
171     base_ref = os.path.join(self._this_dir, self.__BASE_LINE_DIR, self.getTestName() + suffix)
172     ret = False
173     for ref_path in glob.glob("%s_*.%s" % (base_ref, self.__FORMAT)):
174       try:
175         ret = filecmp.cmp(ref_path, gen_path, shallow=False)
176         if ret:
177           break
178       except OSError:
179         ret = False
180     if not ret:
181       # Keep file if assert is false
182       self.keepDir = True
183       print("[%s] -- Failed screenshot equality, or unable to open baseline file - directory is kept alive: %s" % (self.getTestName(), self.tmpDir))
184     return ret
185
186   def showTabWidget(self):
187     tabW = self.plotController._sgPyQt._tabWidget
188     # No simpler way found so far:
189     tabW.show()
190     return tabW
191
192   def getBrowserWidget(self):
193     return self.plotController._curveBrowserView._treeWidget