From c29e9d3834fd06928f3dbf6718160eb42cbdb5c6 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Wed, 10 Apr 2024 11:57:22 -0700 Subject: [PATCH 1/8] Modifications to the Screenshot/Taker classes to support taking screenshots in formats other than jpg; --- src/robot/libraries/Screenshot.py | 756 ++++++++++++++++-------------- 1 file changed, 400 insertions(+), 356 deletions(-) mode change 100644 => 100755 src/robot/libraries/Screenshot.py diff --git a/src/robot/libraries/Screenshot.py b/src/robot/libraries/Screenshot.py old mode 100644 new mode 100755 index 92e25daa9c7..127ae26d68d --- a/src/robot/libraries/Screenshot.py +++ b/src/robot/libraries/Screenshot.py @@ -1,356 +1,400 @@ -# Copyright 2008-2015 Nokia Networks -# Copyright 2016- Robot Framework Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import subprocess -import sys - -try: - import wx -except ImportError: - wx = None -try: - from gtk import gdk -except ImportError: - gdk = None -try: - from PIL import ImageGrab # apparently available only on Windows -except ImportError: - ImageGrab = None - -from robot.api import logger -from robot.libraries.BuiltIn import BuiltIn -from robot.version import get_version -from robot.utils import abspath, get_error_message, get_link_path - - -class Screenshot: - """Library for taking screenshots on the machine where tests are executed. - - Taking the actual screenshot requires a suitable tool or module that may - need to be installed separately. Taking screenshots also requires tests - to be run with a physical or virtual display. - - == Table of contents == - - %TOC% - - = Supported screenshot taking tools and modules = - - How screenshots are taken depends on the operating system. On OSX - screenshots are taken using the built-in ``screencapture`` utility. On - other operating systems you need to have one of the following tools or - Python modules installed. You can specify the tool/module to use when - `importing` the library. If no tool or module is specified, the first - one found will be used. - - - wxPython :: http://wxpython.org :: Generic Python GUI toolkit. - - PyGTK :: http://pygtk.org :: This module is available by default on most - Linux distributions. - - Pillow :: http://python-pillow.github.io :: - Only works on Windows. Also the original PIL package is supported. - - Scrot :: http://en.wikipedia.org/wiki/Scrot :: Not used on Windows. - Install with ``apt-get install scrot`` or similar. - - = Where screenshots are saved = - - By default screenshots are saved into the same directory where the Robot - Framework log file is written. If no log is created, screenshots are saved - into the directory where the XML output file is written. - - It is possible to specify a custom location for screenshots using - ``screenshot_directory`` argument when `importing` the library and - using `Set Screenshot Directory` keyword during execution. It is also - possible to save screenshots using an absolute path. - - = ScreenCapLibrary = - - [https://github.com/mihaiparvu/ScreenCapLibrary|ScreenCapLibrary] is an - external Robot Framework library that can be used as an alternative, - which additionally provides support for multiple formats, adjusting the - quality, using GIFs and video capturing. - """ - - ROBOT_LIBRARY_SCOPE = 'TEST SUITE' - ROBOT_LIBRARY_VERSION = get_version() - - def __init__(self, screenshot_directory=None, screenshot_module=None): - """Configure where screenshots are saved. - - If ``screenshot_directory`` is not given, screenshots are saved into - same directory as the log file. The directory can also be set using - `Set Screenshot Directory` keyword. - - ``screenshot_module`` specifies the module or tool to use when using - this library outside OSX. Possible values are ``wxPython``, - ``PyGTK``, ``PIL`` and ``scrot``, case-insensitively. If no value is - given, the first module/tool found is used in that order. - - Examples: - | =Setting= | =Value= | =Value= | - | Library | Screenshot | | - | Library | Screenshot | ${TEMPDIR} | - | Library | Screenshot | screenshot_module=PyGTK | - """ - self._given_screenshot_dir = self._norm_path(screenshot_directory) - self._screenshot_taker = ScreenshotTaker(screenshot_module) - - def _norm_path(self, path): - if not path: - return path - elif isinstance(path, os.PathLike): - path = str(path) - else: - path = path.replace('/', os.sep) - return os.path.normpath(path) - - @property - def _screenshot_dir(self): - return self._given_screenshot_dir or self._log_dir - - @property - def _log_dir(self): - variables = BuiltIn().get_variables() - outdir = variables['${OUTPUTDIR}'] - log = variables['${LOGFILE}'] - log = os.path.dirname(log) if log != 'NONE' else '.' - return self._norm_path(os.path.join(outdir, log)) - - def set_screenshot_directory(self, path): - """Sets the directory where screenshots are saved. - - It is possible to use ``/`` as a path separator in all operating - systems. Path to the old directory is returned. - - The directory can also be set in `importing`. - """ - path = self._norm_path(path) - if not os.path.isdir(path): - raise RuntimeError("Directory '%s' does not exist." % path) - old = self._screenshot_dir - self._given_screenshot_dir = path - return old - - def take_screenshot(self, name="screenshot", width="800px"): - """Takes a screenshot in JPEG format and embeds it into the log file. - - Name of the file where the screenshot is stored is derived from the - given ``name``. If the ``name`` ends with extension ``.jpg`` or - ``.jpeg``, the screenshot will be stored with that exact name. - Otherwise a unique name is created by adding an underscore, a running - index and an extension to the ``name``. - - The name will be interpreted to be relative to the directory where - the log file is written. It is also possible to use absolute paths. - Using ``/`` as a path separator works in all operating systems. - - ``width`` specifies the size of the screenshot in the log file. - - Examples: (LOGDIR is determined automatically by the library) - | Take Screenshot | | | # LOGDIR/screenshot_1.jpg (index automatically incremented) | - | Take Screenshot | mypic | | # LOGDIR/mypic_1.jpg (index automatically incremented) | - | Take Screenshot | ${TEMPDIR}/mypic | | # /tmp/mypic_1.jpg (index automatically incremented) | - | Take Screenshot | pic.jpg | | # LOGDIR/pic.jpg (always uses this file) | - | Take Screenshot | images/login.jpg | 80% | # Specify both name and width. | - | Take Screenshot | width=550px | | # Specify only width. | - - The path where the screenshot is saved is returned. - """ - path = self._save_screenshot(name) - self._embed_screenshot(path, width) - return path - - def take_screenshot_without_embedding(self, name="screenshot"): - """Takes a screenshot and links it from the log file. - - This keyword is otherwise identical to `Take Screenshot` but the saved - screenshot is not embedded into the log file. The screenshot is linked - so it is nevertheless easily available. - """ - path = self._save_screenshot(name) - self._link_screenshot(path) - return path - - def _save_screenshot(self, name): - name = str(name) if isinstance(name, os.PathLike) else name.replace('/', os.sep) - path = self._get_screenshot_path(name) - return self._screenshot_to_file(path) - - def _screenshot_to_file(self, path): - path = self._validate_screenshot_path(path) - logger.debug('Using %s module/tool for taking screenshot.' - % self._screenshot_taker.module) - try: - self._screenshot_taker(path) - except: - logger.warn('Taking screenshot failed: %s\n' - 'Make sure tests are run with a physical or virtual ' - 'display.' % get_error_message()) - return path - - def _validate_screenshot_path(self, path): - path = abspath(self._norm_path(path)) - if not os.path.exists(os.path.dirname(path)): - raise RuntimeError("Directory '%s' where to save the screenshot " - "does not exist" % os.path.dirname(path)) - return path - - def _get_screenshot_path(self, basename): - if basename.lower().endswith(('.jpg', '.jpeg')): - return os.path.join(self._screenshot_dir, basename) - index = 0 - while True: - index += 1 - path = os.path.join(self._screenshot_dir, "%s_%d.jpg" % (basename, index)) - if not os.path.exists(path): - return path - - def _embed_screenshot(self, path, width): - link = get_link_path(path, self._log_dir) - logger.info('' - % (link, link, width), html=True) - - def _link_screenshot(self, path): - link = get_link_path(path, self._log_dir) - logger.info("Screenshot saved to '%s'." - % (link, path), html=True) - - -class ScreenshotTaker: - - def __init__(self, module_name=None): - self._screenshot = self._get_screenshot_taker(module_name) - self.module = self._screenshot.__name__.split('_')[1] - self._wx_app_reference = None - - def __call__(self, path): - self._screenshot(path) - - def __bool__(self): - return self.module != 'no' - - def test(self, path=None): - if not self: - print("Cannot take screenshots.") - return False - print("Using '%s' to take screenshot." % self.module) - if not path: - print("Not taking test screenshot.") - return True - print("Taking test screenshot to '%s'." % path) - try: - self(path) - except: - print("Failed: %s" % get_error_message()) - return False - else: - print("Success!") - return True - - def _get_screenshot_taker(self, module_name=None): - if sys.platform == 'darwin': - return self._osx_screenshot - if module_name: - return self._get_named_screenshot_taker(module_name.lower()) - return self._get_default_screenshot_taker() - - def _get_named_screenshot_taker(self, name): - screenshot_takers = {'wxpython': (wx, self._wx_screenshot), - 'pygtk': (gdk, self._gtk_screenshot), - 'pil': (ImageGrab, self._pil_screenshot), - 'scrot': (self._scrot, self._scrot_screenshot)} - if name not in screenshot_takers: - raise RuntimeError("Invalid screenshot module or tool '%s'." % name) - supported, screenshot_taker = screenshot_takers[name] - if not supported: - raise RuntimeError("Screenshot module or tool '%s' not installed." - % name) - return screenshot_taker - - def _get_default_screenshot_taker(self): - for module, screenshot_taker in [(wx, self._wx_screenshot), - (gdk, self._gtk_screenshot), - (ImageGrab, self._pil_screenshot), - (self._scrot, self._scrot_screenshot), - (True, self._no_screenshot)]: - if module: - return screenshot_taker - - def _osx_screenshot(self, path): - if self._call('screencapture', '-t', 'jpg', path) != 0: - raise RuntimeError("Using 'screencapture' failed.") - - def _call(self, *command): - try: - return subprocess.call(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - except OSError: - return -1 - - @property - def _scrot(self): - return os.sep == '/' and self._call('scrot', '--version') == 0 - - def _scrot_screenshot(self, path): - if not path.endswith(('.jpg', '.jpeg')): - raise RuntimeError("Scrot requires extension to be '.jpg' or " - "'.jpeg', got '%s'." % os.path.splitext(path)[1]) - if os.path.exists(path): - os.remove(path) - if self._call('scrot', '--silent', path) != 0: - raise RuntimeError("Using 'scrot' failed.") - - def _wx_screenshot(self, path): - if not self._wx_app_reference: - self._wx_app_reference = wx.App(False) - context = wx.ScreenDC() - width, height = context.GetSize() - if wx.__version__ >= '4': - bitmap = wx.Bitmap(width, height, -1) - else: - bitmap = wx.EmptyBitmap(width, height, -1) - memory = wx.MemoryDC() - memory.SelectObject(bitmap) - memory.Blit(0, 0, width, height, context, -1, -1) - memory.SelectObject(wx.NullBitmap) - bitmap.SaveFile(path, wx.BITMAP_TYPE_JPEG) - - def _gtk_screenshot(self, path): - window = gdk.get_default_root_window() - if not window: - raise RuntimeError('Taking screenshot failed.') - width, height = window.get_size() - pb = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, width, height) - pb = pb.get_from_drawable(window, window.get_colormap(), - 0, 0, 0, 0, width, height) - if not pb: - raise RuntimeError('Taking screenshot failed.') - pb.save(path, 'jpeg') - - def _pil_screenshot(self, path): - ImageGrab.grab().save(path, 'JPEG') - - def _no_screenshot(self, path): - raise RuntimeError('Taking screenshots is not supported on this platform ' - 'by default. See library documentation for details.') - - -if __name__ == "__main__": - if len(sys.argv) not in [2, 3]: - sys.exit("Usage: %s |test [wxpython|pygtk|pil|scrot]" - % os.path.basename(sys.argv[0])) - path = sys.argv[1] if sys.argv[1] != 'test' else None - module = sys.argv[2] if len(sys.argv) > 2 else None - ScreenshotTaker(module).test(path) +# Copyright 2008-2015 Nokia Networks +# Copyright 2016- Robot Framework Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +import sys + +try: + import wx +except ImportError: + wx = None +try: + from gtk import gdk +except ImportError: + gdk = None +try: + from PIL import ImageGrab # apparently available only on Windows +except ImportError: + ImageGrab = None + +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn +from robot.version import get_version +from robot.utils import abspath, get_error_message, get_link_path + + +class Screenshot: + """Library for taking screenshots on the machine where tests are executed. + + Taking the actual screenshot requires a suitable tool or module that may + need to be installed separately. Taking screenshots also requires tests + to be run with a physical or virtual display. + + == Table of contents == + + %TOC% + + = Supported screenshot taking tools and modules = + + How screenshots are taken depends on the operating system. On OSX + screenshots are taken using the built-in ``screencapture`` utility. On + other operating systems you need to have one of the following tools or + Python modules installed. You can specify the tool/module to use when + `importing` the library. If no tool or module is specified, the first + one found will be used. + + - wxPython :: http://wxpython.org :: Generic Python GUI toolkit. + - supports: png, jpeg, gif, pcx, pnm, tiff, tga, ico, cur + - PyGTK :: http://pygtk.org :: This module is available by default on most + Linux distributions. + - supports: + - Pillow :: http://python-pillow.github.io :: + Only works on Windows. Also the original PIL package is supported. + - Scrot :: http://en.wikipedia.org/wiki/Scrot :: Not used on Windows. + Install with ``apt-get install scrot`` or similar. + + = Where screenshots are saved = + + By default screenshots are saved into the same directory where the Robot + Framework log file is written. If no log is created, screenshots are saved + into the directory where the XML output file is written. + + It is possible to specify a custom location for screenshots using + ``screenshot_directory`` argument when `importing` the library and + using `Set Screenshot Directory` keyword during execution. It is also + possible to save screenshots using an absolute path. + + = ScreenCapLibrary = + + [https://github.com/mihaiparvu/ScreenCapLibrary|ScreenCapLibrary] is an + external Robot Framework library that can be used as an alternative, + which additionally provides support for multiple formats, adjusting the + quality, using GIFs and video capturing. + """ + + ROBOT_LIBRARY_SCOPE = 'TEST SUITE' + ROBOT_LIBRARY_VERSION = get_version() + IMAGE_FORMATS = { + "jpg": {"extensions": ("jpg", "jpeg",), + "wx_bmp_type": "BITMAP_TYPE_JPEG", + "gtk_type": "jpeg", + "pil_type": "JPEG"}, + "png": {"extensions": ("png",), + "wx_bmp_type": "BITMAP_TYPE_PNG", + "gtk_type": "png", + "pil_type": "PNG"}, + "tiff": {"extensions": ("tiff", "tif",), + "wx_bmp_type": "BITMAP_TYPE_TIFF", + "gtk_type": None, + "pil_type": "TIFF"}, + "bmp": {"extensions": ("bmp",), + "wx_bmp_type": "BITMAP_TYPE_BMP", + "gtk_type": "bmp", + "pil_type": "BMP"} + } + + def __init__(self, screenshot_directory=None, screenshot_module=None, image_format="jpg"): + """Configure where screenshots are saved. + + If ``screenshot_directory`` is not given, screenshots are saved into + same directory as the log file. The directory can also be set using + `Set Screenshot Directory` keyword. + + ``screenshot_module`` specifies the module or tool to use when using + this library outside OSX. Possible values are ``wxPython``, + ``PyGTK``, ``PIL`` and ``scrot``, case-insensitively. If no value is + given, the first module/tool found is used in that order. + + Examples: + | =Setting= | =Value= | =Value= | + | Library | Screenshot | | + | Library | Screenshot | ${TEMPDIR} | + | Library | Screenshot | screenshot_module=PyGTK | + """ + # take in the image format here, so it can be referenced from here on out + self._out_img_format = image_format + self._given_screenshot_dir = self._norm_path(screenshot_directory) + # make sure all screenshot modules support the type we want - they do + self._screenshot_taker = ScreenshotTaker(screenshot_module, self._out_img_format) + + def _norm_path(self, path): + if not path: + return path + elif isinstance(path, os.PathLike): + path = str(path) + else: + path = path.replace('/', os.sep) + return os.path.normpath(path) + + @property + def _screenshot_dir(self): + return self._given_screenshot_dir or self._log_dir + + @property + def _log_dir(self): + variables = BuiltIn().get_variables() + outdir = variables['${OUTPUTDIR}'] + log = variables['${LOGFILE}'] + log = os.path.dirname(log) if log != 'NONE' else '.' + return self._norm_path(os.path.join(outdir, log)) + + def set_screenshot_directory(self, path): + """Sets the directory where screenshots are saved. + + It is possible to use ``/`` as a path separator in all operating + systems. Path to the old directory is returned. + + The directory can also be set in `importing`. + """ + print(f"path passed in is: {path}") + path = self._norm_path(path) + if not os.path.isdir(path): + raise RuntimeError("Directory '%s' does not exist." % path) + old = self._screenshot_dir + self._given_screenshot_dir = path + return old + + def take_screenshot(self, name="screenshot", width="800px", img_format="jpg"): + # TODO: update the docs below once this actually works + """Takes a screenshot in JPEG format and embeds it into the log file. + + Name of the file where the screenshot is stored is derived from the + given ``name``. If the ``name`` ends with extension ``.jpg`` or + ``.jpeg``, the screenshot will be stored with that exact name. + Otherwise a unique name is created by adding an underscore, a running + index and an extension to the ``name``. + + The name will be interpreted to be relative to the directory where + the log file is written. It is also possible to use absolute paths. + Using ``/`` as a path separator works in all operating systems. + + ``width`` specifies the size of the screenshot in the log file. + + ``img_format`` specifies the image format for the screenshot, default is "jpg" + + Examples: (LOGDIR is determined automatically by the library) + | Take Screenshot | | | # LOGDIR/screenshot_1.jpg (index automatically incremented) | + | Take Screenshot | mypic | | # LOGDIR/mypic_1.jpg (index automatically incremented) | + | Take Screenshot | ${TEMPDIR}/mypic | | # /tmp/mypic_1.jpg (index automatically incremented) | + | Take Screenshot | pic.jpg | | # LOGDIR/pic.jpg (always uses this file) | + | Take Screenshot | images/login.jpg | 80% | # Specify both name and width. | + | Take Screenshot | width=550px | | # Specify only width. | + | Take Screenshot | img_format=png | | # Specify only image format, png in this case | + | Take Screenshot | png_image | img_format=tiff | # Specify name and image format, "tiff" in this case | + + The path where the screenshot is saved is returned. + """ + self._out_img_format = img_format + path = self._save_screenshot(name) + self._embed_screenshot(path, width) + return path + + def take_screenshot_without_embedding(self, name="screenshot", img_format="jpg"): + """Takes a screenshot and links it from the log file. + + This keyword is otherwise identical to `Take Screenshot` but the saved + screenshot is not embedded into the log file. The screenshot is linked + so it is nevertheless easily available. + """ + self._out_img_format = img_format + path = self._save_screenshot(name) + self._link_screenshot(path) + return path + + def _save_screenshot(self, name): + name = str(name) if isinstance(name, os.PathLike) else name.replace('/', os.sep) + path = self._get_screenshot_path(name) + return self._screenshot_to_file(path) + + def _screenshot_to_file(self, path): + path = self._validate_screenshot_path(path) + logger.debug('Using %s module/tool for taking screenshot.' + % self._screenshot_taker.module) + try: + self._screenshot_taker(path) + except: + logger.warn('Taking screenshot failed: %s\n' + 'Make sure tests are run with a physical or virtual ' + 'display.' % get_error_message()) + return path + + def _validate_screenshot_path(self, path): + path = abspath(self._norm_path(path)) + if not os.path.exists(os.path.dirname(path)): + raise RuntimeError("Directory '%s' where to save the screenshot " + "does not exist" % os.path.dirname(path)) + return path + + def _get_screenshot_path(self, basename): + try: + # retrieve a tuple of the common file extension variations + file_extensions = Screenshot.IMAGE_FORMATS[self._out_img_format]["extensions"] + except KeyError as ke: + logger.error(f"KeyError in _get_screenshot_path(): {ke}") + # if basename.lower().endswith(file_extensions): + if any([basename.lower().endswith(x) for x in file_extensions]): + return os.path.join(self._screenshot_dir, basename) + index = 0 + while True: + index += 1 + # use the image format key for the file extension + path = os.path.join(self._screenshot_dir, "%s_%d.%s" % (basename, index, self._out_img_format)) + if not os.path.exists(path): + return path + + def _embed_screenshot(self, path, width): + link = get_link_path(path, self._log_dir) + logger.info('' + % (link, link, width), html=True) + + def _link_screenshot(self, path): + link = get_link_path(path, self._log_dir) + logger.info("Screenshot saved to '%s'." + % (link, path), html=True) + + +class ScreenshotTaker: + + def __init__(self, module_name=None, image_format='jpg'): + self._image_format = image_format + self._screenshot = self._get_screenshot_taker(module_name) + self.module = self._screenshot.__name__.split('_')[1] + self._wx_app_reference = None + + def __call__(self, path): + self._screenshot(path) + + def __bool__(self): + return self.module != 'no' + + def test(self, path=None): + if not self: + print("Cannot take screenshots.") + return False + print("Using '%s' to take screenshot." % self.module) + if not path: + print("Not taking test screenshot.") + return True + print("Taking test screenshot to '%s'." % path) + try: + self(path) + except: + print("Failed: %s" % get_error_message()) + return False + else: + print("Success!") + return True + + def _get_screenshot_taker(self, module_name=None): + if sys.platform == 'darwin': + return self._osx_screenshot + if module_name: + return self._get_named_screenshot_taker(module_name.lower()) + return self._get_default_screenshot_taker() + + def _get_named_screenshot_taker(self, name): + screenshot_takers = {'wxpython': (wx, self._wx_screenshot), + 'pygtk': (gdk, self._gtk_screenshot), + 'pil': (ImageGrab, self._pil_screenshot), + 'scrot': (self._scrot, self._scrot_screenshot)} + if name not in screenshot_takers: + raise RuntimeError("Invalid screenshot module or tool '%s'." % name) + supported, screenshot_taker = screenshot_takers[name] + if not supported: + raise RuntimeError("Screenshot module or tool '%s' not installed." + % name) + return screenshot_taker + + def _get_default_screenshot_taker(self): + for module, screenshot_taker in [(wx, self._wx_screenshot), + (gdk, self._gtk_screenshot), + (ImageGrab, self._pil_screenshot), + (self._scrot, self._scrot_screenshot), + (True, self._no_screenshot)]: + if module: + return screenshot_taker + + def _osx_screenshot(self, path): + if self._call('screencapture', '-t', self._image_format, path) != 0: + raise RuntimeError("Using 'screencapture' failed.") + + def _call(self, *command): + try: + return subprocess.call(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + except OSError: + return -1 + + @property + def _scrot(self): + return os.sep == '/' and self._call('scrot', '--version') == 0 + + def _scrot_screenshot(self, path): + try: + file_extensions = Screenshot.IMAGE_FORMATS[self._image_format]["extensions"] + except KeyError as ke: + logger.error(f"KeyError in _scrot_screenshot(): {ke}") + # if not path.endswith(file_extensions): + if any([path.endswith(x) for x in file_extensions]): + raise RuntimeError("Scrot requires extension to be like %s, " + "but got '%s'." % (str(file_extensions), os.path.splitext(path)[1])) + if os.path.exists(path): + os.remove(path) + if self._call('scrot', '--silent', path) != 0: + raise RuntimeError("Using 'scrot' failed.") + + def _wx_screenshot(self, path): + if not self._wx_app_reference: + self._wx_app_reference = wx.App(False) + context = wx.ScreenDC() + width, height = context.GetSize() + if wx.__version__ >= '4': + bitmap = wx.Bitmap(width, height, -1) + else: + bitmap = wx.EmptyBitmap(width, height, -1) + memory = wx.MemoryDC() + memory.SelectObject(bitmap) + memory.Blit(0, 0, width, height, context, -1, -1) + memory.SelectObject(wx.NullBitmap) + bitmap.SaveFile(path, Screenshot.IMAGE_FORMATS[self._image_format]["wx_bmp_type"]) + + def _gtk_screenshot(self, path): + window = gdk.get_default_root_window() + if not window: + raise RuntimeError('Taking screenshot failed.') + width, height = window.get_size() + pb = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, width, height) + pb = pb.get_from_drawable(window, window.get_colormap(), + 0, 0, 0, 0, width, height) + if not pb: + raise RuntimeError('Taking screenshot failed.') + pb.save(path, Screenshot.IMAGE_FORMATS[self._image_format]["gtk_type"]) + + def _pil_screenshot(self, path): + ImageGrab.grab().save(path, Screenshot.IMAGE_FORMATS[self._image_format]["pil_type"]) + + def _no_screenshot(self, path): + raise RuntimeError('Taking screenshots is not supported on this platform ' + 'by default. See library documentation for details.') + + +if __name__ == "__main__": + if len(sys.argv) not in [2, 3]: + sys.exit("Usage: %s |test [wxpython|pygtk|pil|scrot]" + % os.path.basename(sys.argv[0])) + path = sys.argv[1] if sys.argv[1] != 'test' else None + module = sys.argv[2] if len(sys.argv) > 2 else None + ScreenshotTaker(module).test(path) From 269de3abd3a9b748c56f73dfa9096fa790151b57 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Wed, 10 Apr 2024 11:58:15 -0700 Subject: [PATCH 2/8] Removed an errant TODO; --- src/robot/libraries/Screenshot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/robot/libraries/Screenshot.py b/src/robot/libraries/Screenshot.py index 127ae26d68d..4c629e4d015 100755 --- a/src/robot/libraries/Screenshot.py +++ b/src/robot/libraries/Screenshot.py @@ -168,7 +168,6 @@ def set_screenshot_directory(self, path): return old def take_screenshot(self, name="screenshot", width="800px", img_format="jpg"): - # TODO: update the docs below once this actually works """Takes a screenshot in JPEG format and embeds it into the log file. Name of the file where the screenshot is stored is derived from the From 2f2073b884bf22fcff976fb20f3b115849eb19ad Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Wed, 10 Apr 2024 11:59:18 -0700 Subject: [PATCH 3/8] Updated test cases around taking screenshots to prove out the support for non-jpg image formats; --- .../screenshot/take_screenshot.robot | 8 ++++ .../screenshot/screenshot_resource.robot | 5 +- .../screenshot/take_screenshot.robot | 48 ++++++++++++++++--- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/atest/robot/standard_libraries/screenshot/take_screenshot.robot b/atest/robot/standard_libraries/screenshot/take_screenshot.robot index d1a97b97649..9c42797be6f 100644 --- a/atest/robot/standard_libraries/screenshot/take_screenshot.robot +++ b/atest/robot/standard_libraries/screenshot/take_screenshot.robot @@ -15,6 +15,14 @@ Basename May Be Defined ${tc}= Check Test Case ${TESTNAME} Check Embedding In Log ${tc.kws[0].kws[0].msgs[1]} foo_1.jpg +Basename May Be Defined With Screenshot Format Of PNG + ${tc} = Check Test Case ${TESTNAME} + Check Embedding In Log ${tc.kws[0].kws[0].msgs[1]} foo_1.png + +Basename May Be Defined With Screenshot Format Of TIFF + ${tc} = Check Test Case ${TESTNAME} + Check Embedding In Log ${tc.kws[0].kws[0].msgs[1]} foo_1.tiff + Basename With Extension Turns Off Index Generation ${tc}= Check Test Case ${TESTNAME} Check Embedding In Log ${tc.kws[0].kws[0].msgs[1]} xxx.jpg diff --git a/atest/testdata/standard_libraries/screenshot/screenshot_resource.robot b/atest/testdata/standard_libraries/screenshot/screenshot_resource.robot index e1f3f1761e9..9a22a6f554b 100644 --- a/atest/testdata/standard_libraries/screenshot/screenshot_resource.robot +++ b/atest/testdata/standard_libraries/screenshot/screenshot_resource.robot @@ -17,6 +17,7 @@ Save Start Time Set Test Variable \${START TIME} Screenshots Should Exist - [Arguments] ${directory} @{files} - @{actual files}= List Directory ${directory} *.jp*g + [Arguments] ${directory} @{files} ${format}=jpg + ${file_ext_re} = Set Variable If "${format.lower()}" == "jpg" *.jp*g *.${format} + @{actual files}= List Directory ${directory} ${file_ext_re} Lists Should Be Equal ${actual files} ${files} diff --git a/atest/testdata/standard_libraries/screenshot/take_screenshot.robot b/atest/testdata/standard_libraries/screenshot/take_screenshot.robot index 470acbf5ff2..37e723a1415 100644 --- a/atest/testdata/standard_libraries/screenshot/take_screenshot.robot +++ b/atest/testdata/standard_libraries/screenshot/take_screenshot.robot @@ -1,7 +1,7 @@ *** Settings *** -Suite Setup Remove Files ${OUTPUTDIR}/*.jp*g +Suite Setup Remove Files ${OUTPUTDIR}/*.jp*g ${OUTPUTDIR}/*.png ${OUTPUTDIR}/*.tiff ${OUTPUTDIR}/*.bmp Test Setup Save Start Time -Test Teardown Remove Files ${OUTPUTDIR}/*.jp*g +Test Teardown Remove Files ${OUTPUTDIR}/*.jp*g ${OUTPUTDIR}/*.png ${OUTPUTDIR}/*.tiff ${OUTPUTDIR}/*.bmp Resource screenshot_resource.robot *** Variables *** @@ -21,26 +21,60 @@ Basename May Be Defined Repeat Keyword 2 Take Screenshot foo Screenshots Should Exist ${OUTPUTDIR} foo_1.jpg foo_2.jpg +Basename May Be Defined With Screenshot Format Of PNG + Repeat Keyword 2 Take Screenshot foo img_format=png + Screenshots Should Exist ${OUTPUTDIR} foo_1.png foo_2.png format=png + +Basename May Be Defined With Screenshot Format Of TIFF + Repeat Keyword 2 Take Screenshot foo img_format=tiff + Screenshots Should Exist ${OUTPUTDIR} foo_1.tiff foo_2.tiff format=tiff + Basename With Extension Turns Off Index Generation - Repeat Keyword 3 Take Screenshot xxx.jpg - Repeat Keyword 2 Take Screenshot yyy.jpeg - Screenshots Should Exist ${OUTPUTDIR} xxx.jpg yyy.jpeg + Repeat Keyword 3 Take Screenshot xxx.jpg img_format=jpg + Repeat Keyword 2 Take Screenshot yyy.jpeg img_format=jpg + Screenshots Should Exist ${OUTPUTDIR} xxx.jpg yyy.jpeg format=jpg + Repeat Keyword 3 Take Screenshot xxx.png img_format=png + Screenshots Should Exist ${OUTPUTDIR} xxx.png format=png + Repeat Keyword 3 Take Screenshot xxx.bmp img_format=bmp + Screenshots Should Exist ${OUTPUTDIR} xxx.bmp format=bmp Name as `pathlib.Path` Take Screenshot ${{pathlib.Path('name.jpg')}} Screenshots Should Exist ${OUTPUTDIR} name.jpg +Name as `pathlib.Path` - Format PNG + Take Screenshot ${{pathlib.Path('name.png')}} img_format=png + Screenshots Should Exist ${OUTPUTDIR} name.png format=png + +Name as `pathlib.Path` - Format TIFF + Take Screenshot ${{pathlib.Path('name.tiff')}} img_format=tiff + Screenshots Should Exist ${OUTPUTDIR} name.tiff format=tiff + Screenshot Width Can Be Given Take Screenshot width=300px Screenshots Should Exist ${OUTPUTDIR} ${FIRST_SCREENSHOT} +Screenshot Width Can Be Given For PNG + Take Screenshot foo.png width=300px img_format=png + Screenshots Should Exist ${OUTPUTDIR} foo.png format=png + +Screenshot Width Can Be Given For BMP + Take Screenshot foo.bmp width=300px img_format=bmp + Screenshots Should Exist ${OUTPUTDIR} foo.bmp format=bmp + Basename With Non-existing Directories Fails - [Documentation] FAIL Directory '${OUTPUTDIR}${/}non-existing' where to save the screenshot does not exist - Take Screenshot ${OUTPUTDIR}${/}non-existing${/}foo + [Documentation] FAIL Directory '${/}non-existing' where to save the screenshot does not exist + Take Screenshot ${/}non-existing${/}foo Without Embedding Take Screenshot Without Embedding no_embed.jpeg +Take Screenshot Without Embedding For PNG + Take Screenshot Without Embedding no_embed.png img_format=png + +Take Screenshot Without Embedding for TIFF + Take Screenshot Without Embedding no_embed.tiff img_format=tiff + *** Keywords *** Take Screenshot And Verify [Arguments] @{expected files} From bd1cca4b86e24ce68a5d0f3633116e331f0a8610 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Thu, 11 Apr 2024 09:59:49 -0700 Subject: [PATCH 4/8] Fix newlines in the file, no longer crlf, and updated take_screenshot() comments --- src/robot/libraries/Screenshot.py | 798 +++++++++++++++--------------- 1 file changed, 399 insertions(+), 399 deletions(-) diff --git a/src/robot/libraries/Screenshot.py b/src/robot/libraries/Screenshot.py index 4c629e4d015..508316e65b5 100755 --- a/src/robot/libraries/Screenshot.py +++ b/src/robot/libraries/Screenshot.py @@ -1,399 +1,399 @@ -# Copyright 2008-2015 Nokia Networks -# Copyright 2016- Robot Framework Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import subprocess -import sys - -try: - import wx -except ImportError: - wx = None -try: - from gtk import gdk -except ImportError: - gdk = None -try: - from PIL import ImageGrab # apparently available only on Windows -except ImportError: - ImageGrab = None - -from robot.api import logger -from robot.libraries.BuiltIn import BuiltIn -from robot.version import get_version -from robot.utils import abspath, get_error_message, get_link_path - - -class Screenshot: - """Library for taking screenshots on the machine where tests are executed. - - Taking the actual screenshot requires a suitable tool or module that may - need to be installed separately. Taking screenshots also requires tests - to be run with a physical or virtual display. - - == Table of contents == - - %TOC% - - = Supported screenshot taking tools and modules = - - How screenshots are taken depends on the operating system. On OSX - screenshots are taken using the built-in ``screencapture`` utility. On - other operating systems you need to have one of the following tools or - Python modules installed. You can specify the tool/module to use when - `importing` the library. If no tool or module is specified, the first - one found will be used. - - - wxPython :: http://wxpython.org :: Generic Python GUI toolkit. - - supports: png, jpeg, gif, pcx, pnm, tiff, tga, ico, cur - - PyGTK :: http://pygtk.org :: This module is available by default on most - Linux distributions. - - supports: - - Pillow :: http://python-pillow.github.io :: - Only works on Windows. Also the original PIL package is supported. - - Scrot :: http://en.wikipedia.org/wiki/Scrot :: Not used on Windows. - Install with ``apt-get install scrot`` or similar. - - = Where screenshots are saved = - - By default screenshots are saved into the same directory where the Robot - Framework log file is written. If no log is created, screenshots are saved - into the directory where the XML output file is written. - - It is possible to specify a custom location for screenshots using - ``screenshot_directory`` argument when `importing` the library and - using `Set Screenshot Directory` keyword during execution. It is also - possible to save screenshots using an absolute path. - - = ScreenCapLibrary = - - [https://github.com/mihaiparvu/ScreenCapLibrary|ScreenCapLibrary] is an - external Robot Framework library that can be used as an alternative, - which additionally provides support for multiple formats, adjusting the - quality, using GIFs and video capturing. - """ - - ROBOT_LIBRARY_SCOPE = 'TEST SUITE' - ROBOT_LIBRARY_VERSION = get_version() - IMAGE_FORMATS = { - "jpg": {"extensions": ("jpg", "jpeg",), - "wx_bmp_type": "BITMAP_TYPE_JPEG", - "gtk_type": "jpeg", - "pil_type": "JPEG"}, - "png": {"extensions": ("png",), - "wx_bmp_type": "BITMAP_TYPE_PNG", - "gtk_type": "png", - "pil_type": "PNG"}, - "tiff": {"extensions": ("tiff", "tif",), - "wx_bmp_type": "BITMAP_TYPE_TIFF", - "gtk_type": None, - "pil_type": "TIFF"}, - "bmp": {"extensions": ("bmp",), - "wx_bmp_type": "BITMAP_TYPE_BMP", - "gtk_type": "bmp", - "pil_type": "BMP"} - } - - def __init__(self, screenshot_directory=None, screenshot_module=None, image_format="jpg"): - """Configure where screenshots are saved. - - If ``screenshot_directory`` is not given, screenshots are saved into - same directory as the log file. The directory can also be set using - `Set Screenshot Directory` keyword. - - ``screenshot_module`` specifies the module or tool to use when using - this library outside OSX. Possible values are ``wxPython``, - ``PyGTK``, ``PIL`` and ``scrot``, case-insensitively. If no value is - given, the first module/tool found is used in that order. - - Examples: - | =Setting= | =Value= | =Value= | - | Library | Screenshot | | - | Library | Screenshot | ${TEMPDIR} | - | Library | Screenshot | screenshot_module=PyGTK | - """ - # take in the image format here, so it can be referenced from here on out - self._out_img_format = image_format - self._given_screenshot_dir = self._norm_path(screenshot_directory) - # make sure all screenshot modules support the type we want - they do - self._screenshot_taker = ScreenshotTaker(screenshot_module, self._out_img_format) - - def _norm_path(self, path): - if not path: - return path - elif isinstance(path, os.PathLike): - path = str(path) - else: - path = path.replace('/', os.sep) - return os.path.normpath(path) - - @property - def _screenshot_dir(self): - return self._given_screenshot_dir or self._log_dir - - @property - def _log_dir(self): - variables = BuiltIn().get_variables() - outdir = variables['${OUTPUTDIR}'] - log = variables['${LOGFILE}'] - log = os.path.dirname(log) if log != 'NONE' else '.' - return self._norm_path(os.path.join(outdir, log)) - - def set_screenshot_directory(self, path): - """Sets the directory where screenshots are saved. - - It is possible to use ``/`` as a path separator in all operating - systems. Path to the old directory is returned. - - The directory can also be set in `importing`. - """ - print(f"path passed in is: {path}") - path = self._norm_path(path) - if not os.path.isdir(path): - raise RuntimeError("Directory '%s' does not exist." % path) - old = self._screenshot_dir - self._given_screenshot_dir = path - return old - - def take_screenshot(self, name="screenshot", width="800px", img_format="jpg"): - """Takes a screenshot in JPEG format and embeds it into the log file. - - Name of the file where the screenshot is stored is derived from the - given ``name``. If the ``name`` ends with extension ``.jpg`` or - ``.jpeg``, the screenshot will be stored with that exact name. - Otherwise a unique name is created by adding an underscore, a running - index and an extension to the ``name``. - - The name will be interpreted to be relative to the directory where - the log file is written. It is also possible to use absolute paths. - Using ``/`` as a path separator works in all operating systems. - - ``width`` specifies the size of the screenshot in the log file. - - ``img_format`` specifies the image format for the screenshot, default is "jpg" - - Examples: (LOGDIR is determined automatically by the library) - | Take Screenshot | | | # LOGDIR/screenshot_1.jpg (index automatically incremented) | - | Take Screenshot | mypic | | # LOGDIR/mypic_1.jpg (index automatically incremented) | - | Take Screenshot | ${TEMPDIR}/mypic | | # /tmp/mypic_1.jpg (index automatically incremented) | - | Take Screenshot | pic.jpg | | # LOGDIR/pic.jpg (always uses this file) | - | Take Screenshot | images/login.jpg | 80% | # Specify both name and width. | - | Take Screenshot | width=550px | | # Specify only width. | - | Take Screenshot | img_format=png | | # Specify only image format, png in this case | - | Take Screenshot | png_image | img_format=tiff | # Specify name and image format, "tiff" in this case | - - The path where the screenshot is saved is returned. - """ - self._out_img_format = img_format - path = self._save_screenshot(name) - self._embed_screenshot(path, width) - return path - - def take_screenshot_without_embedding(self, name="screenshot", img_format="jpg"): - """Takes a screenshot and links it from the log file. - - This keyword is otherwise identical to `Take Screenshot` but the saved - screenshot is not embedded into the log file. The screenshot is linked - so it is nevertheless easily available. - """ - self._out_img_format = img_format - path = self._save_screenshot(name) - self._link_screenshot(path) - return path - - def _save_screenshot(self, name): - name = str(name) if isinstance(name, os.PathLike) else name.replace('/', os.sep) - path = self._get_screenshot_path(name) - return self._screenshot_to_file(path) - - def _screenshot_to_file(self, path): - path = self._validate_screenshot_path(path) - logger.debug('Using %s module/tool for taking screenshot.' - % self._screenshot_taker.module) - try: - self._screenshot_taker(path) - except: - logger.warn('Taking screenshot failed: %s\n' - 'Make sure tests are run with a physical or virtual ' - 'display.' % get_error_message()) - return path - - def _validate_screenshot_path(self, path): - path = abspath(self._norm_path(path)) - if not os.path.exists(os.path.dirname(path)): - raise RuntimeError("Directory '%s' where to save the screenshot " - "does not exist" % os.path.dirname(path)) - return path - - def _get_screenshot_path(self, basename): - try: - # retrieve a tuple of the common file extension variations - file_extensions = Screenshot.IMAGE_FORMATS[self._out_img_format]["extensions"] - except KeyError as ke: - logger.error(f"KeyError in _get_screenshot_path(): {ke}") - # if basename.lower().endswith(file_extensions): - if any([basename.lower().endswith(x) for x in file_extensions]): - return os.path.join(self._screenshot_dir, basename) - index = 0 - while True: - index += 1 - # use the image format key for the file extension - path = os.path.join(self._screenshot_dir, "%s_%d.%s" % (basename, index, self._out_img_format)) - if not os.path.exists(path): - return path - - def _embed_screenshot(self, path, width): - link = get_link_path(path, self._log_dir) - logger.info('' - % (link, link, width), html=True) - - def _link_screenshot(self, path): - link = get_link_path(path, self._log_dir) - logger.info("Screenshot saved to '%s'." - % (link, path), html=True) - - -class ScreenshotTaker: - - def __init__(self, module_name=None, image_format='jpg'): - self._image_format = image_format - self._screenshot = self._get_screenshot_taker(module_name) - self.module = self._screenshot.__name__.split('_')[1] - self._wx_app_reference = None - - def __call__(self, path): - self._screenshot(path) - - def __bool__(self): - return self.module != 'no' - - def test(self, path=None): - if not self: - print("Cannot take screenshots.") - return False - print("Using '%s' to take screenshot." % self.module) - if not path: - print("Not taking test screenshot.") - return True - print("Taking test screenshot to '%s'." % path) - try: - self(path) - except: - print("Failed: %s" % get_error_message()) - return False - else: - print("Success!") - return True - - def _get_screenshot_taker(self, module_name=None): - if sys.platform == 'darwin': - return self._osx_screenshot - if module_name: - return self._get_named_screenshot_taker(module_name.lower()) - return self._get_default_screenshot_taker() - - def _get_named_screenshot_taker(self, name): - screenshot_takers = {'wxpython': (wx, self._wx_screenshot), - 'pygtk': (gdk, self._gtk_screenshot), - 'pil': (ImageGrab, self._pil_screenshot), - 'scrot': (self._scrot, self._scrot_screenshot)} - if name not in screenshot_takers: - raise RuntimeError("Invalid screenshot module or tool '%s'." % name) - supported, screenshot_taker = screenshot_takers[name] - if not supported: - raise RuntimeError("Screenshot module or tool '%s' not installed." - % name) - return screenshot_taker - - def _get_default_screenshot_taker(self): - for module, screenshot_taker in [(wx, self._wx_screenshot), - (gdk, self._gtk_screenshot), - (ImageGrab, self._pil_screenshot), - (self._scrot, self._scrot_screenshot), - (True, self._no_screenshot)]: - if module: - return screenshot_taker - - def _osx_screenshot(self, path): - if self._call('screencapture', '-t', self._image_format, path) != 0: - raise RuntimeError("Using 'screencapture' failed.") - - def _call(self, *command): - try: - return subprocess.call(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - except OSError: - return -1 - - @property - def _scrot(self): - return os.sep == '/' and self._call('scrot', '--version') == 0 - - def _scrot_screenshot(self, path): - try: - file_extensions = Screenshot.IMAGE_FORMATS[self._image_format]["extensions"] - except KeyError as ke: - logger.error(f"KeyError in _scrot_screenshot(): {ke}") - # if not path.endswith(file_extensions): - if any([path.endswith(x) for x in file_extensions]): - raise RuntimeError("Scrot requires extension to be like %s, " - "but got '%s'." % (str(file_extensions), os.path.splitext(path)[1])) - if os.path.exists(path): - os.remove(path) - if self._call('scrot', '--silent', path) != 0: - raise RuntimeError("Using 'scrot' failed.") - - def _wx_screenshot(self, path): - if not self._wx_app_reference: - self._wx_app_reference = wx.App(False) - context = wx.ScreenDC() - width, height = context.GetSize() - if wx.__version__ >= '4': - bitmap = wx.Bitmap(width, height, -1) - else: - bitmap = wx.EmptyBitmap(width, height, -1) - memory = wx.MemoryDC() - memory.SelectObject(bitmap) - memory.Blit(0, 0, width, height, context, -1, -1) - memory.SelectObject(wx.NullBitmap) - bitmap.SaveFile(path, Screenshot.IMAGE_FORMATS[self._image_format]["wx_bmp_type"]) - - def _gtk_screenshot(self, path): - window = gdk.get_default_root_window() - if not window: - raise RuntimeError('Taking screenshot failed.') - width, height = window.get_size() - pb = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, width, height) - pb = pb.get_from_drawable(window, window.get_colormap(), - 0, 0, 0, 0, width, height) - if not pb: - raise RuntimeError('Taking screenshot failed.') - pb.save(path, Screenshot.IMAGE_FORMATS[self._image_format]["gtk_type"]) - - def _pil_screenshot(self, path): - ImageGrab.grab().save(path, Screenshot.IMAGE_FORMATS[self._image_format]["pil_type"]) - - def _no_screenshot(self, path): - raise RuntimeError('Taking screenshots is not supported on this platform ' - 'by default. See library documentation for details.') - - -if __name__ == "__main__": - if len(sys.argv) not in [2, 3]: - sys.exit("Usage: %s |test [wxpython|pygtk|pil|scrot]" - % os.path.basename(sys.argv[0])) - path = sys.argv[1] if sys.argv[1] != 'test' else None - module = sys.argv[2] if len(sys.argv) > 2 else None - ScreenshotTaker(module).test(path) +# Copyright 2008-2015 Nokia Networks +# Copyright 2016- Robot Framework Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +import sys + +try: + import wx +except ImportError: + wx = None +try: + from gtk import gdk +except ImportError: + gdk = None +try: + from PIL import ImageGrab # apparently available only on Windows +except ImportError: + ImageGrab = None + +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn +from robot.version import get_version +from robot.utils import abspath, get_error_message, get_link_path + + +class Screenshot: + """Library for taking screenshots on the machine where tests are executed. + + Taking the actual screenshot requires a suitable tool or module that may + need to be installed separately. Taking screenshots also requires tests + to be run with a physical or virtual display. + + == Table of contents == + + %TOC% + + = Supported screenshot taking tools and modules = + + How screenshots are taken depends on the operating system. On OSX + screenshots are taken using the built-in ``screencapture`` utility. On + other operating systems you need to have one of the following tools or + Python modules installed. You can specify the tool/module to use when + `importing` the library. If no tool or module is specified, the first + one found will be used. + + - wxPython :: http://wxpython.org :: Generic Python GUI toolkit. + - supports: png, jpeg, gif, pcx, pnm, tiff, tga, ico, cur + - PyGTK :: http://pygtk.org :: This module is available by default on most + Linux distributions. + - supports: + - Pillow :: http://python-pillow.github.io :: + Only works on Windows. Also the original PIL package is supported. + - Scrot :: http://en.wikipedia.org/wiki/Scrot :: Not used on Windows. + Install with ``apt-get install scrot`` or similar. + + = Where screenshots are saved = + + By default screenshots are saved into the same directory where the Robot + Framework log file is written. If no log is created, screenshots are saved + into the directory where the XML output file is written. + + It is possible to specify a custom location for screenshots using + ``screenshot_directory`` argument when `importing` the library and + using `Set Screenshot Directory` keyword during execution. It is also + possible to save screenshots using an absolute path. + + = ScreenCapLibrary = + + [https://github.com/mihaiparvu/ScreenCapLibrary|ScreenCapLibrary] is an + external Robot Framework library that can be used as an alternative, + which additionally provides support for multiple formats, adjusting the + quality, using GIFs and video capturing. + """ + + ROBOT_LIBRARY_SCOPE = 'TEST SUITE' + ROBOT_LIBRARY_VERSION = get_version() + IMAGE_FORMATS = { + "jpg": {"extensions": ("jpg", "jpeg",), + "wx_bmp_type": "BITMAP_TYPE_JPEG", + "gtk_type": "jpeg", + "pil_type": "JPEG"}, + "png": {"extensions": ("png",), + "wx_bmp_type": "BITMAP_TYPE_PNG", + "gtk_type": "png", + "pil_type": "PNG"}, + "tiff": {"extensions": ("tiff", "tif",), + "wx_bmp_type": "BITMAP_TYPE_TIFF", + "gtk_type": None, + "pil_type": "TIFF"}, + "bmp": {"extensions": ("bmp",), + "wx_bmp_type": "BITMAP_TYPE_BMP", + "gtk_type": "bmp", + "pil_type": "BMP"} + } + + def __init__(self, screenshot_directory=None, screenshot_module=None, image_format="jpg"): + """Configure where screenshots are saved. + + If ``screenshot_directory`` is not given, screenshots are saved into + same directory as the log file. The directory can also be set using + `Set Screenshot Directory` keyword. + + ``screenshot_module`` specifies the module or tool to use when using + this library outside OSX. Possible values are ``wxPython``, + ``PyGTK``, ``PIL`` and ``scrot``, case-insensitively. If no value is + given, the first module/tool found is used in that order. + + Examples: + | =Setting= | =Value= | =Value= | + | Library | Screenshot | | + | Library | Screenshot | ${TEMPDIR} | + | Library | Screenshot | screenshot_module=PyGTK | + """ + # take in the image format here, so it can be referenced from here on out + self._out_img_format = image_format + self._given_screenshot_dir = self._norm_path(screenshot_directory) + # make sure all screenshot modules support the type we want - they do + self._screenshot_taker = ScreenshotTaker(screenshot_module, self._out_img_format) + + def _norm_path(self, path): + if not path: + return path + elif isinstance(path, os.PathLike): + path = str(path) + else: + path = path.replace('/', os.sep) + return os.path.normpath(path) + + @property + def _screenshot_dir(self): + return self._given_screenshot_dir or self._log_dir + + @property + def _log_dir(self): + variables = BuiltIn().get_variables() + outdir = variables['${OUTPUTDIR}'] + log = variables['${LOGFILE}'] + log = os.path.dirname(log) if log != 'NONE' else '.' + return self._norm_path(os.path.join(outdir, log)) + + def set_screenshot_directory(self, path): + """Sets the directory where screenshots are saved. + + It is possible to use ``/`` as a path separator in all operating + systems. Path to the old directory is returned. + + The directory can also be set in `importing`. + """ + print(f"path passed in is: {path}") + path = self._norm_path(path) + if not os.path.isdir(path): + raise RuntimeError("Directory '%s' does not exist." % path) + old = self._screenshot_dir + self._given_screenshot_dir = path + return old + + def take_screenshot(self, name="screenshot", width="800px", img_format="jpg"): + """Takes a screenshot in the specified format, jpg by default, and embeds + it into the log file + + Name of the file where the screenshot is stored is derived from the + given ``name``. If the ``name`` ends with extension ``.jpg`` or + ``.jpeg``, the screenshot will be stored with that exact name. + Otherwise a unique name is created by adding an underscore, a running + index and an extension to the ``name``. + + The name will be interpreted to be relative to the directory where + the log file is written. It is also possible to use absolute paths. + Using ``/`` as a path separator works in all operating systems. + + ``width`` specifies the size of the screenshot in the log file. + + ``img_format`` specifies the image format for the screenshot, default is "jpg" + + Examples: (LOGDIR is determined automatically by the library) + | Take Screenshot | | | # LOGDIR/screenshot_1.jpg (index automatically incremented) | + | Take Screenshot | mypic | | # LOGDIR/mypic_1.jpg (index automatically incremented) | + | Take Screenshot | ${TEMPDIR}/mypic | | # /tmp/mypic_1.jpg (index automatically incremented) | + | Take Screenshot | pic.jpg | | # LOGDIR/pic.jpg (always uses this file) | + | Take Screenshot | images/login.jpg | 80% | # Specify both name and width. | + | Take Screenshot | width=550px | | # Specify only width. | + | Take Screenshot | img_format=png | | # Specify only image format, png in this case | + | Take Screenshot | png_image | img_format=tiff | # Specify name and image format, "tiff" in this case | + + The path where the screenshot is saved is returned. + """ + self._out_img_format = img_format + path = self._save_screenshot(name) + self._embed_screenshot(path, width) + return path + + def take_screenshot_without_embedding(self, name="screenshot", img_format="jpg"): + """Takes a screenshot and links it from the log file. + + This keyword is otherwise identical to `Take Screenshot` but the saved + screenshot is not embedded into the log file. The screenshot is linked + so it is nevertheless easily available. + """ + self._out_img_format = img_format + path = self._save_screenshot(name) + self._link_screenshot(path) + return path + + def _save_screenshot(self, name): + name = str(name) if isinstance(name, os.PathLike) else name.replace('/', os.sep) + path = self._get_screenshot_path(name) + return self._screenshot_to_file(path) + + def _screenshot_to_file(self, path): + path = self._validate_screenshot_path(path) + logger.debug('Using %s module/tool for taking screenshot.' + % self._screenshot_taker.module) + try: + self._screenshot_taker(path) + except: + logger.warn('Taking screenshot failed: %s\n' + 'Make sure tests are run with a physical or virtual ' + 'display.' % get_error_message()) + return path + + def _validate_screenshot_path(self, path): + path = abspath(self._norm_path(path)) + if not os.path.exists(os.path.dirname(path)): + raise RuntimeError("Directory '%s' where to save the screenshot " + "does not exist" % os.path.dirname(path)) + return path + + def _get_screenshot_path(self, basename): + try: + # retrieve a tuple of the common file extension variations + file_extensions = Screenshot.IMAGE_FORMATS[self._out_img_format]["extensions"] + except KeyError as ke: + logger.error(f"KeyError in _get_screenshot_path(): {ke}") + # if basename.lower().endswith(file_extensions): + if any([basename.lower().endswith(x) for x in file_extensions]): + return os.path.join(self._screenshot_dir, basename) + index = 0 + while True: + index += 1 + # use the image format key for the file extension + path = os.path.join(self._screenshot_dir, "%s_%d.%s" % (basename, index, self._out_img_format)) + if not os.path.exists(path): + return path + + def _embed_screenshot(self, path, width): + link = get_link_path(path, self._log_dir) + logger.info('' + % (link, link, width), html=True) + + def _link_screenshot(self, path): + link = get_link_path(path, self._log_dir) + logger.info("Screenshot saved to '%s'." + % (link, path), html=True) + + +class ScreenshotTaker: + + def __init__(self, module_name=None, image_format='jpg'): + self._image_format = image_format + self._screenshot = self._get_screenshot_taker(module_name) + self.module = self._screenshot.__name__.split('_')[1] + self._wx_app_reference = None + + def __call__(self, path): + self._screenshot(path) + + def __bool__(self): + return self.module != 'no' + + def test(self, path=None): + if not self: + print("Cannot take screenshots.") + return False + print("Using '%s' to take screenshot." % self.module) + if not path: + print("Not taking test screenshot.") + return True + print("Taking test screenshot to '%s'." % path) + try: + self(path) + except: + print("Failed: %s" % get_error_message()) + return False + else: + print("Success!") + return True + + def _get_screenshot_taker(self, module_name=None): + if sys.platform == 'darwin': + return self._osx_screenshot + if module_name: + return self._get_named_screenshot_taker(module_name.lower()) + return self._get_default_screenshot_taker() + + def _get_named_screenshot_taker(self, name): + screenshot_takers = {'wxpython': (wx, self._wx_screenshot), + 'pygtk': (gdk, self._gtk_screenshot), + 'pil': (ImageGrab, self._pil_screenshot), + 'scrot': (self._scrot, self._scrot_screenshot)} + if name not in screenshot_takers: + raise RuntimeError("Invalid screenshot module or tool '%s'." % name) + supported, screenshot_taker = screenshot_takers[name] + if not supported: + raise RuntimeError("Screenshot module or tool '%s' not installed." + % name) + return screenshot_taker + + def _get_default_screenshot_taker(self): + for module, screenshot_taker in [(wx, self._wx_screenshot), + (gdk, self._gtk_screenshot), + (ImageGrab, self._pil_screenshot), + (self._scrot, self._scrot_screenshot), + (True, self._no_screenshot)]: + if module: + return screenshot_taker + + def _osx_screenshot(self, path): + if self._call('screencapture', '-t', self._image_format, path) != 0: + raise RuntimeError("Using 'screencapture' failed.") + + def _call(self, *command): + try: + return subprocess.call(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + except OSError: + return -1 + + @property + def _scrot(self): + return os.sep == '/' and self._call('scrot', '--version') == 0 + + def _scrot_screenshot(self, path): + try: + file_extensions = Screenshot.IMAGE_FORMATS[self._image_format]["extensions"] + except KeyError as ke: + logger.error(f"KeyError in _scrot_screenshot(): {ke}") + if any([path.endswith(x) for x in file_extensions]): + raise RuntimeError("Scrot requires extension to be like %s, " + "but got '%s'." % (str(file_extensions), os.path.splitext(path)[1])) + if os.path.exists(path): + os.remove(path) + if self._call('scrot', '--silent', path) != 0: + raise RuntimeError("Using 'scrot' failed.") + + def _wx_screenshot(self, path): + if not self._wx_app_reference: + self._wx_app_reference = wx.App(False) + context = wx.ScreenDC() + width, height = context.GetSize() + if wx.__version__ >= '4': + bitmap = wx.Bitmap(width, height, -1) + else: + bitmap = wx.EmptyBitmap(width, height, -1) + memory = wx.MemoryDC() + memory.SelectObject(bitmap) + memory.Blit(0, 0, width, height, context, -1, -1) + memory.SelectObject(wx.NullBitmap) + bitmap.SaveFile(path, Screenshot.IMAGE_FORMATS[self._image_format]["wx_bmp_type"]) + + def _gtk_screenshot(self, path): + window = gdk.get_default_root_window() + if not window: + raise RuntimeError('Taking screenshot failed.') + width, height = window.get_size() + pb = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, width, height) + pb = pb.get_from_drawable(window, window.get_colormap(), + 0, 0, 0, 0, width, height) + if not pb: + raise RuntimeError('Taking screenshot failed.') + pb.save(path, Screenshot.IMAGE_FORMATS[self._image_format]["gtk_type"]) + + def _pil_screenshot(self, path): + ImageGrab.grab().save(path, Screenshot.IMAGE_FORMATS[self._image_format]["pil_type"]) + + def _no_screenshot(self, path): + raise RuntimeError('Taking screenshots is not supported on this platform ' + 'by default. See library documentation for details.') + + +if __name__ == "__main__": + if len(sys.argv) not in [2, 3]: + sys.exit("Usage: %s |test [wxpython|pygtk|pil|scrot]" + % os.path.basename(sys.argv[0])) + path = sys.argv[1] if sys.argv[1] != 'test' else None + module = sys.argv[2] if len(sys.argv) > 2 else None + ScreenshotTaker(module).test(path) From 5c941c3bcac3a3e6c75faec5b38459d5a6ac8468 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Thu, 11 Apr 2024 10:02:11 -0700 Subject: [PATCH 5/8] Removed a few comments that I'd missed previously --- src/robot/libraries/Screenshot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/robot/libraries/Screenshot.py b/src/robot/libraries/Screenshot.py index 508316e65b5..4ecf505f2ae 100755 --- a/src/robot/libraries/Screenshot.py +++ b/src/robot/libraries/Screenshot.py @@ -124,10 +124,8 @@ def __init__(self, screenshot_directory=None, screenshot_module=None, image_form | Library | Screenshot | ${TEMPDIR} | | Library | Screenshot | screenshot_module=PyGTK | """ - # take in the image format here, so it can be referenced from here on out self._out_img_format = image_format self._given_screenshot_dir = self._norm_path(screenshot_directory) - # make sure all screenshot modules support the type we want - they do self._screenshot_taker = ScreenshotTaker(screenshot_module, self._out_img_format) def _norm_path(self, path): @@ -244,7 +242,6 @@ def _get_screenshot_path(self, basename): file_extensions = Screenshot.IMAGE_FORMATS[self._out_img_format]["extensions"] except KeyError as ke: logger.error(f"KeyError in _get_screenshot_path(): {ke}") - # if basename.lower().endswith(file_extensions): if any([basename.lower().endswith(x) for x in file_extensions]): return os.path.join(self._screenshot_dir, basename) index = 0 From 18e55659977e92adb30cd28ff9b568d7a0ed4fb7 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Wed, 19 Jun 2024 10:44:47 -0700 Subject: [PATCH 6/8] Created Test Setup/Teardown keywords for the tests; Resolved issues with comparing the right directories; --- .../screenshot/take_screenshot.robot | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/atest/testdata/standard_libraries/screenshot/take_screenshot.robot b/atest/testdata/standard_libraries/screenshot/take_screenshot.robot index 37e723a1415..0ecaf9457e1 100644 --- a/atest/testdata/standard_libraries/screenshot/take_screenshot.robot +++ b/atest/testdata/standard_libraries/screenshot/take_screenshot.robot @@ -1,17 +1,22 @@ *** Settings *** +Library OperatingSystem Suite Setup Remove Files ${OUTPUTDIR}/*.jp*g ${OUTPUTDIR}/*.png ${OUTPUTDIR}/*.tiff ${OUTPUTDIR}/*.bmp -Test Setup Save Start Time -Test Teardown Remove Files ${OUTPUTDIR}/*.jp*g ${OUTPUTDIR}/*.png ${OUTPUTDIR}/*.tiff ${OUTPUTDIR}/*.bmp +Test Setup Take Screenshot Test Setup +Test Teardown Take Screenshot Test Teardown Resource screenshot_resource.robot *** Variables *** ${FIRST_SCREENSHOT} screenshot_1.jpg ${SECOND_SCREENSHOT} screenshot_2.jpg +${SCREENSHOT DIR} = %{TEMPDIR}${/}robot_atest_screenshots +${BASENAME} = ${SCREENSHOT DIR}${/}screenshot +${FIRST_SCREENSHOT} = ${BASENAME}_1.jpg *** Test Cases *** Screenshot Is Embedded in Log File ${path}= Take Screenshot and Verify ${FIRST_SCREENSHOT} - Should Be Equal ${path} ${OUTPUTDIR}${/}${FIRST_SCREENSHOT} + ${path_normalized} = Normalize Path ${SCREENSHOT DIR} + Should Be Equal ${path} ${SCREENSHOT DIR}${/}${FIRST_SCREENSHOT} Each Screenshot Gets Separate Index Take Screenshot and Verify ${FIRST_SCREENSHOT} @@ -19,52 +24,52 @@ Each Screenshot Gets Separate Index Basename May Be Defined Repeat Keyword 2 Take Screenshot foo - Screenshots Should Exist ${OUTPUTDIR} foo_1.jpg foo_2.jpg + Screenshots Should Exist ${SCREENSHOT DIR} foo_1.jpg foo_2.jpg Basename May Be Defined With Screenshot Format Of PNG Repeat Keyword 2 Take Screenshot foo img_format=png - Screenshots Should Exist ${OUTPUTDIR} foo_1.png foo_2.png format=png + Screenshots Should Exist ${SCREENSHOT DIR} foo_1.png foo_2.png format=png Basename May Be Defined With Screenshot Format Of TIFF Repeat Keyword 2 Take Screenshot foo img_format=tiff - Screenshots Should Exist ${OUTPUTDIR} foo_1.tiff foo_2.tiff format=tiff + Screenshots Should Exist ${SCREENSHOT DIR} foo_1.tiff foo_2.tiff format=tiff Basename With Extension Turns Off Index Generation Repeat Keyword 3 Take Screenshot xxx.jpg img_format=jpg Repeat Keyword 2 Take Screenshot yyy.jpeg img_format=jpg - Screenshots Should Exist ${OUTPUTDIR} xxx.jpg yyy.jpeg format=jpg + Screenshots Should Exist ${SCREENSHOT DIR} xxx.jpg yyy.jpeg format=jpg Repeat Keyword 3 Take Screenshot xxx.png img_format=png - Screenshots Should Exist ${OUTPUTDIR} xxx.png format=png + Screenshots Should Exist ${SCREENSHOT DIR} xxx.png format=png Repeat Keyword 3 Take Screenshot xxx.bmp img_format=bmp - Screenshots Should Exist ${OUTPUTDIR} xxx.bmp format=bmp + Screenshots Should Exist ${SCREENSHOT DIR} xxx.bmp format=bmp Name as `pathlib.Path` Take Screenshot ${{pathlib.Path('name.jpg')}} - Screenshots Should Exist ${OUTPUTDIR} name.jpg + Screenshots Should Exist ${SCREENSHOT DIR} name.jpg Name as `pathlib.Path` - Format PNG Take Screenshot ${{pathlib.Path('name.png')}} img_format=png - Screenshots Should Exist ${OUTPUTDIR} name.png format=png + Screenshots Should Exist ${SCREENSHOT DIR} name.png format=png Name as `pathlib.Path` - Format TIFF Take Screenshot ${{pathlib.Path('name.tiff')}} img_format=tiff - Screenshots Should Exist ${OUTPUTDIR} name.tiff format=tiff + Screenshots Should Exist ${SCREENSHOT DIR} name.tiff format=tiff Screenshot Width Can Be Given Take Screenshot width=300px - Screenshots Should Exist ${OUTPUTDIR} ${FIRST_SCREENSHOT} + Screenshots Should Exist ${SCREENSHOT DIR} ${FIRST_SCREENSHOT} Screenshot Width Can Be Given For PNG Take Screenshot foo.png width=300px img_format=png - Screenshots Should Exist ${OUTPUTDIR} foo.png format=png + Screenshots Should Exist ${SCREENSHOT DIR} foo.png format=png Screenshot Width Can Be Given For BMP Take Screenshot foo.bmp width=300px img_format=bmp - Screenshots Should Exist ${OUTPUTDIR} foo.bmp format=bmp + Screenshots Should Exist ${SCREENSHOT DIR} foo.bmp format=bmp Basename With Non-existing Directories Fails - [Documentation] FAIL Directory '${/}non-existing' where to save the screenshot does not exist - Take Screenshot ${/}non-existing${/}foo + Run Keyword And Expect Error Directory '/non-existing' where to save the screenshot does not exist + ... Take Screenshot ${/}non-existing${/}foo Without Embedding Take Screenshot Without Embedding no_embed.jpeg @@ -76,8 +81,18 @@ Take Screenshot Without Embedding for TIFF Take Screenshot Without Embedding no_embed.tiff img_format=tiff *** Keywords *** +Take Screenshot Test Setup + Save Start Time + Create Directory ${SCREENSHOT DIR} + Set Screenshot Directory ${SCREENSHOT DIR} + +Take Screenshot Test Teardown + Remove Directory ${SCREENSHOT DIR} recursive=${True} + Remove Files ${SCREENSHOT DIR}/*.jp*g ${SCREENSHOT DIR}/*.png + ... ${SCREENSHOT DIR}/*.tiff ${SCREENSHOT DIR}/*.bmp + Take Screenshot And Verify [Arguments] @{expected files} ${path}= Take Screenshot - Screenshots Should Exist ${OUTPUTDIR} @{expected files} + Screenshots Should Exist ${SCREENSHOT DIR} @{expected files} RETURN ${path} From 9400624569a5014f3ce5888dc41d21c12d1d2840 Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Sun, 30 Jun 2024 06:54:25 -0700 Subject: [PATCH 7/8] Addition of the screenshot_dir variable for use as test resrouces; --- atest/resources/atest_resource.robot | 1 + 1 file changed, 1 insertion(+) diff --git a/atest/resources/atest_resource.robot b/atest/resources/atest_resource.robot index ed84a5873b2..e39d29dc8aa 100644 --- a/atest/resources/atest_resource.robot +++ b/atest/resources/atest_resource.robot @@ -33,6 +33,7 @@ ${RUNNER DEFAULTS} ... --ConsoleMarkers OFF ... --PYTHONPATH "${CURDIR}${/}..${/}testresources${/}testlibs" ... --PYTHONPATH "${CURDIR}${/}..${/}testresources${/}listeners" +${SCREENSHOT DIR} %{TEMPDIR}${/}robot_atest_screenshots *** Keywords *** Run Tests From c634b7eb441050222ff87d09cf6429908bd795ae Mon Sep 17 00:00:00 2001 From: Chris Callan Date: Sun, 30 Jun 2024 06:55:57 -0700 Subject: [PATCH 8/8] Modified check keywords to use the screenshot dir during path verification; --- .../screenshot/take_screenshot.robot | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/atest/robot/standard_libraries/screenshot/take_screenshot.robot b/atest/robot/standard_libraries/screenshot/take_screenshot.robot index 9c42797be6f..bf437da63fe 100644 --- a/atest/robot/standard_libraries/screenshot/take_screenshot.robot +++ b/atest/robot/standard_libraries/screenshot/take_screenshot.robot @@ -46,9 +46,17 @@ Without Embedding *** Keywords *** Check Embedding In Log [Arguments] ${message} ${path} ${width}=800px - Check Log Message ${message} HTML + ${rel_dir} = Get Screenshot Dir As Relative Path ${path} + Check Log Message ${message} HTML Check Linking In Log [Arguments] ${message} ${file} - ${path} = Normalize Path ${OUTDIR}/${file} - Check Log Message ${message} Screenshot saved to '${path}'. HTML + ${path} = Set Variable ${SCREENSHOT DIR}/${file} + ${rel_dir} = Get Screenshot Dir As Relative Path ${file} + Check Log Message ${message} Screenshot saved to '${path}'. HTML + +Get Screenshot Dir As Relative Path + [Arguments] ${path} + ${parent_dir} = Evaluate "${SCREENSHOT DIR}".split('/')[-1] + ${ret_val} = Set Variable ../${parent_dir}/${path} + RETURN ${ret_val} \ No newline at end of file