PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/src/robot/libraries/Screenshot.py

https://gitlab.com/freedmpure/robotframework
Python | 328 lines | 196 code | 34 blank | 98 comment | 43 complexity | 5c6851aa03b8b49108c5dd2f77a6d698 MD5 | raw file
  1. # Copyright 2008-2015 Nokia Solutions and Networks
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import sys
  15. import os
  16. if sys.platform.startswith('java'):
  17. from java.awt import Toolkit, Robot, Rectangle
  18. from javax.imageio import ImageIO
  19. from java.io import File
  20. elif sys.platform == 'cli':
  21. import clr
  22. clr.AddReference('System.Windows.Forms')
  23. clr.AddReference('System.Drawing')
  24. from System.Drawing import Bitmap, Graphics, Imaging
  25. from System.Windows.Forms import Screen
  26. else:
  27. try:
  28. import wx
  29. except ImportError:
  30. wx = None
  31. try:
  32. from gtk import gdk
  33. except ImportError:
  34. gdk = None
  35. try:
  36. from PIL import ImageGrab # apparently available only on Windows
  37. except ImportError:
  38. ImageGrab = None
  39. from robot import utils
  40. from robot.api import logger
  41. from robot.libraries.BuiltIn import BuiltIn
  42. from robot.version import get_version
  43. class Screenshot(object):
  44. """Test library for taking screenshots on the machine where tests are run.
  45. Notice that successfully taking screenshots requires tests to be run with
  46. a physical or virtual display.
  47. = Using with Python =
  48. With Python you need to have one of the following modules installed to be
  49. able to use this library. The first module that is found will be used.
  50. - wxPython :: http://wxpython.org :: Required also by RIDE so many Robot
  51. Framework users already have this module installed.
  52. - PyGTK :: http://pygtk.org :: This module is available by default on most
  53. Linux distributions.
  54. - Python Imaging Library (PIL) :: http://www.pythonware.com/products/pil ::
  55. This module can take screenshots only on Windows.
  56. = Using with Jython and IronPython =
  57. With Jython and IronPython this library uses APIs provided by JVM and .NET
  58. platforms, respectively. These APIs are always available and thus no
  59. external modules are needed.
  60. IronPython support was added in Robot Framework 2.7.5.
  61. = Where screenshots are saved =
  62. By default screenshots are saved into the same directory where the Robot
  63. Framework log file is written. If no log is created, screenshots are saved
  64. into the directory where the XML output file is written.
  65. It is possible to specify a custom location for screenshots using
  66. ``screenshot_directory`` argument when `importing` the library and
  67. using `Set Screenshot Directory` keyword during execution. It is also
  68. possible to save screenshots using an absolute path.
  69. """
  70. ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
  71. ROBOT_LIBRARY_VERSION = get_version()
  72. def __init__(self, screenshot_directory=None):
  73. """Configure where screenshots are saved.
  74. If ``screenshot_directory`` is not given, screenshots are saved into
  75. same directory as the log file. The directory can also be set using
  76. `Set Screenshot Directory` keyword.
  77. Examples (use only one of these):
  78. | =Setting= | =Value= | =Value= | =Value= |
  79. | Library | Screenshot | | # Default location |
  80. | Library | Screenshot | ${TEMPDIR} | # System temp |
  81. """
  82. self._given_screenshot_dir = self._norm_path(screenshot_directory)
  83. self._screenshot_taker = ScreenshotTaker()
  84. def _norm_path(self, path):
  85. if not path:
  86. return path
  87. return os.path.normpath(path.replace('/', os.sep))
  88. @property
  89. def _screenshot_dir(self):
  90. return self._given_screenshot_dir or self._log_dir
  91. @property
  92. def _log_dir(self):
  93. variables = BuiltIn().get_variables()
  94. outdir = variables['${OUTPUTDIR}']
  95. log = variables['${LOGFILE}']
  96. log = os.path.dirname(log) if log != 'NONE' else '.'
  97. return self._norm_path(os.path.join(outdir, log))
  98. def set_screenshot_directory(self, path):
  99. """Sets the directory where screenshots are saved.
  100. It is possible to use ``/`` as a path separator in all operating
  101. systems. Path to the old directory is returned.
  102. The directory can also be set in `importing`.
  103. """
  104. path = self._norm_path(path)
  105. if not os.path.isdir(path):
  106. raise RuntimeError("Directory '%s' does not exist." % path)
  107. old = self._screenshot_dir
  108. self._given_screenshot_dir = path
  109. return old
  110. def take_screenshot(self, name="screenshot", width="800px"):
  111. """Takes a screenshot in JPEG format and embeds it into the log file.
  112. Name of the file where the screenshot is stored is derived from the
  113. given ``name``. If the ``name`` ends with extension ``.jpg`` or
  114. ``.jpeg``, the screenshot will be stored with that exact name.
  115. Otherwise a unique name is created by adding an underscore, a running
  116. index and an extension to the ``name``.
  117. The name will be interpreted to be relative to the directory where
  118. the log file is written. It is also possible to use absolute paths.
  119. Using ``/`` as a path separator works in all operating systems.
  120. ``width`` specifies the size of the screenshot in the log file.
  121. Examples: (LOGDIR is determined automatically by the library)
  122. | Take Screenshot | | | # LOGDIR/screenshot_1.jpg (index automatically incremented) |
  123. | Take Screenshot | mypic | | # LOGDIR/mypic_1.jpg (index automatically incremented) |
  124. | Take Screenshot | ${TEMPDIR}/mypic | | # /tmp/mypic_1.jpg (index automatically incremented) |
  125. | Take Screenshot | pic.jpg | | # LOGDIR/pic.jpg (always uses this file) |
  126. | Take Screenshot | images/login.jpg | 80% | # Specify both name and width. |
  127. | Take Screenshot | width=550px | | # Specify only width. |
  128. The path where the screenshot is saved is returned.
  129. """
  130. path = self._save_screenshot(name)
  131. self._embed_screenshot(path, width)
  132. return path
  133. def take_screenshot_without_embedding(self, name="screenshot"):
  134. """Takes a screenshot and links it from the log file.
  135. This keyword is otherwise identical to `Take Screenshot` but the saved
  136. screenshot is not embedded into the log file. The screenshot is linked
  137. so it is nevertheless easily available.
  138. """
  139. path = self._save_screenshot(name)
  140. self._link_screenshot(path)
  141. return path
  142. def _save_screenshot(self, basename, directory=None):
  143. path = self._get_screenshot_path(basename, directory)
  144. return self._screenshot_to_file(path)
  145. def _screenshot_to_file(self, path):
  146. path = self._validate_screenshot_path(path)
  147. logger.debug('Using %s modules for taking screenshot.'
  148. % self._screenshot_taker.module)
  149. try:
  150. self._screenshot_taker(path)
  151. except:
  152. logger.warn('Taking screenshot failed: %s\n'
  153. 'Make sure tests are run with a physical or virtual display.'
  154. % utils.get_error_message())
  155. return path
  156. def _validate_screenshot_path(self, path):
  157. path = utils.abspath(self._norm_path(path))
  158. if not os.path.exists(os.path.dirname(path)):
  159. raise RuntimeError("Directory '%s' where to save the screenshot "
  160. "does not exist" % os.path.dirname(path))
  161. return path
  162. def _get_screenshot_path(self, basename, directory):
  163. directory = self._norm_path(directory) if directory else self._screenshot_dir
  164. if basename.lower().endswith(('.jpg', '.jpeg')):
  165. return os.path.join(directory, basename)
  166. index = 0
  167. while True:
  168. index += 1
  169. path = os.path.join(directory, "%s_%d.jpg" % (basename, index))
  170. if not os.path.exists(path):
  171. return path
  172. def _embed_screenshot(self, path, width):
  173. link = utils.get_link_path(path, self._log_dir)
  174. logger.info('<a href="%s"><img src="%s" width="%s"></a>'
  175. % (link, link, width), html=True)
  176. def _link_screenshot(self, path):
  177. link = utils.get_link_path(path, self._log_dir)
  178. logger.info("Screenshot saved to '<a href=\"%s\">%s</a>'."
  179. % (link, path), html=True)
  180. class ScreenshotTaker(object):
  181. def __init__(self, module_name=None):
  182. self._screenshot = self._get_screenshot_taker(module_name)
  183. self.module = self._screenshot.__name__.split('_')[1]
  184. self._wx_app_reference = None
  185. def __call__(self, path):
  186. self._screenshot(path)
  187. def __nonzero__(self):
  188. return self.module != 'no'
  189. def test(self, path=None):
  190. print "Using '%s' module." % self.module
  191. if not self:
  192. return False
  193. if not path:
  194. print "Not taking test screenshot."
  195. return True
  196. print "Taking test screenshot to '%s'." % path
  197. try:
  198. self(path)
  199. except:
  200. print "Failed: %s" % utils.get_error_message()
  201. return False
  202. else:
  203. print "Success!"
  204. return True
  205. def _get_screenshot_taker(self, module_name):
  206. if sys.platform.startswith('java'):
  207. return self._java_screenshot
  208. if sys.platform == 'cli':
  209. return self._cli_screenshot
  210. if module_name:
  211. method_name = '_%s_screenshot' % module_name.lower()
  212. if hasattr(self, method_name):
  213. return getattr(self, method_name)
  214. return self._get_default_screenshot_taker()
  215. def _get_default_screenshot_taker(self):
  216. for module, screenshot_taker in [(wx, self._wx_screenshot),
  217. (gdk, self._gtk_screenshot),
  218. (ImageGrab, self._pil_screenshot),
  219. (True, self._no_screenshot)]:
  220. if module:
  221. return screenshot_taker
  222. def _java_screenshot(self, path):
  223. size = Toolkit.getDefaultToolkit().getScreenSize()
  224. rectangle = Rectangle(0, 0, size.width, size.height)
  225. image = Robot().createScreenCapture(rectangle)
  226. ImageIO.write(image, 'jpg', File(path))
  227. def _cli_screenshot(self, path):
  228. bmp = Bitmap(Screen.PrimaryScreen.Bounds.Width,
  229. Screen.PrimaryScreen.Bounds.Height)
  230. graphics = Graphics.FromImage(bmp)
  231. try:
  232. graphics.CopyFromScreen(0, 0, 0, 0, bmp.Size)
  233. finally:
  234. graphics.Dispose()
  235. bmp.Save(path, Imaging.ImageFormat.Jpeg)
  236. def _wx_screenshot(self, path):
  237. if not self._wx_app_reference:
  238. self._wx_app_reference = wx.App(False)
  239. context = wx.ScreenDC()
  240. width, height = context.GetSize()
  241. bitmap = wx.EmptyBitmap(width, height, -1)
  242. memory = wx.MemoryDC()
  243. memory.SelectObject(bitmap)
  244. memory.Blit(0, 0, width, height, context, -1, -1)
  245. memory.SelectObject(wx.NullBitmap)
  246. bitmap.SaveFile(path, wx.BITMAP_TYPE_JPEG)
  247. def _gtk_screenshot(self, path):
  248. window = gdk.get_default_root_window()
  249. if not window:
  250. raise RuntimeError('Taking screenshot failed')
  251. width, height = window.get_size()
  252. pb = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, width, height)
  253. pb = pb.get_from_drawable(window, window.get_colormap(),
  254. 0, 0, 0, 0, width, height)
  255. if not pb:
  256. raise RuntimeError('Taking screenshot failed')
  257. pb.save(path, 'jpeg')
  258. def _pil_screenshot(self, path):
  259. ImageGrab.grab().save(path, 'JPEG')
  260. def _no_screenshot(self, path):
  261. raise RuntimeError('Taking screenshots is not supported on this platform '
  262. 'by default. See library documentation for details.')
  263. if __name__ == "__main__":
  264. if len(sys.argv) not in [2, 3]:
  265. sys.exit("Usage: %s <path> [wx|gtk|pil] OR test [<path>]" % os.path.basename(sys.argv[0]))
  266. if sys.argv[1] == 'test':
  267. sys.exit(0 if ScreenshotTaker().test(*sys.argv[2:]) else 1)
  268. path = utils.abspath(sys.argv[1])
  269. module = sys.argv[2] if len(sys.argv) == 3 else None
  270. shooter = ScreenshotTaker(module)
  271. print 'Using %s modules' % shooter.module
  272. shooter(path)
  273. print path