All of lore.kernel.org
 help / color / mirror / Atom feed
* [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test
@ 2018-04-24 17:24 Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 01/14] Add a SeleniumCommand that compares screenshots Guilherme Campos Camargo
                   ` (15 more replies)
  0 siblings, 16 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Hello, everyone.

This series of patches adds a new test case class to the
fuego_release_test functional test. The name of this new test class is
"CheckScreenshot", and its purpose is to get a screenshot of a web-page
(in this specific case, Fuego's Jenkins' webpage) and compare it with a
reference image.

Currently, CheckScreenshot supports:
 - Taking screenshot of an HTML element of a page and compare it with a
   reference image.
 - Taking screenshot of the full viewport (full page if it fits in the
   viewport) and also compare it with a reference image.
 - Take a screenshot (full or element) and compare only specific regions
   of interest with a reference image. The mask is a black-and-white
   image in which black regions are ignored by the comparison algorithm.

On these patches we have also added a helper script (take_screenshot.py)
that may be used for collecting reference screenshots and a few test
cases to serve as example of usage.

A README.md file has also been to explain the usage of
fuego_release_test and the helper script, including a few examples.


# Running

Currently this test requires a modified version of Fuego to be executed,
given that it needs to install some dependencies and needs to map the
dockerd socket from the host to the fuego container.

The modified version can be found in two different branches on
Profusion's fuego fork.

 1 - Branch master: Just a few commits that are necessary for making
 this test work, applied on top of fuego/next. We plan to try to
 integrate these commits into fuego/next in the future.

 2 - Branch fuego-base-image: A more complex change on fuego, that makes
 the necessary changes for allowing it to be shipped as a docker image
 through dockerhub.

The steps for each one of the versions above are given below:

## Building the image (from the branch fuego-test)

To run the test, execute the following commands:

```
git clone --branch master https://bitbucket.org/profusionmobi/fuego-core.git
git clone --branch fuego-test https://bitbucket.org/profusionmobi/fuego.git
cd fuego
./install fuego-to-test-fuego
./fuego-host-scripts/docker-create-container.sh fuego-to-test-fuego fuego-to-test-fuego-container
./fuego-host-scripts/docker-start-container.sh fuego-to-test-fuego-container
```

Then, add the fuego-test board and the Functional.fuegotest and start
the test through Jenkins (localhost:8080/fuego/)

```
ftc add-nodes fuego-test
ftc add-jobs -b fuego-test -t Functional.fuegotest
```

## Using the modified Fuego Base Image from Dockerhub
(fuego-base-image):

You can also use the fuego base image that's being developed in
Profusion's fuego-base-image branch in our fork:
https://bitbucket.org/profusionmobi/fuego/branch/fuego-base-image

The image is already available on dockerhub and can be
downloaded/executed with:

```
docker pull fuegotest/fuego
docker run -it \
  -p 8080:8080 \
  -v $(pwd)/host_fuego_home:/var/fuego_home \
  -e JENKINS_UID=$(id -u) \
  -e JENKINS_GID=$(id -g) \
  -v /var/run/docker.sock:/var/run/docker.sock \
  fuegotest/fuego:latest
```

Wait for the shell to be available and add fuego-test board and
Functional.fuegotest as explained in the last section.

```
ftc add-nodes fuego-test
ftc add-jobs -b fuego-test -t Functional.fuegotest
```

You can also run the test in standalone mode (given that you have all
the dependencies installed in your system). See the test README.md for
more instructions.

Thanks.

Guilherme Campos Camargo (14):
  Add a SeleniumCommand that compares screenshots
  Minor style fix
  Add a CheckScreenshot command into the COMMANDS_TO_TEST list
  Increase the size of the webdriver viewport
  Allow working_dir and install_dirs to be different
  Prevent exception NameError when removing container
  Add an example reference screenshot
  Add helper script for taking element/full-page screenshots
  Add mask-img-path argument to CheckScreenshot for ignored areas
  Add a README.md
  Allow Full viewport Screenshots
  Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7
  Improve logging
  Set logging level to INFO by default

 .../Functional.fuego_release_test/README.md   | 182 +++++++++
 .../fuego_test.sh                             |   6 +-
 .../helpers/take_screenshot.py                | 145 ++++++++
 .../screenshots/footer.png                    | Bin 0 -> 7371 bytes
 .../screenshots/footer_mask.png               | Bin 0 -> 302 bytes
 .../screenshots/full_screenshot.png           | Bin 0 -> 46323 bytes
 .../screenshots/full_screenshot_mask.png      | Bin 0 -> 10717 bytes
 .../screenshots/side-panel-tasks.png          | Bin 0 -> 9609 bytes
 .../Functional.fuego_release_test/test_run.py | 347 ++++++++++++++++--
 9 files changed, 639 insertions(+), 41 deletions(-)
 create mode 100644 engine/tests/Functional.fuego_release_test/README.md
 create mode 100755 engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/footer.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_mask.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/side-panel-tasks.png

-- 
2.17.0


^ permalink raw reply	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 01/14] Add a SeleniumCommand that compares screenshots
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 02/14] Minor style fix Guilherme Campos Camargo
                   ` (14 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

The newly added test takes a screenshot of an element present on the
current webpage and compares it to a reference image. An optional
argument 'threshold' may be provided to the CheckScreenshot constructor
to allow a certain amount of difference to be disconsidered.

This value represents the normalized root mean squared error (-metric
rmse as briefly explained in
https://www.imagemagick.org/script/command-line-options.php#. By
default, this 'threshold' is set to 0.0.

For the image comparison we're using ImageMagick

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../Functional.fuego_release_test/test_run.py | 148 +++++++++++++++++-
 1 file changed, 145 insertions(+), 3 deletions(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index d4c3882..35c7e90 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -9,13 +9,15 @@ import re
 import subprocess
 import sys
 import time
+from io import BytesIO
 
 import docker
 import pexpect
 import requests
+import selenium.common.exceptions as selenium_exceptions
 from selenium import webdriver
 from selenium.webdriver.common.by import By
-import selenium.common.exceptions as selenium_exceptions
+from PIL import Image
 
 LOGGER = logging.getLogger('test_run')
 STREAM_HANDLER = logging.StreamHandler()
@@ -39,6 +41,13 @@ def loop_until_timeout(func, timeout=10, num_tries=5):
     return False
 
 
+def silent_remove(filename):
+    try:
+        os.remove(filename)
+    except (FileNotFoundError, IsADirectoryError):
+        pass
+
+
 class SeleniumCommand:
     def exec(self, selenium_ctx):
         self.driver = selenium_ctx.driver
@@ -86,6 +95,20 @@ class SeleniumCommand:
         LOGGER.debug("  Element found")
         return element
 
+    @staticmethod
+    def get_element_screenshot(context, element):
+        location = element.location_once_scrolled_into_view
+        size = element.size
+
+        viewport_screenshot = Image.open(
+            BytesIO(context.driver.get_screenshot_as_png()))
+        element_screenshot = viewport_screenshot.\
+            crop((location['x'], location['y'],
+                  location['x'] + size['width'],
+                  location['y'] + size['height'],))
+
+        return element_screenshot
+
 
 class Visit(SeleniumCommand):
     def __init__(self, url, timeout=10, expected_result=200):
@@ -122,12 +145,121 @@ class CheckText(SeleniumCommand):
 
         element = SeleniumCommand.\
             find_element(selenium_ctx, self.locator, self.pattern)
-        if element:
-            result = SeleniumCommand.check_element_text(element, self.text)
+        if not element:
+            return False
 
+        result = SeleniumCommand.check_element_text(element, self.text)
         return result == self.expected_result
 
 
+class CheckScreenshot(SeleniumCommand):
+    def __init__(self, locator, pattern, ref_img, diff_img=None,
+                 expected_result=True, threshold=0.0,
+                 rm_images_on_success=True):
+        def add_suffix(filename, suffix):
+            head, sep, tail = ref_img.rpartition('.')
+            if sep:
+                return head + sep + suffix + '.' + tail
+
+            return tail + '.' + suffix
+
+        self.pattern = pattern
+        self.locator = locator
+        self.reference_img_path = ref_img
+        self.expected_result = expected_result
+        self.threshold = threshold
+        self.rm_images_on_success = rm_images_on_success
+
+        if diff_img is None:
+            self.diff_img_path = add_suffix(ref_img, 'diff')
+        else:
+            self.diff_img_path = diff_img
+
+        self.test_img_path = add_suffix(ref_img, 'test')
+
+    def compare_images(self, current_img_path, reference_img_path,
+                       diff_img_path, threshold):
+        cmd = ['magick',
+               'compare',
+               '-verbose',
+               '-metric',
+               'RMSE',
+               '-highlight-color',
+               'Red',
+               '-compose',
+               'Src',
+               current_img_path,
+               reference_img_path,
+               diff_img_path
+               ]
+
+        LOGGER.debug('  Comparing images...')
+        LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
+        process = subprocess.Popen(cmd, universal_newlines=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        process_stdout, process_stderr = process.communicate()
+        LOGGER.debug('  Results:')
+        LOGGER.debug('    return code: %s', process.returncode)
+        LOGGER.debug('    stdout: %s', process_stdout.strip())
+        LOGGER.debug('    stderr: %s', process_stderr.strip())
+
+        RESULTS_REGEX = re.compile(".*all:.*\(([0-9\-\.]*)\).*")
+        result = RESULTS_REGEX.search(process_stderr)
+
+        if not result:
+            LOGGER.error("   Error processing the output.")
+            return False
+
+        difference = float(result.group(1))
+        if difference > threshold:
+            LOGGER.debug("  Resulting difference (%s) above threshold (%s). ",
+                         difference, threshold)
+            LOGGER.debug("  Element's screenshot does not match the reference."
+                         "  See %s for a visual representation of the "
+                         "  differences.", diff_img_path)
+            return False
+
+        LOGGER.debug("  Resulting difference (%s) below threshold (%s).",
+                     difference, threshold)
+        LOGGER.debug("  Element's screenshot matches the reference.")
+        return True
+
+    def exec(self, selenium_ctx):
+        super().exec(selenium_ctx)
+
+        element = SeleniumCommand.\
+            find_element(selenium_ctx, self.locator, self.pattern)
+        if not element:
+            return False
+
+        screenshot = SeleniumCommand.\
+            get_element_screenshot(selenium_ctx, element)
+
+        try:
+            screenshot.save(self.test_img_path, format='PNG')
+        except FileNotFoundError:
+            LOGGER.error(
+                "  Unable to save the screenshot to '%s'. Does the directory "
+                "exist and can you write to it?", self.test_img_path)
+            return False
+
+        result = self.compare_images(self.test_img_path,
+                                     self.reference_img_path,
+                                     self.diff_img_path,
+                                     self.threshold)
+
+        if result != self.expected_result:
+            return False
+
+        # Remove diff and test images if there's a match
+        if self.rm_images_on_success:
+            silent_remove(self.diff_img_path)
+            silent_remove(self.test_img_path)
+
+        return True
+
+
 class Click(SeleniumCommand):
     def __init__(self, locator, pattern):
         self.pattern = pattern
@@ -474,6 +606,16 @@ def main():
         if container:
             container.delete()
 
+    def get_abs_working_dirs():
+        abs_install_dir = os.path.abspath(args.install_dir)
+
+        if args.working_dir:
+            abs_working_dir = os.path.abspath(args.working_dir)
+        else:
+            abs_working_dir = abs_install_dir
+
+        return abs_install_dir, abs_working_dir
+
     def execute_tests(timeout):
         LOGGER.debug("Starting tests")
 
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 02/14] Minor style fix
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 01/14] Add a SeleniumCommand that compares screenshots Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list Guilherme Campos Camargo
                   ` (13 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Minor indentation fix.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 engine/tests/Functional.fuego_release_test/test_run.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 35c7e90..b380acd 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -316,8 +316,8 @@ class ShExpect():
 
             self.client.sendline(
                 'echo "%s${%s}%s"' % (self.COMMAND_OUTPUT_DELIM,
-                                        self.OUTPUT_VARIABLE,
-                                        self.COMMAND_OUTPUT_DELIM))
+                                      self.OUTPUT_VARIABLE,
+                                      self.COMMAND_OUTPUT_DELIM))
             self.client.expect(self.COMMAND_OUTPUT_PATTERN)
             out = self.client.match.group(1)
 
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 01/14] Add a SeleniumCommand that compares screenshots Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 02/14] Minor style fix Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-25 23:17   ` Tim.Bird
  2018-04-24 17:24 ` [Fuego] [PATCH 04/14] Increase the size of the webdriver viewport Guilherme Campos Camargo
                   ` (12 subsequent siblings)
  15 siblings, 1 reply; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

This test will take a screenshot of 'tasks' element from the side-panel
of the Jenkins UI (that contains the 'New Item', 'People', 'Build
History' and the 'Manage Jenkins' buttons) and compare it with an image
that's stored in the workdir, called 'side-panel-tasks.png'. The allowed
threshold is 0.1 (10%).

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 engine/tests/Functional.fuego_release_test/test_run.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index b380acd..2075862 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -725,6 +725,12 @@ def main():
               'docker.default.Functional.hello_world"]'),
         CheckText(By.ID, 'executors',
                   text='docker.default.Functional.hello_world'),
+
+        # Compare screenshot of an element of Jenkins UI
+        CheckScreenshot(By.ID, 'tasks',
+                        rm_images_on_success=True,
+                        ref_img='screenshots/side-panel-tasks.png',
+                        threshold=0.1)
     ]
 
     if not execute_tests(args.timeout):
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 04/14] Increase the size of the webdriver viewport
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (2 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different Guilherme Campos Camargo
                   ` (11 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Increase the viewport size to 1920x1080 to allow large elements to fit
the screenshot.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 engine/tests/Functional.fuego_release_test/test_run.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 2075862..ed3c313 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -576,7 +576,7 @@ class SeleniumContainerSession():
         options = webdriver.ChromeOptions()
         options.add_argument('headless')
         options.add_argument('no-sandbox')
-        options.add_argument('window-size=1200x600')
+        options.add_argument('window-size=1920x1080')
         options.add_experimental_option(
             'prefs', {'intl.accept_languages': 'en,en_US'})
 
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (3 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 04/14] Increase the size of the webdriver viewport Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-25 23:20   ` Tim.Bird
  2018-04-24 17:24 ` [Fuego] [PATCH 06/14] Prevent exception NameError when removing container Guilherme Campos Camargo
                   ` (10 subsequent siblings)
  15 siblings, 1 reply; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

The `working_dir` is where all the test assets reside and also where all
the results (for instance the resulting images for the CheckScreenshot
test) will be stored after the tests.

The `install_dir` is where the install script can be found.

`install_dir` is a required argument, while `working_dir` is optional
and defaults to `install_dir`.

On this patch we're also modifying fuego_test.sh to copy the required
assets from the test directory to the buildzone.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../Functional.fuego_release_test/fuego_test.sh |  6 ++++--
 .../Functional.fuego_release_test/test_run.py   | 17 ++++++++++++++---
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/engine/tests/Functional.fuego_release_test/fuego_test.sh b/engine/tests/Functional.fuego_release_test/fuego_test.sh
index f0c872b..e87d6a4 100755
--- a/engine/tests/Functional.fuego_release_test/fuego_test.sh
+++ b/engine/tests/Functional.fuego_release_test/fuego_test.sh
@@ -18,11 +18,13 @@ function test_build {
     echo "Cloning fuego-core from ${fuego_core_repo}:${fuego_core_branch}"
     git clone --depth 1 --single-branch --branch "${fuego_core_branch}" \
         "${fuego_core_repo}" "${fuego_release_dir}/fuego-core"
-    cd -
+
+    echo "Copying assets from ${TEST_HOME} to the buildzone."
+    cp -r "${TEST_HOME}/screenshots" .
 }
 
 function test_run {
-    sudo -n ${TEST_HOME}/test_run.py "${fuego_release_dir}/fuego"
+    sudo -n "${TEST_HOME}/test_run.py" "${fuego_release_dir}/fuego" -w .
     if [ "${?}" = 0 ]; then
         report "echo ok 1 fuego release test"
     else
diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index ed3c313..8b97bde 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -638,7 +638,12 @@ def main():
         return tests_ok
 
     parser = argparse.ArgumentParser()
-    parser.add_argument('working_dir', help="The working directory", type=str)
+    parser.add_argument('install_dir', help="The directory where the install "
+                        "script resides.", type=str)
+    parser.add_argument('-w', '--working_dir', help="The working directory. "
+                        "Location of the test assets and where test results "
+                        "will be stored. Defaults to install_dir.",
+                        default=None, type=str)
     parser.add_argument('-s', '--install-script',
                         help="The script that will be used to install the " +
                         "docker image. Defaults to '%s'" %
@@ -672,8 +677,10 @@ def main():
                         default=True, action='store_false')
     args = parser.parse_args()
 
-    LOGGER.debug("Changing working dir to '%s'", args.working_dir)
-    os.chdir(args.working_dir)
+    args.install_dir, args.working_dir = get_abs_working_dirs()
+
+    LOGGER.debug("Changing working dir to '%s'", args.install_dir)
+    os.chdir(args.install_dir)
 
     container = FuegoContainer(args.install_script, args.image_name,
                                args.container_name, args.jenkins_port,
@@ -690,6 +697,10 @@ def main():
     if not selenium_session.start():
         return 1
 
+    if os.getcwd != args.working_dir:
+        LOGGER.debug("Changing working dir to '%s'", args.working_dir)
+        os.chdir(args.working_dir)
+
     COMMANDS_TO_TEST = [
         # Set Selenium Browser root
         Visit(url=container.get_url()),
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 06/14] Prevent exception NameError when removing container
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (4 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 07/14] Add an example reference screenshot Guilherme Campos Camargo
                   ` (9 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

A NameError exception was being raised on early script exits, for
example when the usage was being printed by argparse.

The exception was due to the FuegoContainer object (container) not
having been declared when atexit cleanup routine is called.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 engine/tests/Functional.fuego_release_test/test_run.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 8b97bde..ba840b6 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -603,8 +603,10 @@ def main():
 
     @atexit.register
     def cleanup():
-        if container:
+        try:
             container.delete()
+        except NameError:
+            pass
 
     def get_abs_working_dirs():
         abs_install_dir = os.path.abspath(args.install_dir)
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 07/14] Add an example reference screenshot
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (5 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 06/14] Prevent exception NameError when removing container Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 08/14] Add helper script for taking element/full-page screenshots Guilherme Campos Camargo
                   ` (8 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Add a reference image to serve as an example.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../screenshots/side-panel-tasks.png             | Bin 0 -> 9609 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/side-panel-tasks.png

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/side-panel-tasks.png b/engine/tests/Functional.fuego_release_test/screenshots/side-panel-tasks.png
new file mode 100644
index 0000000000000000000000000000000000000000..f67300e71cfdd3848054e4a85d5f79c0974856ac
GIT binary patch
literal 9609
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
SBErTLKu$_ovQFG2^nU<Xn8>#P

literal 0
HcmV?d00001

-- 
2.17.0


^ permalink raw reply	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 08/14] Add helper script for taking element/full-page screenshots
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (6 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 07/14] Add an example reference screenshot Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas Guilherme Campos Camargo
                   ` (7 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Add the helper script `take_screenshot.py` that navigates to a given URL
and takes a screenshot of an element - or from the full viewport when
the element is not provided.

This script is useful for generating reference screenshots to be
compared through the fuego-release-test's CheckScreenshot Selenium
Command.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../helpers/take_screenshot.py                | 145 ++++++++++++++++++
 1 file changed, 145 insertions(+)
 create mode 100755 engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py

diff --git a/engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py b/engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py
new file mode 100755
index 0000000..c7ae049
--- /dev/null
+++ b/engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+import argparse
+import requests
+import sys
+from io import BytesIO
+
+import selenium.common.exceptions as selenium_exceptions
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+from PIL import Image
+
+
+def public_attributes(obj):
+    return [attr for attr in dir(By) if not attr.startswith('__')]
+
+
+AVAILABLE_LOCATORS = public_attributes(By)
+DEFAULT_VIEWPORT_RESOLUTION = "1920x1080"
+
+
+class SeleniumSession:
+    def __init__(self, resolution):
+        options = webdriver.ChromeOptions()
+        options.add_argument('headless')
+        options.add_argument('no-sandbox')
+        options.add_argument('window-size=%s' % resolution)
+        options.add_experimental_option(
+            'prefs', {'intl.accept_languages': 'en,en_US'})
+
+        self.driver = webdriver.Chrome(chrome_options=options)
+        self.driver.implicitly_wait(3)
+
+    def __del__(self):
+        if self.driver:
+            self.driver.quit()
+
+    def go(self, url):
+        try:
+            r = requests.get(url)
+        except (requests.exceptions.MissingSchema,
+                requests.exceptions.ConnectionError,
+                requests.exceptions.ConnectTimeout) as e:
+            print(e)
+            return 404
+
+        if r.status_code != requests.codes.ok:
+            print("Unable to navigate to the page %s" % url)
+            print("Please provide a valid URL")
+            return r.status_code
+
+        self.driver.get(url)
+        return r.status_code
+
+    def take_screenshot(self, locator_str, pattern):
+        def take_element_screenshot(locator, pattern):
+            try:
+                element = self.driver.find_element(locator, pattern)
+            except selenium_exceptions.NoSuchElementException:
+                print("Element not found")
+                return None
+
+            location = element.location_once_scrolled_into_view
+            size = element.size
+
+            viewport_screenshot = Image.open(
+                BytesIO(self.driver.get_screenshot_as_png()))
+            element_screenshot = viewport_screenshot.\
+                crop((location['x'], location['y'],
+                      location['x'] + size['width'],
+                      location['y'] + size['height'],))
+
+            return element_screenshot
+
+        def take_full_screenshot():
+            return Image.open(
+                BytesIO(self.driver.get_screenshot_as_png()))
+
+        if not locator_str or not pattern:
+            print("Taking full screenshot (locator and/or pattern are empty)")
+            return take_full_screenshot()
+
+        locator = getattr(By, locator_str, None)
+        if not locator:
+            print("Locator '%s' is not supported" % locator_str)
+            return None
+
+        print("Taking element screenshot")
+        return take_element_screenshot(locator, pattern)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('url', help="""The URL from where a screenshot will be
+                        taken""", type=str)
+    parser.add_argument('-l', '--locator', help="the "
+                        """locator that will be used to find the element on the
+                        page. Available locators are: %s""" %
+                        AVAILABLE_LOCATORS, default=None, type=str)
+    parser.add_argument('-p', '--pattern', help="""The pattern that will be
+                        used for locating the element.""",
+                        default=None, type=str)
+    parser.add_argument('-o', '--out', help="""Resulting screenshot filename.
+                        Defaults to '${locator}_${pattern}.png' if those
+                        arguments are given or 'full_viewport.png' if not.""",
+                        default=None, type=int)
+    parser.add_argument('-r', '--resolution', help="""Viewport resolution.
+                        Defaults to %s. Make sure your page or element fits
+                        this resolution, otherwise the results might be
+                        cropped.""" % DEFAULT_VIEWPORT_RESOLUTION,
+                        default=DEFAULT_VIEWPORT_RESOLUTION, type=str)
+    args = parser.parse_args()
+
+    def generate_out_filename():
+        if not args.locator or not args.pattern:
+            return 'full_screenshot.png'
+        else:
+            return args.locator + '_' + args.pattern + '.png'
+
+    if not args.out:
+        args.out = generate_out_filename()
+
+    selenium = SeleniumSession(args.resolution)
+
+    status_code = selenium.go(args.url)
+    if status_code != requests.codes.ok:
+        print("Unable to navigate to URL")
+        return 1
+
+    screenshot = selenium.take_screenshot(args.locator, args.pattern)
+    if not screenshot:
+        print("Unable to take a screenshot")
+        return 1
+
+    try:
+        screenshot.save(args.out, format='PNG')
+    except FileNotFoundError as e:
+        print("Unable to save the screenshot to '%s'" % args.out)
+        print("  Reason: %s" % e)
+        return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (7 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 08/14] Add helper script for taking element/full-page screenshots Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-25 23:28   ` Tim.Bird
  2018-04-24 17:24 ` [Fuego] [PATCH 10/14] Add a README.md Guilherme Campos Camargo
                   ` (6 subsequent siblings)
  15 siblings, 1 reply; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

The new mask-img-path argument that has been added to CheckScreenshot
can be used to provide a mask image that will determine which parts of
the screenshots will be compared.

The mask needs to be a Black and White .png image in which the Black
areas will be ignored on the comparison, while the White areas will be
compared normally.

A new CheckScreenshot has been added to check the 'page_generated'
element of the Jenkins web-interface. That element is positioned in the
lower right corner of the page and contains a DateTime field that
changes continually.

The assets used as reference and mask have also been included in this
commit.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../screenshots/page-generated-mask.png       | Bin 0 -> 183 bytes
 .../screenshots/page-generated.png            | Bin 0 -> 4583 bytes
 .../Functional.fuego_release_test/test_run.py |  25 +++++++++++++-----
 3 files changed, 19 insertions(+), 6 deletions(-)
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/page-generated.png

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png b/engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png
new file mode 100644
index 0000000000000000000000000000000000000000..a565ba4dd4b8cbd5ab4be9c7050d8046f63e238a
GIT binary patch
literal 183
zcmeAS@N?(olHy`uVBq!ia0y~yU{nCI`8e2sWPi}XA|S<<<n8Xl@E-&h>|H(?D8gCb
z5n0T@z%2~Ij105pNB{-dOFVsD*&ne8i79f=*0Tx$3b}Z?IEG~0dwXRgFM|Qk;SIa~
z|9=xV!PeoVu!%-)b%bgsN6kYo1_^-#2L?7434sPiMmClN2OtF(I`D*X)n68A7Wv8g
QK=T+pUHx3vIVCg!0HVY$sQ>@~

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-generated.png b/engine/tests/Functional.fuego_release_test/screenshots/page-generated.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b16d92e5cc3ec6c9f57723115e21f8f23f5eab9
GIT binary patch
literal 4583
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
RPL==w002ovPDHLkV1i3q$F=|f

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index ba840b6..5fa2ca1 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -153,7 +153,7 @@ class CheckText(SeleniumCommand):
 
 
 class CheckScreenshot(SeleniumCommand):
-    def __init__(self, locator, pattern, ref_img, diff_img=None,
+    def __init__(self, locator, pattern, ref_img, mask_img=None, diff_img=None,
                  expected_result=True, threshold=0.0,
                  rm_images_on_success=True):
         def add_suffix(filename, suffix):
@@ -166,6 +166,7 @@ class CheckScreenshot(SeleniumCommand):
         self.pattern = pattern
         self.locator = locator
         self.reference_img_path = ref_img
+        self.mask_img_path = mask_img
         self.expected_result = expected_result
         self.threshold = threshold
         self.rm_images_on_success = rm_images_on_success
@@ -178,8 +179,8 @@ class CheckScreenshot(SeleniumCommand):
         self.test_img_path = add_suffix(ref_img, 'test')
 
     def compare_images(self, current_img_path, reference_img_path,
-                       diff_img_path, threshold):
-        cmd = ['magick',
+                       mask_img_path, diff_img_path, threshold):
+        cmd = ('magick',
                'compare',
                '-verbose',
                '-metric',
@@ -188,10 +189,14 @@ class CheckScreenshot(SeleniumCommand):
                'Red',
                '-compose',
                'Src',
+               '-read-mask' if mask_img_path else None,
+               mask_img_path if mask_img_path else None,
                current_img_path,
                reference_img_path,
-               diff_img_path
-               ]
+               diff_img_path,
+               )
+
+        cmd = list(filter(None, cmd))
 
         LOGGER.debug('  Comparing images...')
         LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
@@ -246,6 +251,7 @@ class CheckScreenshot(SeleniumCommand):
 
         result = self.compare_images(self.test_img_path,
                                      self.reference_img_path,
+                                     self.mask_img_path,
                                      self.diff_img_path,
                                      self.threshold)
 
@@ -743,7 +749,14 @@ def main():
         CheckScreenshot(By.ID, 'tasks',
                         rm_images_on_success=True,
                         ref_img='screenshots/side-panel-tasks.png',
-                        threshold=0.1)
+                        threshold=0.1),
+
+        # Compare screenshot of an element of Jenkins UI ignoring an area
+        CheckScreenshot(By.CLASS_NAME, 'page_generated',
+                        rm_images_on_success=False,
+                        mask_img='screenshots/page-generated-mask.png',
+                        ref_img='screenshots/page-generated.png',
+                        threshold=0.1),
     ]
 
     if not execute_tests(args.timeout):
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 10/14] Add a README.md
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (8 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 11/14] Allow Full viewport Screenshots Guilherme Campos Camargo
                   ` (5 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Add a README.md that explains the usage of the fuego-release-test as
well as the usage of the helper scripts.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../Functional.fuego_release_test/README.md   | 182 ++++++++++++++++++
 1 file changed, 182 insertions(+)
 create mode 100644 engine/tests/Functional.fuego_release_test/README.md

diff --git a/engine/tests/Functional.fuego_release_test/README.md b/engine/tests/Functional.fuego_release_test/README.md
new file mode 100644
index 0000000..1ea6ab3
--- /dev/null
+++ b/engine/tests/Functional.fuego_release_test/README.md
@@ -0,0 +1,182 @@
+# Fuego Release Test
+
+This Fuego Functional test may be used for automating tests of new Fuego
+Releases through Fuego itself, or may be used in standalone mode (outside
+Fuego) to test a Fuego version that's present in your filesystem.
+
+## Dependencies
+
+ - Docker
+ - Python3
+ - Python-Pillow
+ - Pexpect
+ - Chromium
+ - SeleniumHQ
+
+## Usage
+
+### Within Fuego
+
+For using it within Fuego, simply add a `fuego-test` board and associate this
+test to it (`Functional.fuego_release_test`) with the following commands on
+Fuego shell.
+
+```
+$ ftc add-nodes fuego-test
+$ ftc add-jobs -b fuegotest -t Functional.fuego_release_test
+```
+
+After doing that, use Fuego as usual to trigger the `fuego_release_test` and
+check the results.
+
+By default `fuego_release_test` clones Fuego and Fuego-Core from the official
+master branches (default specs on `spec.json`), but `test` and a `next` specs
+are also already available on the `specs.json`. You can always edit the specs
+to add your preferred repos/branches.
+
+### Standalone
+
+Sometimes it's useful to execute `fuego_release_test` outside Fuego, with a
+Fuego version that's present locally in your filesystem.
+
+For doing that, simply execute the `test_run.py` script pointing to your Fuego
+installation directory.
+
+See examples below:
+
+Test a `Fuego` version that's located at `~/fuego` and use the current
+directory as working-dir (location where test assets will be saved and looked
+for (screenshots, for example).
+```
+$ ./test_run.py -w . ~/fuego
+```
+
+Same as above, but do not remove (neither stops) the `fuego-container` after
+the execution of the tests. You'll be able to access the inner Jenkins (Fuego
+that runs inside Fuego) through `${fuego_container_ip}:8080/fuego`. The
+`fuego_container ip` is dynamically set by docker, and is displayed in the
+first lines of the output of the script.
+```
+$ ./test_run.py --no-rm-container -p 8080 -w . ~/fuego
+```
+
+Output example with the IP of the container:
+```
+...
+test_run:DEBUG: Container 'fuego-release-container' created
+test_run:DEBUG: Starting container 'fuego-release-container'
+test_run:DEBUG: Running fetch_ip()
+test_run:DEBUG:   Try number 1...
+test_run:DEBUG:   Success
+test_run:DEBUG: Trying to reach jenkins at container 'fuego-release-container' via the container's IP '172.17.0.2' at port '8080'
+test_run:DEBUG: Running ping_jenkins()
+...
+```
+
+Docker commands will be executed from within the script. For that reason, you
+may be required to execute it with `sudo` or as a user with root permissions.
+
+## Test Classes
+
+Fuego Release Test implements a few wrapper classes on top of SeleniumHQ and
+Pexpect. Those classes are used for the instantiation of Test Case objects that
+are executed sequentially through `execute_tests()`.
+
+A few examples are given below:
+
+### Pexpect Commands
+
+#### ShExpect
+Execute a shell command expecting its output to match a given regexp.
+
+```
+ShExpect('ftc list-nodes -q', r'.*docker.*')
+```
+
+### Selenium Commands
+
+#### Visit
+Ask for SeleniumHQ to visit a given URL.
+
+```
+Visit(url='http://localhost:8080/fuego')
+```
+
+#### Click
+Find the first element containing a partial link text matching 'docker' and
+click it.
+
+```
+Click(By.PARTIAL_LINK_TEXT, 'docker'),
+```
+
+#### CheckText
+Find the element whose id is 'executors' and check if its text matches the
+given string ('docker.defaut...')
+```
+CheckText(By.ID, 'executors',
+          text='docker.default.Functional.hello_world'),
+```
+
+#### CheckScreenshot
+
+Find an element whose class name is 'page_generated', take a screenshot of it
+and compare with a reference screenshot, given a comparison mask (B/W in which
+Black regions will not be compared) and a threshold (percentage of non-matching
+pixels that are tolerated)
+```
+CheckScreenshot(By.CLASS_NAME, 'page_generated',
+                rm_images_on_success=False,
+                mask_img='screenshots/page-generated-mask.png',
+                ref_img='screenshots/page-generated.png',
+                threshold=0.1),
+```
+
+Take a full screenshot and compare with the image in
+`screenshots/full_screenshot.png` using the ignore-mask
+`screenshots/full_screenshot_mask.png`. The viewport resolution can be set on
+the SeleniumContext constructor.
+```
+CheckScreenshot(ref_img='screenshots/full_screenshot.png',
+                rm_images_on_success=False,
+                mask_img='screenshots/full_screenshot_mask.png',
+                threshold=0.01),
+```
+
+Note that the test by default does not store the currently captured screenshot
+and the diff image in case of success. If you'd like to see those images, set
+`rm_images_on_success` to False, and those images will be available in the
+working directory (script's -w argument).
+
+## Helpers
+
+A `take_screenshot.py` helper script has been implemented to aid the generation
+of reference screenshots (used by CheckScreenshot tests).
+
+The usage of the script is documented on `take_screnshot.py --help` and a few
+examples are given below:
+
+#### Full Screen
+
+Take a full viewport screenshot (default resolution is 1920x1080) from
+`http://localhost:8080/fuego` and save it in the current directory as
+`full_viewport.png`.
+
+```
+./take_screenshot.py http://localhost:8080/fuego
+```
+
+Take a full viewport screenshot as above, but sets the viewport to 800x600
+
+```
+./take_screenshot.py http://localhost:8080/fuego -r 800x600
+```
+
+#### Element Screenshot
+Finds a element by CLASS_NAME (-l locator) that matches the string
+'page_generated' (-p pattern) saving it in the current directory as
+`CLASS_NAME_page_generated.png`.
+
+```
+./take_screenshot.py http://172.17.0.2:8080/fuego/ -l CLASS_NAME -p page_generated
+```
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 11/14] Allow Full viewport Screenshots
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (9 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 10/14] Add a README.md Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 12/14] Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7 Guilherme Campos Camargo
                   ` (4 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

The locator and pattern arguments from CheckScreenshot are now optional.
If CheckScreenshot is instantiated without those arguments, a full
screenshot of the viewport will be captured.

Additionally, viewport_width and viewport_height have been added as
optional arguments allowing the resolution of the viewport to be chosen.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../screenshots/footer.png                    | Bin 0 -> 7371 bytes
 .../screenshots/footer_mask.png               | Bin 0 -> 302 bytes
 .../screenshots/full_screenshot.png           | Bin 0 -> 46323 bytes
 .../screenshots/full_screenshot_mask.png      | Bin 0 -> 10717 bytes
 .../screenshots/page-generated-mask.png       | Bin 183 -> 0 bytes
 .../screenshots/page-generated.png            | Bin 4583 -> 0 bytes
 .../Functional.fuego_release_test/test_run.py |  52 ++++++++++++------
 7 files changed, 34 insertions(+), 18 deletions(-)
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/footer.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.png
 create mode 100644 engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_mask.png
 delete mode 100644 engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png
 delete mode 100644 engine/tests/Functional.fuego_release_test/screenshots/page-generated.png

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/footer.png b/engine/tests/Functional.fuego_release_test/screenshots/footer.png
new file mode 100644
index 0000000000000000000000000000000000000000..e53208439951e6f0ce400a2eeceb30f16522b7e2
GIT binary patch
literal 7371
zcmZ8m1yCH@wrwl{g1ZF>?ruRskijK52^QP}g9I3K0zrbiWw4;Zli*I602$mRxCLi$
zmp||S``)Yir*?In>h7vOed_GB*IGOBm6kF-E)6aK0QjmZin;)Rm4}*>Vxyz3+l4Y~
z06>?dswk%q$=YA=(I<U92OLTzqoWnFhMK?o@QE=mpF=m5%Z&KvVvDhn5BYa%WS^A+
z7I9d&TD4&m);A#JbkSKwMQDiuZfA}C$#&`PJP?V53~AXNw4DrOBXT?$4(1unD#QRe
zBFulsDLDLo9PQ<QZ(!bWWGsJR#18o<wk!zQB>tJJVDG6vyqAz&YA~OuDd9KdF&o?n
zYMY9`FotG7X!_WEkdnB}wvgeR>J<ldSuB?u3I0xKf8#Dta!_r&w7Zdd`^O;249a`z
z-}54!Q>67iWZPfGp%U5l8GIkLQjuoL&EZTo<JiEpZf3df13EgfH14oSTST`wT^HBK
zyo2IDj=T(<{P(&#<^qtQ?rv59aCGD>a97$5nEu_@cRMb3r{OwOL-1&ClD_E96J{$w
zrFHIVjeknOAQj5^`$F_sS#jc{e>@i#S5aY+oy7A281iGDijmQDoQqU6ZZuVlD?a`i
zYd<Bmc9uXVC)0uA3qPVkXPbinFc}C9VC^n?eDOGdi$mYQ;Ao-M99lsi!g%I&rcB82
zaQUpdsfl&fTq2t<LR|N@X?^`@oI!RyGE%3a#H7-=E=J|n3eW5L)=um)5np=$+zI0~
zp`)YYy^!UX70My3<DPdN7EO7TmEF<OoPx8^>Gp1Q%Ig8xN9ErqNATX{-r=;=G)t;t
zDZ5j@CDAgcGT_7S7M;;}E8Hj1bape~#htkZ(j-#7TknIP0x>EqT-@Be#?r%npAFR2
z%ahy6^OII94{0wWC8eay=wv=AuC6uSpvP)7)XMrXy+r-Ty`c2oK|keoj=6Ay>z_rR
zLuz8LmZG||z9Q%6V9(ii_08d*%Uhz`KDs~~l)e_%AC#U{y9wgSeULV=3eTK+lp6_o
zRkaH~iN6}`Db0PDB$n&EOS$b&tmZB=Vf}?DP#(Fcs5kZ5((w+;e%XI8Rd8NWPt@2M
zGv621k}r0tVY+JfiNVQfH%AC51OSAcv+~Y^@2t_$+|4QLe=Yn*n4ZA+*bjVHB5Ah?
zhy94LWi1VVstq4Sc_hg$4#jhqyFPbkzMG6tdz36}S9n_wN*0|gMJIe~)786t1Lks?
z>jtT&k%qgPNP{#|epz|Onfm9PmsPZXS1{9Y+NZOAvi1d?@Zh9jBiQH1BKZ{e$S-8>
zeNNR!9D;vxfyA|U$f8)Yp{%NGcCCS*<M8-zB`vhictbXF(C!J|?*~`MQf-8du9Baj
zf<i$Sol8O;=IT2qC-Ww|t3zVAy1JSVa$X_1U{-2|B=T3&N!z68M5LmXq1{xK#T}ek
z0ZSL>mWCFFN%Os?0S&lq4%{RgM<{D!{_6gPod3RSH{-?0$%zSK&(t{0*l2fz8{R9O
zUonhK(D-isz7N0d(QG#{&^O4+dU`cZ^n_v%CbdMn)b1-I!Y*Z`rd9?P7%w()xw0jF
zk-(OWX#MGJZ@*c`Jsp8h%O#?xP^23uUYTn2YWdsjmuHqY7xa=0kVhaLn%HKyquXDh
zg=o$CuR+f*|Jo8nkkUT&ad=1+w0;j#L?r|!`sd{A=n^koy`0~pj)R)K7`1S4$kFBH
zx&!c1RdxFZh?}8DF%e+{ymWM&{Y<`VFCkT7T9?JgZ**C@)pM2r)^uB5<+~nnB)RIw
z=f04c8cV1X-`C{cGqYZKD@lT6B?A#-u9Hn1JkgNalX6LF_b_?9yFDiq6OD!Rfj3eC
zZ+8)xURb5%H9f?yLSMeT@kX|-ORKsk-XN}`&G4@Xdmay^CX)|yel_!4K6*t?-CMep
zHhl?`$h<3ocM5cN3!rn&fX@bFMw!>A{4^7Ym~%U_o6bkpdN*4mE$-uWd_w0VdA&ub
z)%2Vj9>P*OTdKt%v^9Pb*e~PW)#?_ivf7imNR!Zd_30^GMOS>62$LRH>a{{&bG?l~
zKB4_DcuJ6gA{Fz?K1Veb%_i5D$jI<OJ3cg2Fa4OM%N{KdbD7QiT8IsBa9szWwm0e+
zS0>Pmnwr{?dfS#eEsukSmK1v8r(&&;oL2w$Z)JNQS0DO5J1g5`EOSS&b4^{1?~Rx}
z;zt-MJ2eJimA@trUUd2jo|vF%I!o77>r*RYwRJYEI?33$-$R_#pL-jL+iO>6LYOhg
zv{coezOb74>q2|6xA?%fX$GEwiF0sq=1992mwtD=sn1qqa$i}!@;(>4^1a#|GQBm`
z*C^?)eA8kz4`IfXla!Q<!=xMTgMdo<Gfh4p-M?q>6V`z4QDu)x2KL5&A9Ww9(xT=U
zUbQA|ZIvO$<mBeHXmM&QNLDcX%dJESpY;ldMR12OUKG$v2Ohuso$&1H3X5KNA<HZq
zQpx+e9(yTI*H*Jc3vrVHwg)S)CCBzquL^RMa|pj42)|b1<Se~sbLX*W(*H&*1tVm5
zF*P~GZ!8io|D|#GeRNFY#qF#*?VO_s&1CmrQ&EvYUR~MFj_9tD4hNZmXethAPik5U
z?tld&No4DUMu6#R2<YT+e_~z4kg&h+56N&i5jLiDbufT=n7!gLV&MO{rDDLSRR+>$
z9T%S-6C+}lqm^E=CVWm#Mn<wqawKF&p@#-+Z)egGC~wuBSTQFf_*{o5hwjWxO+$vK
zGQ}1o`NZV3p?D#TN8fS8>c`>o@W&3J3{m+^OiYk|0F8wN=Yb0H!r_=KS#;NMrA=H(
zTYUL<uw4D@1(^+#<$#K>jOJB*E-eIyfGL<!p6z%kyLI)N*lA2Jb^;qyUq5u+PBg0H
zDSA{M2bVnC`O|z1fNCITW)tLZ2t!JwrkEI)i|YnY3q<AHkXX(;uzu8u%2k5}_02ir
z51QS4%&}>!CL?DZ`aTXr7RKlQo?62hOIpuA{(D`_SNe<zt$jRYJ^XOGkMHXOgk=g`
zlvx`5d0{7^fVN~iE@3G^L`&(-si&`cpkB!YQKABEMe24^Hr^E2K3T|vs|nEgLfG_U
z+VN8cc6D|?akXdEP8|4i+@?LDsTy41n{}k4!V~7@<M9|DawaB4u+-hv&^0!CJzaEq
z8W|@SxT?ZMwhgYOp3UXPt0WnPipz7Z;UnTm2HH1N$NKK~=2K&NXn+77DJ!Wh3+IMG
zB>>>y`t~MqLV{n5HBTS3-MyO6t73sagpE%pO;0Dq`<q`bnBDRBzf|}?0yF|OHZn4@
zQ%Z}95(>A}(_mkU!;Wob&??Z;&XIs`cs!?zTLRx3PEinA=S#QQv9htOVJyd-4&-(A
zOZhzbJ-8Ph{ld8qy20wnPbE)9dmQI_XR9{>$9`S5x0yeDd4HNt-C@yMYrV;TK@ZXp
z%G_%3<X?PiVj`8P*bdpA&-7$>#f+GknjkRE=-)6WxWAh=o6{l7cCx!RnqePyYs|u@
zo$k}MC498kM(W$Y__MyL$#%DRe~3>@hY(qHVz!2CX!!me2XMO|08vDf(^EGi^B&fe
z1zVS`^R1sek-Z2s_)=edKDgU}Dh4Ln4RO(to8%78&del{!RbS}(*DvS>)z*crnj%`
zdbpL@BVY|7S2eV^0(du<&%iSR_=`DS<$+J%Wu^1JaTM|$`?k`>{^E2F8(mFJZE`{%
z1Ogo?z?Jd1c{!iv9q?$!U(R4Y`lzF0o|t;(cN^-C4o*B=x7idE71I=jg+!#Jo?Um>
zq_@&Tu9y}Ux;<xC4WNojM~n8H9G9o(UiGkuCl3Y!R0|E@1r)-;W$Wwf{Td;E-bm%h
z1O~;$Py4i<ouG1?BD(0<K4`FO?6$jxQiH9gxVgBwxo70}a?27+m%BvPboHX~o6HmB
zZK7C{>E7LDb5_V31JE;1OGuAH-zK?u4o*J=W;8x%10CmFH4mUDNhpTJ2ngy2X1p1>
zP_6pvc=yV<yqNJ!!3+aM)8QYGiBzEHk)e6MKmE;^NES<M&WjPcHGSv-B}=kvMN%R3
zR5{%porC>ZQTP$o=mC|Q_!C%?!o1~s%YM5z_N}K%W6+KhT!4uFCV22C3I8yZqSfu9
ztkh<V@`P$nLs{BiZ*i&{6L4!Zo<CuFtfz^`*d9@^Vbg8JH-x{qOG2LuOL`sUqu^nO
zXctWrqzn|9#H^u3|MA4hVT`PdtO(3sWRv=ak03{YeQO&g#D62W+^_x>d{9>5JgrX}
z(6G__lbY@yy3aC?ur3+SZ5mlz6ntZ3zkS?IhHKj#D;nf*-_@i}QPn9>!Pt>kQ{XxE
zj*tw5AB)#R8xL1zCOnY!@ba9Rpu+(UPe1!|vs4r}E2+MY6gO1W^c7l!WZfIfkbz{{
z?7}0??#b`&+aeC=tKPi%j2$xYdEXsX#5BF|fe$Jy&4cgdboAr+#NTHqS+3aoVqM+i
zGGni9i;^-#B_nK1TDGBKZ4RKN6;oaLV~(dTPq>BAgWtZD@U93BQt~U#ZqwAY>O<7@
z{z8rJ?rbx=!s5b}jp_(f@;}z!1&<UW9PyqO3)+1873LXyb(LFN=l_6?d@)WAFmiKS
zTTAf@K2z1UpacZel;=9#c($O@bYzgsmy9#dXO72k{RuC9yx$iVOA`}6g9=jSLm|xd
z=VBaDLd{fGt5&TFB&(}tW-(}nB3U+8t<w*k@74W*59KZWzf^nf!f0rp+FLrZDQ&w1
zjHltZDxl;?t+FEmF^Y@(B3s&<hVfHPKOUx>7M)C;-+~&74K5MHDrx%PPq(lqB_#!?
z^VinaN(WKN=z9KoTSaMPX0R^_|Jc1Fr(<*Whn%a7i<cJ#ZK|AzQER1+fE;r}Lsv`3
zDT{WyLn^#Su)$)44pcxVq{iI)>5|*`k-xbq-^B$)m-|Qq=6ZFYshgKN3LAV#@AfMW
zpFsKd%0}_gsps`!$q1INw?iKMoWtJ7*yvG`T2Fliwzi9X+47m%6xK3ft{8wMwG4rt
zvZRSqK^NPHfaH*|@cbWtoo`HWIPw+KPk)T7Yw(NERucejLeqt+dH_%y?!*qZdkbIy
zXaGwI<x*TA)Vn2n$jxtQTq@JK5Ag~BlJO_C7c?FNM3D}j>PGIMn)6ppqwo!^KJHOg
zuW;RDVKp<JTJygtsMQ7XUfm}B>x=#Vriv<<f;wY#Qf-%%xdXlBXuw{H6W0h!yPt6R
z&YyD&zaL)rHt2FHI;4TAbISG<Mq<jcy!uJW%ltogR^C0Q*c1#rb(_rXC?nQz`$zjG
zxT&Y}3)KiDr_hBE$~~tWN>!g8)6ISFB}jzn=<K}aCJ~FTsn3-vu2wj{eW~Q4xqsus
zkk4>2<oA-Ou(b3w>ikbb6cjK}+ArQ78xuYCPm3wY#cRKp(6==T6a5S76?u7ijTm{R
z^SDruq9RpH6&kL(%BSe4Q!U+PH;MG1j^k5A&EhwcISIPB&MvyTx;o@iV8&(}5kr9x
z)k1S~^Zx$+!omVB?(wA-D{FUkobRgDn#=w>0lb2-uaCm5tqZK{YL(Cd4l+U9u#oh`
z)KpGh-ra@bK7x@ygS%g}$8vHc0%OEi&;dqXURGXS(O&y=X=E4*uzRPIs5F=%0GXhg
zkv%2SLD{}U$YM(MrH{{r-I7a<-EfxZTtQvY<PzB8aFX_fa%dk$wsmlzrKvjS;}$=X
z($A5-7~C9Hre@|6sHVGv#~#reZ$(B%c6dvm<&c9VM?@2=R-n_iIh46ux9qB)k&$7%
zWED8Nv2D+vMM6%Vm_lZnHP&PIXRTMzw#e51eE;C!fZ|QI3d@h#cX;r_A?mk+l(=E?
zOo=o*w7y|b#V+K@x=lAjF}dme8u`a=diiH-H`D&KCfao24g7plHz0ut9gztgOrZ6_
zwBz7yZEMA)u1`UA_$#xeoE($yKYOWwfq?l}4=1IL)oLZ0G~o>R^ZSJ_Qxm9}uGdOq
z7sn^PzWGTE?aCj;^N3MGKVkrkGKCjEDV`-$E$|#}-Vyqy2oKt<f3@_GXHK}TN#n@;
z_4XV9>@=qx-`QT$M_LQK9{T&od6O#nlZA9Xfu87E4(0Pq=NDQx>k?ipfQnb3R87QM
z{WCly9|f%Z&R*X`*8<w<L@SwoDQmoL<=`xdPCm6}LlVr#t(8s=cCy`*%q6Mye`Fpr
z319G#)-sIx5g56xjrfG~;lJus%Tzw3PaHiyGA{6LXK>q--PWeixy)Hwru8T>z}lKQ
z0{)B$n<{*Aa<a6v6dl+)eve|s2f_=7&4Il^`dnvgz0!aHKqLlR7#aD1Ea4v6r+a6*
zV0Ne@V*b6hP{Segj{L8zk;Z#E=uD0M^)iMd8OWE}Niy5j!Degofi_rWIX+FE*u~Z9
z=X8^U)3vh<`WJ=S`79rD^3(=-SLOLgOaPTkFDL`eoBW=KTjD7hd3thZYheKThK4$&
zZF);Lv@36^C@5&|FLee=g?k-DxE_L9aoc%7US9a^X_)E#`>^*!e}0dAp|Q2y&|uTk
z*C&I2%kN@JMkHTnwO@3^;>T82y13Hw<)a2}{?s^6GKt>bp^I&)?PsDJk%cK_NO9%P
z+*w@n+E#p~#7`ah$CA66D6-db!UMc?b~e%SKG@8+>X7NcN!b+b+VGH)k}@%Q^Eh*v
zkqBFa#$XFGHj=HUH$J;?bbUf};`jtN)X?ZvLUmc0n21m({A*c7k`YlBO{Y}imqa;K
zWwOyrUZjme<6~Qo!y}3O)cnS&#E#q|GN5L0j%taM^I6%;<YnmF!xG}9FTxtGWr<31
zk;~QPxMu=&0ARE5)8W=yJUNhGbg2)*aj)Zz9mWN^YX2RUl%AZB!nh<WV}D4U45uw&
zXuM_FIw&tXQ2JO3Vj~uZ(1H)e_^KStn~<YhEAL?l_|+2)n8>a1;YnHZd1!j~7qkOx
zbh34pLYCjqtgHo}p(oYxf&lR7I!M_WCP|5m^QfT{opv8c_X)k250PcS?A)*-_u9Xl
zPoWpHpF=>z9!oVl(h!sD9`*hd{kJpd@Jdpu*{1Y|kkA;)mHNaHaQIx;Gqf@ESN0q+
z9l<7waAt15z6|Q6K3{2IK7R4nl4@#nH1Ts5+u?<;u;=)|X~kZ{^LJuW(tED{{*T+C
z;mHa!l~TS8Cyl1A%F4<pPywFd7rWUjv!^Phrlobk|MCLQczMlUzYYVN7-w$UY_hPj
z))@0pM5o?~J+@eb4+QMAeZ4W|OaJ*+y7eJr+RoVF^2P^=RmCC3wm34f+;^6jR~{^9
zgi&oyX!YGs$lT6~>Jia;Us1V?j7;te@}6#%3=@r~5NT3!U;f+kg^y@AEJZAt8f>t^
zN5J~#MoE#hql3dc2ZvVg4L>h~!L88(RGn~e5P`ijFgN5IQCVM*y%x)tQB#l8Y9XfU
zdMiLb?>{s3M-1_ciXlokbAX=W3D9YGA&{v7d}s}6jfr`$zkZjKlM^1^Q}~N4_6v<X
zMtp2s&5!%l2$gZ&9F||~t8LjC3Hmzbqoboo*ORc3gm0>Bq&j2^t%Kqc>$r<X*VbqA
z_RUpGjgNql*;(He)3=_UPZm|Qyj|7R)n7Ya9Tl*r8l)WZV890LgfdO}DAzoE)5Rbx
zB%I4zKeL0RutS3^w!YQYHwJRIe*5;drA11ZnVVCvdtjipqf6b0C+Ph23dN*0Y&Y;8
zF^gX;@S6nP6(?N3S+^oP7AUDJ@Cm_z!7YExT*^|wFaQ+<R}<+&ds$WXBpiXLRvo&n
zDB+aNNKd0LCa*ZAgKjpm-y_$><|x5^pZ|mo+g!Q7+0nOcUO%Gcm!#$)pycLp?&JLW
z6tb`T+OdySw#1+F3MJq+2&^*4)1Pm!rdCotDrDhg5mIBsB{;Dy;#hlcaLZp@_M4Ep
zm<a<I(JkQ|XVngCeTTF;&L{brnrPzAEvdYG8(SIE9duC<6LBRGXL;t9!6Rm#S5|fR
zyKa+?;1PfwjTq#2aoFD)8Y*uh%6YD1gd+z;MDI(aPRviWfn%}K`)z;B(D8*ENmJbB
zM+{;CZcS9l-FBW$RGkp<Ygp%hskPz~absJ-K5QDC#$(LK%q@&4^x=KPl-THx9QuHv
zTy54=CF>m%Rz!Ud&qR4P{1#0t%blY;J2dV%?id(%rGG6n5QaJiv&a2CA3iEe{h4#w
zcbS-+Jd8@#V5>5o^b%QCG%%>kXv|1V3=T_e8i71|J+n9}#x3l;-9<hoG{%<9etci`
z0^EMFt+yG+@VqWyQkUJn7v4)~CVg>Jx@NCKHaa$Sx=>3Gd~@25Du~doF#hpISIwhu
z0zti<30cV4G8T0It|M6hVYd>>EJc)>Kxa|5&yn|^8O``;b>mt{@Or{CC#N!DGy4L}
zuCDGAe7xRO+iP{t(4{z3W@Jg(qusmzC`Ghm++#<Dhli3w`A`*zMq%=T;ZH$XaTKG9
zLZFOh+>}p{94glXX;BYv2oLZjAS^s0rmtT+BGI6?_yl4wnZYgs{^$P1gaD)DeQ;yr
z<rn71Lpd){ZWu?V295Zl!}c*KFc}@imu6?5(~4DXEM=yiHJ8_nkM*Gg33p-_d!;8X
zF8l9&o0Dsp+pVHQ|1~hpGZdLn7LYVaK%!1BH315^G$?jw5=m3Z4OUmJn*Ew12pBs5
zm+y#&C>-t~;E&@sF9*!qJvgEEOrH*`Huk%XBB(#4H`r@y4}<6KKGVQbQpl!Vja@_*
z52MeoJaABo$DTi@*<<_9F>5)>IY8Ox(Xf-w{U^PYH92f%&KzewAG0QF*a6Hv$@Cw)
zrb0m~CLg^o8rjXlJ;Yb;nnq&kwbJMe<`>j@dv%cgd_*4MSs~3R5TEsKZgr%gAPq0$
zR_m3%u@MK=<Z9jIsIM3(>a4FD&cTgxXRC2hr+Xr`&~P{h0Kx-aC;3oFyEa!`l0!E^
zEeFInU*iktEx=G>f_AglO9xQF%F@(lO3x%=&YAJ!TeLCR+n+47*{d#cE{*i(_TsQ)
zbH9tFAXXhX)GKpbV))Uzq~x@MTz1My_*(>T#o6`^9Nj_vMN!k~OcrbYU2%{tf|6-*
zAG40!Hp>G=bVmJ&IxJATtCQu3%`YByhYFZ5v0TFE=K7h<FgiY;zFTfccjc3k0oa%6
zkuk43xpSv#92TZ)p%SavoX3#A?{Bm61ev$R2^&do)}0^iobVaWEvx@y*$_=I;KA#`
zH-k3Eh16OEA8)+~TaC4~<0Ja_YACfz%SvzWEbmKJWJ&$Scj`NxKFg{@X3WroKk!@O
z-l<yxZ|AGE4K5#_r3;hb!oPhAjVH!IRwI3wZx4m49@g3&1<52nV*Jw!3sP|%lCmYY
zrKa9a=O44MZ<}3i@lIg=g3=9mcxRD{t0<QJ<A)GeSs&Er>qDyD;Q3{V!iOOJsg^!0
zse(sGVm-1w;ILU9+J&D1HG+7zer%jvoP_WX?Y-R{vb7H$4EctH?E?Q%BqM~J+}uk|
z@c8bXFQZ?j?z~SMD6B1pbEM}wHi7Wq73T1}zYS~1OU>X1b|Or)k2k7DThMWc*~SGl
z04{O<P*aB5V0%z<Z`r<yJtsf+y>`S&2UdGN`oDXBm~tZ>^Z`%*)9S+x$%D%LzvdtE
a0jPam#CfG1K#X!f09BBdVwL>ckpBUQjUv+k

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png b/engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd99b235ae55587cd82ff9e23a783195b84e5a24
GIT binary patch
literal 302
zcmeAS@N?(olHy`uVBq!ia0y~yU|j%Y3v;jm$+Tmuwg4%%ByV>YhW{YAVDIwDKoQOY
zkH}&M25w;xW@MN(M*=9wUgGKN%KnH&SVUO;|Gtf9fkL}IT^vIyZoR#+k@tWB&w&lQ
z{{Me7XM(2#+b5RZS$FU91C_`b87ncI-+cAKyXin414Ba-BQJ<{IMe`g0)s-L12c%8
zV6cD@M0-eF08tDoJQ-kGh|L5{b23YT=!O>RXn(|zc!pgmh`TZoWU8mDpUXO@geCw&
Ca6(=H

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.png b/engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d606a993c2ab3df523d4475e9723e2a917a75e2
GIT binary patch
literal 46323
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
Woe4X*SGH}+Ay1<IB>e2cU;hQcM9<Fv

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_mask.png b/engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_mask.png
new file mode 100644
index 0000000000000000000000000000000000000000..85d359440dc79d220c38eff2ffd21fda8efa0851
GIT binary patch
literal 10717
zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4u_bxCyD<C*!3BGlPX>x`
z7I;J!Gca%qgD@k*tT_@uLG}_)Usv`=EW#oRlBc$u3}awWVDNNt45_&F=BA_I0Rx7^
z8%qCJF1L6!S>ea)&AIYGEg*3Iw<?6ekYUik2w^JmBtRGq9Lyl2fk5F91IS50&@rkE
z6bz#Q0ty9&(G&s>28Pir0}hAL!T}r%qlE)F7)A?+(ZXT0qJ*S@(Y!F47e@2KXkHlY
zK#le&fw^F`zW^=@M*9okU>NN$fP-PQjRX#c(ZT@|48#=<54PnWS7SKhz{6ts<6pGh
z^Z6F0r+qlR#2E#d3?Sw>v@q~MSOSU+%n+6f#{md~VN@A7AVvcO91No=1RM;bSq4%%
zj1~@~g~MoG7|ja|qxHgQy}&Re+enc|C2s<k48(!f0)XOhG$oIw<e{08M^o=;a}=C+
zhh#ZCntBIy>YdLCPFnYjjolc2+>BK|(8@kKd;|=V(Yyvu7x-IEM;L9V@aLJl`+5PS
N*wfX|Wt~$(6985ZI<Wu%

literal 0
HcmV?d00001

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png b/engine/tests/Functional.fuego_release_test/screenshots/page-generated-mask.png
deleted file mode 100644
index a565ba4dd4b8cbd5ab4be9c7050d8046f63e238a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 183
zcmeAS@N?(olHy`uVBq!ia0y~yU{nCI`8e2sWPi}XA|S<<<n8Xl@E-&h>|H(?D8gCb
z5n0T@z%2~Ij105pNB{-dOFVsD*&ne8i79f=*0Tx$3b}Z?IEG~0dwXRgFM|Qk;SIa~
z|9=xV!PeoVu!%-)b%bgsN6kYo1_^-#2L?7434sPiMmClN2OtF(I`D*X)n68A7Wv8g
QK=T+pUHx3vIVCg!0HVY$sQ>@~

diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-generated.png b/engine/tests/Functional.fuego_release_test/screenshots/page-generated.png
deleted file mode 100644
index 9b16d92e5cc3ec6c9f57723115e21f8f23f5eab9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4583
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
RPL==w002ovPDHLkV1i3q$F=|f

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 5fa2ca1..47829f3 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -96,7 +96,12 @@ class SeleniumCommand:
         return element
 
     @staticmethod
-    def get_element_screenshot(context, element):
+    def get_element_screenshot(context, element=None):
+
+        if not element:
+            return Image.open(
+                BytesIO(context.driver.get_screenshot_as_png()))
+
         location = element.location_once_scrolled_into_view
         size = element.size
 
@@ -153,8 +158,8 @@ class CheckText(SeleniumCommand):
 
 
 class CheckScreenshot(SeleniumCommand):
-    def __init__(self, locator, pattern, ref_img, mask_img=None, diff_img=None,
-                 expected_result=True, threshold=0.0,
+    def __init__(self, ref_img, locator=None, pattern=None, mask_img=None,
+                 diff_img=None, expected_result=True, threshold=0.0,
                  rm_images_on_success=True):
         def add_suffix(filename, suffix):
             head, sep, tail = ref_img.rpartition('.')
@@ -209,7 +214,7 @@ class CheckScreenshot(SeleniumCommand):
         LOGGER.debug('    stdout: %s', process_stdout.strip())
         LOGGER.debug('    stderr: %s', process_stderr.strip())
 
-        RESULTS_REGEX = re.compile(".*all:.*\(([0-9\-\.]*)\).*")
+        RESULTS_REGEX = re.compile(".*all:.*\(([0-9\-\.e]*)\).*")
         result = RESULTS_REGEX.search(process_stderr)
 
         if not result:
@@ -233,10 +238,12 @@ class CheckScreenshot(SeleniumCommand):
     def exec(self, selenium_ctx):
         super().exec(selenium_ctx)
 
-        element = SeleniumCommand.\
-            find_element(selenium_ctx, self.locator, self.pattern)
-        if not element:
-            return False
+        element = None
+        if self.locator and self.pattern:
+            element = SeleniumCommand.\
+                find_element(selenium_ctx, self.locator, self.pattern)
+            if not element:
+                return False
 
         screenshot = SeleniumCommand.\
             get_element_screenshot(selenium_ctx, element)
@@ -251,8 +258,7 @@ class CheckScreenshot(SeleniumCommand):
 
         result = self.compare_images(self.test_img_path,
                                      self.reference_img_path,
-                                     self.mask_img_path,
-                                     self.diff_img_path,
+                                     self.mask_img_path, self.diff_img_path,
                                      self.threshold)
 
         if result != self.expected_result:
@@ -573,16 +579,19 @@ class PexpectContainerSession():
 
 
 class SeleniumContainerSession():
-    def __init__(self, container):
+    def __init__(self, container, viewport_width=1920, viewport_height=1080):
         self.container = container
         self.driver = None
         self.root_url = container.get_url()
+        self.viewport_width = viewport_width
+        self.viewport_height = viewport_height
 
     def start(self):
         options = webdriver.ChromeOptions()
         options.add_argument('headless')
         options.add_argument('no-sandbox')
-        options.add_argument('window-size=1920x1080')
+        options.add_argument(
+            'window-size= %sx%s' % (self.viewport_width, self.viewport_height))
         options.add_experimental_option(
             'prefs', {'intl.accept_languages': 'en,en_US'})
 
@@ -701,7 +710,8 @@ def main():
     if not pexpect_session.start():
         return 1
 
-    selenium_session = SeleniumContainerSession(container)
+    selenium_session = SeleniumContainerSession(container, viewport_width=1920,
+                                                viewport_height=1080)
     if not selenium_session.start():
         return 1
 
@@ -713,6 +723,12 @@ def main():
         # Set Selenium Browser root
         Visit(url=container.get_url()),
 
+        # Compare screenshot of the full viewport ignoring an area
+        CheckScreenshot(ref_img='screenshots/full_screenshot.png',
+                        rm_images_on_success=False,
+                        mask_img='screenshots/full_screenshot_mask.png',
+                        threshold=0.05),
+
         # Add Nodes
         ShExpect('ftc add-nodes docker'),
         ShExpect('ftc list-nodes -q', r'.*docker.*'),
@@ -746,16 +762,16 @@ def main():
                   text='docker.default.Functional.hello_world'),
 
         # Compare screenshot of an element of Jenkins UI
-        CheckScreenshot(By.ID, 'tasks',
+        CheckScreenshot(ref_img='screenshots/side-panel-tasks.png',
+                        locator=By.ID, pattern='tasks',
                         rm_images_on_success=True,
-                        ref_img='screenshots/side-panel-tasks.png',
                         threshold=0.1),
 
         # Compare screenshot of an element of Jenkins UI ignoring an area
-        CheckScreenshot(By.CLASS_NAME, 'page_generated',
+        CheckScreenshot(ref_img='screenshots/footer.png',
+                        locator=By.CLASS_NAME, pattern='col-md-18',
                         rm_images_on_success=False,
-                        mask_img='screenshots/page-generated-mask.png',
-                        ref_img='screenshots/page-generated.png',
+                        mask_img='screenshots/footer_mask.png',
                         threshold=0.1),
     ]
 
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 12/14] Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (10 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 11/14] Allow Full viewport Screenshots Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:24 ` [Fuego] [PATCH 13/14] Improve logging Guilherme Campos Camargo
                   ` (3 subsequent siblings)
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

The compare command on ImageMagick 7+ supports the --read-mask modifier,
that simplifies the comparison by allowing an ignore-mask to be
provided. ImageMagick 6 does not support that option, so in order to
ignore regions we need to compose two intermediate images, from the
reference and from the test, in which the ignored regions are painted
black. On this patch, we're implementing that logic, allowing Magick 7
and Magick 6 to be used.

The reason why we're allowing Magick 6 to be used is because Magick 7 is
not yet officially available neither on Jessie nor on Stretch, and if we
wanted to install it from source on Fuego, we would need to upgrade a
few dependencies (as libc) what could possibly end up causing negative
side effects.

On the other hand we're not dropping completely ImageMagick 7
implementation because users may want to execute the test outside Fuego
(as explained on the README), taking advantage of the new features if
ImageMagick 7 is installed in their system.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../Functional.fuego_release_test/test_run.py | 104 ++++++++++++++----
 1 file changed, 83 insertions(+), 21 deletions(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 47829f3..a36014d 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -183,10 +183,44 @@ class CheckScreenshot(SeleniumCommand):
 
         self.test_img_path = add_suffix(ref_img, 'test')
 
-    def compare_images(self, current_img_path, reference_img_path,
-                       mask_img_path, diff_img_path, threshold):
-        cmd = ('magick',
-               'compare',
+    def compare_cmd_magick_v7(test_img, ref_img, mask_img, diff_img):
+        """
+        This compare command uses the --read-mask option that works only with
+        ImagMagick V7.0.3.9+. That option allows a mask to be used for limiting
+        the regions to be compared.
+        """
+        cmd = ('compare',
+               '-verbose',
+               '-metric',
+               'RMSE',
+               '-highlight-color',
+               'Red',
+               '-compose',
+               'Src',
+               '-read-mask' if mask_img else None,
+               mask_img if mask_img else None,
+               test_img,
+               ref_img,
+               diff_img,
+               )
+        return ' '.join(list(filter(None, cmd)))
+
+    def compare_cmd_magick_v6(test_img, ref_img, mask_img, diff_img):
+        """
+        This compare command does not use the option --read-mask that is
+        available only on ImageMagic V7.0.3.9+. It has been tested with
+        ImageMagick V6.0 and works fine. The strategy being used here is to
+        multiply test and ref by the same B/W mask, in which black regions will
+        be ignored during the comparison.
+        """
+        def apply_mask_cmd(img, mask=None):
+            if not mask:
+                return (img,)
+
+            return (
+                '<(composite -compose Multiply %s %s png:)' % (img, mask),)
+
+        cmd = ('compare',
                '-verbose',
                '-metric',
                'RMSE',
@@ -194,28 +228,56 @@ class CheckScreenshot(SeleniumCommand):
                'Red',
                '-compose',
                'Src',
-               '-read-mask' if mask_img_path else None,
-               mask_img_path if mask_img_path else None,
-               current_img_path,
-               reference_img_path,
-               diff_img_path,
                )
+        cmd += apply_mask_cmd(test_img, mask_img)
+        cmd += apply_mask_cmd(ref_img, mask_img)
+        cmd += (diff_img,)
 
-        cmd = list(filter(None, cmd))
+        return ' '.join(list(cmd))
 
-        LOGGER.debug('  Comparing images...')
-        LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
-        process = subprocess.Popen(cmd, universal_newlines=True,
-                                   stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        process_stdout, process_stderr = process.communicate()
-        LOGGER.debug('  Results:')
-        LOGGER.debug('    return code: %s', process.returncode)
-        LOGGER.debug('    stdout: %s', process_stdout.strip())
-        LOGGER.debug('    stderr: %s', process_stderr.strip())
+    def compare_images(self, current_img_path, reference_img_path,
+                       mask_img_path, diff_img_path, threshold):
+        def exec_cmd(cmd):
+            LOGGER.debug('    cmd: $ %s', cmd)
+            process = subprocess.Popen(cmd, universal_newlines=True,
+                                       executable='/bin/bash', shell=True,
+                                       stdout=subprocess.PIPE,
+                                       stderr=subprocess.PIPE)
+            process_stdout, process_stderr = process.communicate()
+            LOGGER.debug('  Results:')
+            LOGGER.debug('    return code: %s', process.returncode)
+            LOGGER.debug('    stdout: %s', process_stdout.strip())
+            LOGGER.debug('    stderr: %s', process_stderr.strip())
+
+            return (process.returncode, process_stdout, process_stderr,)
+
+        def cmd_failed(ret):
+            """
+            ImageMagick Compare command returns 2 if the command failed, 1 if
+            the images are dissimilar and 0 if the images are similar. This
+            function checks if the return code represents an error, returning
+            True if so.
+            """
+            MAGICK_CMP_ERROR = 2
+            return ret == MAGICK_CMP_ERROR
+
+        LOGGER.debug('  Comparing images with ImageMagick v7 API...')
+        ret, stdout, stderr = exec_cmd(
+            CheckScreenshot.compare_cmd_magick_v7(current_img_path,
+                                                  reference_img_path,
+                                                  mask_img_path,
+                                                  diff_img_path))
+
+        if cmd_failed(ret):
+            LOGGER.debug('  Comparing images with ImageMagick v6 API...')
+            ret, stdout, stderr = exec_cmd(
+                CheckScreenshot.compare_cmd_magick_v6(current_img_path,
+                                                      reference_img_path,
+                                                      mask_img_path,
+                                                      diff_img_path))
 
         RESULTS_REGEX = re.compile(".*all:.*\(([0-9\-\.e]*)\).*")
-        result = RESULTS_REGEX.search(process_stderr)
+        result = RESULTS_REGEX.search(stderr)
 
         if not result:
             LOGGER.error("   Error processing the output.")
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 13/14] Improve logging
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (11 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 12/14] Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7 Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-25 23:38   ` Tim.Bird
  2018-04-24 17:24 ` [Fuego] [PATCH 14/14] Set logging level to INFO by default Guilherme Campos Camargo
                   ` (2 subsequent siblings)
  15 siblings, 1 reply; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Use LOGGER.info for the messages that are required to identify which
tests are running and if they passed.

Use Logger.error for test failures.

Use Logger.debug for additional information that may be useful while
debugging the tests' execution.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 .../Functional.fuego_release_test/test_run.py | 81 +++++++++++--------
 1 file changed, 49 insertions(+), 32 deletions(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index a36014d..0fe9260 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -52,8 +52,8 @@ class SeleniumCommand:
     def exec(self, selenium_ctx):
         self.driver = selenium_ctx.driver
         self.driver.refresh()
-        LOGGER.debug("Executing Selenium Command '%s'",
-                     self.__class__.__name__)
+        LOGGER.info("Executing Selenium Command '%s'",
+                    self.__class__.__name__)
 
     @staticmethod
     def check_element_text(element, text):
@@ -62,11 +62,13 @@ class SeleniumCommand:
                 LOGGER.\
                     debug("  Text '%s' matches element.text '%s'",
                           text, element.text)
+                LOGGER.info("  Matching text found")
                 return True
             else:
                 LOGGER.\
                     debug("  Text '%s' does not match element.text '%s'",
                           text, element.text)
+                LOGGER.error("  Matching text not found.")
                 return False
         except (selenium_exceptions.ElementNotVisibleException,
                 selenium_exceptions.NoSuchAttributeException,):
@@ -77,7 +79,7 @@ class SeleniumCommand:
     def click_element(element):
         try:
             element.click()
-            LOGGER.debug("  Element clicked")
+            LOGGER.info("  Element clicked")
             return True
         except (selenium_exceptions.ElementClickInterceptedException,
                 selenium_exceptions.ElementNotVisibleException,):
@@ -124,17 +126,17 @@ class Visit(SeleniumCommand):
     def exec(self, selenium_ctx):
         super().exec(selenium_ctx)
 
-        LOGGER.debug("  Visiting '%s'", self.url)
+        LOGGER.info("  Visiting '%s'", self.url)
         self.driver.get(self.url)
 
         r = requests.get(self.url)
         if r.status_code != self.expected_result:
-            LOGGER.debug("  HTTP Status Code '%s' is different " +
+            LOGGER.error("  HTTP Status Code '%s' is different " +
                          "from the expected '%s'", r.status_cod, self.url)
             return False
 
-        LOGGER.debug("  HTTP Status Code is same as expected '%s'",
-                     r.status_code)
+        LOGGER.info("  HTTP Status Code is same as expected '%s'",
+                    r.status_code)
         return True
 
 
@@ -280,21 +282,21 @@ class CheckScreenshot(SeleniumCommand):
         result = RESULTS_REGEX.search(stderr)
 
         if not result:
-            LOGGER.error("   Error processing the output.")
+            LOGGER.error("   Error processing the output")
             return False
 
         difference = float(result.group(1))
         if difference > threshold:
-            LOGGER.debug("  Resulting difference (%s) above threshold (%s). ",
+            LOGGER.error("  Resulting difference (%s) above threshold (%s)",
                          difference, threshold)
-            LOGGER.debug("  Element's screenshot does not match the reference."
+            LOGGER.error("  Element's screenshot does not match the reference."
                          "  See %s for a visual representation of the "
                          "  differences.", diff_img_path)
             return False
 
-        LOGGER.debug("  Resulting difference (%s) below threshold (%s).",
-                     difference, threshold)
-        LOGGER.debug("  Element's screenshot matches the reference.")
+        LOGGER.info("  Resulting difference (%s) below threshold (%s)",
+                    difference, threshold)
+        LOGGER.info("  Element's screenshot matches the reference")
         return True
 
     def exec(self, selenium_ctx):
@@ -354,8 +356,8 @@ class Back(SeleniumCommand):
     def exec(self, selenium_ctx):
         super().exec(selenium_ctx)
 
-        LOGGER.debug("  Going back")
         self.driver.back()
+        LOGGER.info("  Went back")
 
         return True
 
@@ -378,7 +380,7 @@ class ShExpect():
     def exec(self, pexpect_ctx):
         self.client = pexpect_ctx.client
 
-        LOGGER.debug("Executing command '%s'", self.cmd)
+        LOGGER.info("Executing command '%s'", self.cmd)
         try:
             self.client.sendline('%s=$(%s 2>&1)' %
                                  (self.OUTPUT_VARIABLE, self.cmd))
@@ -413,6 +415,9 @@ class ShExpect():
         except pexpect.exceptions.EOF:
             LOGGER.error("Lost connection with docker. Aborting")
             return False
+
+        LOGGER.info("  Command result and command output matches " +
+                    "the expected result. (Return code: %d)", result)
         return True
 
 
@@ -428,10 +433,10 @@ class FuegoContainer:
     def delete(self):
         if self.container:
             if self.rm_after_test:
-                LOGGER.debug("Removing Container")
+                LOGGER.info("Removing Container")
                 self.container.remove(force=True)
             else:
-                LOGGER.debug("Not Removing the test container")
+                LOGGER.info("Not Removing the test container")
 
     def install(self):
         self.docker_client = docker.APIClient()
@@ -476,22 +481,25 @@ class FuegoContainer:
                     mount['source'] = None
 
         cmd = './%s %s' % (self.install_script, self.image_name)
-        LOGGER.debug("Running '%s' to install the docker image. " +
-                     "This may take a while....", cmd)
+        LOGGER.info("Running '%s' to install the docker image. " +
+                    "This may take a while....", cmd)
         status = subprocess.call(cmd, shell=True)
 
         LOGGER.debug("Install output code: %s", status)
 
         if status != 0:
+            LOGGER.error("Installation Failed")
             return None
 
+        LOGGER.info("Installation Complete")
+        LOGGER.info("Creating the container '%s'..." % self.container_name)
+
         docker_client = docker.from_env()
         containers = docker_client.containers.list(
             all=True, filters={'name': self.container_name})
         if containers:
-            LOGGER.debug(
-                "Erasing the container '%s', so a new one can be created",
-                self.container_name)
+            LOGGER.debug("Container already exists. Removing it, so a new " +
+                         "one can be created")
             containers[0].remove(force=True)
 
         mounts = [
@@ -530,7 +538,7 @@ class FuegoContainer:
                     for m in mounts],
             name=self.container_name, command='/bin/bash')
 
-        LOGGER.debug("Container '%s' created", self.container_name)
+        LOGGER.info("Container '%s' successfully created", self.container_name)
         return container
 
     def is_running(self):
@@ -584,14 +592,16 @@ class PexpectContainerSession():
         self.timeout = timeout
 
     def start(self):
-        LOGGER.debug(
-            "Starting container '%s'", self.container.container_name)
+        LOGGER.info(
+            "Starting container '%s'...", self.container.container_name)
         self.client = pexpect.spawnu(
             '%s %s' % (self.start_script, self.container.container_name),
             echo=False, timeout=self.timeout)
 
         PexpectContainerSession.set_ps1(self.client)
 
+        LOGGER.info("Container started with the ip '%s'",
+                    self.container.get_ip())
         if not self.wait_for_jenkins():
             return False
 
@@ -629,14 +639,13 @@ class PexpectContainerSession():
         container_addr = self.container.get_ip()
         if container_addr is None:
             return False
-        LOGGER.debug("Trying to reach jenkins at container '%s' via " +
-                     "the container's IP '%s' at port '%d'",
-                     self.container.container_name,
-                     container_addr, self.container.jenkins_port)
+        LOGGER.info("Waiting for jenkins on '%s:%d'...",
+                    container_addr, self.container.jenkins_port)
         if not loop_until_timeout(ping_jenkins, timeout=60):
             LOGGER.error("Could not connect to jenkins")
             return False
 
+        LOGGER.info("Jenkins is running")
         return True
 
 
@@ -649,6 +658,7 @@ class SeleniumContainerSession():
         self.viewport_height = viewport_height
 
     def start(self):
+        LOGGER.info("Starting a Selenium Session on %s...", self.root_url)
         options = webdriver.ChromeOptions()
         options.add_argument('headless')
         options.add_argument('no-sandbox')
@@ -662,7 +672,7 @@ class SeleniumContainerSession():
 
         self.driver.get(self.root_url)
 
-        LOGGER.debug("Started a Selenium Session on %s", self.root_url)
+        LOGGER.info("Selenium Session started successfully")
         return True
 
     def __del__(self):
@@ -696,7 +706,7 @@ def main():
         return abs_install_dir, abs_working_dir
 
     def execute_tests(timeout):
-        LOGGER.debug("Starting tests")
+        LOGGER.info("Starting tests...")
 
         ctx_mapper = {
             ShExpect: pexpect_session,
@@ -709,10 +719,14 @@ def main():
                 if isinstance(cmd, base_class):
                     if not cmd.exec(ctx):
                         tests_ok = False
+                        LOGGER.error("  FAIL")
                         break
+                    LOGGER.info("  PASS")
 
         if tests_ok:
-            LOGGER.debug("Tests finished.")
+            LOGGER.info("All tests finished with SUCCESS")
+        else:
+            LOGGER.info("Tests finished with ERRORS")
 
         return tests_ok
 
@@ -770,11 +784,14 @@ def main():
     pexpect_session = PexpectContainerSession(container, args.start_script,
                                               args.timeout)
     if not pexpect_session.start():
+        LOGGER.error("Pexpect session failed to start")
         return 1
 
     selenium_session = SeleniumContainerSession(container, viewport_width=1920,
                                                 viewport_height=1080)
+
     if not selenium_session.start():
+        LOGGER.error("Selenium session failed to start")
         return 1
 
     if os.getcwd != args.working_dir:
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [Fuego] [PATCH 14/14] Set logging level to INFO by default
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (12 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 13/14] Improve logging Guilherme Campos Camargo
@ 2018-04-24 17:24 ` Guilherme Campos Camargo
  2018-04-24 17:40 ` [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Camargo
  2018-04-25 23:10 ` Tim.Bird
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Campos Camargo @ 2018-04-24 17:24 UTC (permalink / raw)
  To: fuego

Set logging level to INFO to log only the necessary information for
identifying test passes and failures.

Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
---
 engine/tests/Functional.fuego_release_test/test_run.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/engine/tests/Functional.fuego_release_test/test_run.py b/engine/tests/Functional.fuego_release_test/test_run.py
index 0fe9260..044c0d3 100755
--- a/engine/tests/Functional.fuego_release_test/test_run.py
+++ b/engine/tests/Functional.fuego_release_test/test_run.py
@@ -23,7 +23,7 @@ LOGGER = logging.getLogger('test_run')
 STREAM_HANDLER = logging.StreamHandler()
 STREAM_HANDLER.setFormatter(
     logging.Formatter("%(name)s:%(levelname)s: %(message)s"))
-LOGGER.setLevel(logging.DEBUG)
+LOGGER.setLevel(logging.INFO)
 LOGGER.addHandler(STREAM_HANDLER)
 
 
-- 
2.17.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (13 preceding siblings ...)
  2018-04-24 17:24 ` [Fuego] [PATCH 14/14] Set logging level to INFO by default Guilherme Campos Camargo
@ 2018-04-24 17:40 ` Guilherme Camargo
  2018-04-25 23:10 ` Tim.Bird
  15 siblings, 0 replies; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-24 17:40 UTC (permalink / raw)
  To: fuego

[-- Attachment #1: Type: text/plain, Size: 6000 bytes --]

On Tue, Apr 24, 2018 at 2:24 PM, Guilherme Campos Camargo <
guicc@profusion.mobi> wrote:

> Hello, everyone.
>
> This series of patches adds a new test case class to the
> fuego_release_test functional test. The name of this new test class is
> "CheckScreenshot", and its purpose is to get a screenshot of a web-page
> (in this specific case, Fuego's Jenkins' webpage) and compare it with a
> reference image.
>
> Currently, CheckScreenshot supports:
>  - Taking screenshot of an HTML element of a page and compare it with a
>    reference image.
>  - Taking screenshot of the full viewport (full page if it fits in the
>    viewport) and also compare it with a reference image.
>  - Take a screenshot (full or element) and compare only specific regions
>    of interest with a reference image. The mask is a black-and-white
>    image in which black regions are ignored by the comparison algorithm.
>
> On these patches we have also added a helper script (take_screenshot.py)
> that may be used for collecting reference screenshots and a few test
> cases to serve as example of usage.
>
> A README.md file has also been to explain the usage of
> fuego_release_test and the helper script, including a few examples.
>
>
> # Running
>
> Currently this test requires a modified version of Fuego to be executed,
> given that it needs to install some dependencies and needs to map the
> dockerd socket from the host to the fuego container.
>
> The modified version can be found in two different branches on
> Profusion's fuego fork.
>
> ​Correction: Branch 'fuego-test'​


>  1 - Branch master: Just a few commits that are necessary for making
>  this test work, applied on top of fuego/next. We plan to try to
>  integrate these commits into fuego/next in the future.
>
>  2 - Branch fuego-base-image: A more complex change on fuego, that makes
>  the necessary changes for allowing it to be shipped as a docker image
>  through dockerhub.
>
> The steps for each one of the versions above are given below:
>
> ## Building the image (from the branch fuego-test)
>
> To run the test, execute the following commands:
>

​Instructions are correct:​

>
> ```
> git clone --branch master https://bitbucket.org/
> profusionmobi/fuego-core.git
> git clone --branch fuego-test https://bitbucket.org/
> profusionmobi/fuego.git
> cd fuego
> ./install fuego-to-test-fuego
> ./fuego-host-scripts/docker-create-container.sh fuego-to-test-fuego
> fuego-to-test-fuego-container
> ./fuego-host-scripts/docker-start-container.sh
> fuego-to-test-fuego-container
> ```
>
> Then, add the fuego-test board and the Functional.fuegotest and start
> the test through Jenkins (localhost:8080/fuego/)
>
> ```
> ftc add-nodes fuego-test
> ftc add-jobs -b fuego-test -t Functional.fuegotest
> ```
>
> ## Using the modified Fuego Base Image from Dockerhub
> (fuego-base-image):
>
> You can also use the fuego base image that's being developed in
> Profusion's fuego-base-image branch in our fork:
> https://bitbucket.org/profusionmobi/fuego/branch/fuego-base-image
>
> The image is already available on dockerhub and can be
> downloaded/executed with:
>
> ```
> docker pull fuegotest/fuego
> docker run -it \
>   -p 8080:8080 \
>   -v $(pwd)/host_fuego_home:/var/fuego_home \
>   -e JENKINS_UID=$(id -u) \
>   -e JENKINS_GID=$(id -g) \
>   -v /var/run/docker.sock:/var/run/docker.sock \
>   fuegotest/fuego:latest
> ```
>
> Wait for the shell to be available and add fuego-test board and
> Functional.fuegotest as explained in the last section.
>
> ```
> ftc add-nodes fuego-test
> ftc add-jobs -b fuego-test -t Functional.fuegotest
> ```
>
> You can also run the test in standalone mode (given that you have all
> the dependencies installed in your system). See the test README.md for
> more instructions.
>
> Thanks.
>
> Guilherme Campos Camargo (14):
>   Add a SeleniumCommand that compares screenshots
>   Minor style fix
>   Add a CheckScreenshot command into the COMMANDS_TO_TEST list
>   Increase the size of the webdriver viewport
>   Allow working_dir and install_dirs to be different
>   Prevent exception NameError when removing container
>   Add an example reference screenshot
>   Add helper script for taking element/full-page screenshots
>   Add mask-img-path argument to CheckScreenshot for ignored areas
>   Add a README.md
>   Allow Full viewport Screenshots
>   Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7
>   Improve logging
>   Set logging level to INFO by default
>
>  .../Functional.fuego_release_test/README.md   | 182 +++++++++
>  .../fuego_test.sh                             |   6 +-
>  .../helpers/take_screenshot.py                | 145 ++++++++
>  .../screenshots/footer.png                    | Bin 0 -> 7371 bytes
>  .../screenshots/footer_mask.png               | Bin 0 -> 302 bytes
>  .../screenshots/full_screenshot.png           | Bin 0 -> 46323 bytes
>  .../screenshots/full_screenshot_mask.png      | Bin 0 -> 10717 bytes
>  .../screenshots/side-panel-tasks.png          | Bin 0 -> 9609 bytes
>  .../Functional.fuego_release_test/test_run.py | 347 ++++++++++++++++--
>  9 files changed, 639 insertions(+), 41 deletions(-)
>  create mode 100644 engine/tests/Functional.fuego_release_test/README.md
>  create mode 100755 engine/tests/Functional.fuego_
> release_test/helpers/take_screenshot.py
>  create mode 100644 engine/tests/Functional.fuego_
> release_test/screenshots/footer.png
>  create mode 100644 engine/tests/Functional.fuego_
> release_test/screenshots/footer_mask.png
>  create mode 100644 engine/tests/Functional.fuego_
> release_test/screenshots/full_screenshot.png
>  create mode 100644 engine/tests/Functional.fuego_
> release_test/screenshots/full_screenshot_mask.png
>  create mode 100644 engine/tests/Functional.fuego_
> release_test/screenshots/side-panel-tasks.png
>
> --
> 2.17.0
>
>

[-- Attachment #2: Type: text/html, Size: 7696 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test
  2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
                   ` (14 preceding siblings ...)
  2018-04-24 17:40 ` [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Camargo
@ 2018-04-25 23:10 ` Tim.Bird
  2018-04-26 12:14   ` Guilherme Camargo
  15 siblings, 1 reply; 30+ messages in thread
From: Tim.Bird @ 2018-04-25 23:10 UTC (permalink / raw)
  To: guicc, fuego

These look good.  I've applied all 14 patches, and pushed them to my master branch.

I have a few questions, which I'll raise in response to individual patches.

Thanks!
 -- Tim

> -----Original Message-----
> From: Guilherme Campos Camargo
> Hello, everyone.
> 
> This series of patches adds a new test case class to the
> fuego_release_test functional test. The name of this new test class is
> "CheckScreenshot", and its purpose is to get a screenshot of a web-page
> (in this specific case, Fuego's Jenkins' webpage) and compare it with a
> reference image.
> 
> Currently, CheckScreenshot supports:
>  - Taking screenshot of an HTML element of a page and compare it with a
>    reference image.
>  - Taking screenshot of the full viewport (full page if it fits in the
>    viewport) and also compare it with a reference image.
>  - Take a screenshot (full or element) and compare only specific regions
>    of interest with a reference image. The mask is a black-and-white
>    image in which black regions are ignored by the comparison algorithm.
> 
> On these patches we have also added a helper script (take_screenshot.py)
> that may be used for collecting reference screenshots and a few test
> cases to serve as example of usage.
> 
> A README.md file has also been to explain the usage of
> fuego_release_test and the helper script, including a few examples.
> 
> 
> # Running
> 
> Currently this test requires a modified version of Fuego to be executed,
> given that it needs to install some dependencies and needs to map the
> dockerd socket from the host to the fuego container.
> 
> The modified version can be found in two different branches on
> Profusion's fuego fork.
> 
>  1 - Branch master: Just a few commits that are necessary for making
>  this test work, applied on top of fuego/next. We plan to try to
>  integrate these commits into fuego/next in the future.
> 
>  2 - Branch fuego-base-image: A more complex change on fuego, that makes
>  the necessary changes for allowing it to be shipped as a docker image
>  through dockerhub.
> 
> The steps for each one of the versions above are given below:
> 
> ## Building the image (from the branch fuego-test)
> 
> To run the test, execute the following commands:
> 
> ```
> git clone --branch master https://bitbucket.org/profusionmobi/fuego-
> core.git
> git clone --branch fuego-test https://bitbucket.org/profusionmobi/fuego.git
> cd fuego
> ./install fuego-to-test-fuego
> ./fuego-host-scripts/docker-create-container.sh fuego-to-test-fuego fuego-
> to-test-fuego-container
> ./fuego-host-scripts/docker-start-container.sh fuego-to-test-fuego-
> container
> ```
> 
> Then, add the fuego-test board and the Functional.fuegotest and start
> the test through Jenkins (localhost:8080/fuego/)
> 
> ```
> ftc add-nodes fuego-test
> ftc add-jobs -b fuego-test -t Functional.fuegotest
> ```
> 
> ## Using the modified Fuego Base Image from Dockerhub
> (fuego-base-image):
> 
> You can also use the fuego base image that's being developed in
> Profusion's fuego-base-image branch in our fork:
> https://bitbucket.org/profusionmobi/fuego/branch/fuego-base-image
> 
> The image is already available on dockerhub and can be
> downloaded/executed with:
> 
> ```
> docker pull fuegotest/fuego
> docker run -it \
>   -p 8080:8080 \
>   -v $(pwd)/host_fuego_home:/var/fuego_home \
>   -e JENKINS_UID=$(id -u) \
>   -e JENKINS_GID=$(id -g) \
>   -v /var/run/docker.sock:/var/run/docker.sock \
>   fuegotest/fuego:latest
> ```
> 
> Wait for the shell to be available and add fuego-test board and
> Functional.fuegotest as explained in the last section.
> 
> ```
> ftc add-nodes fuego-test
> ftc add-jobs -b fuego-test -t Functional.fuegotest
> ```
> 
> You can also run the test in standalone mode (given that you have all
> the dependencies installed in your system). See the test README.md for
> more instructions.
> 
> Thanks.
> 
> Guilherme Campos Camargo (14):
>   Add a SeleniumCommand that compares screenshots
>   Minor style fix
>   Add a CheckScreenshot command into the COMMANDS_TO_TEST list
>   Increase the size of the webdriver viewport
>   Allow working_dir and install_dirs to be different
>   Prevent exception NameError when removing container
>   Add an example reference screenshot
>   Add helper script for taking element/full-page screenshots
>   Add mask-img-path argument to CheckScreenshot for ignored areas
>   Add a README.md
>   Allow Full viewport Screenshots
>   Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7
>   Improve logging
>   Set logging level to INFO by default
> 
>  .../Functional.fuego_release_test/README.md   | 182 +++++++++
>  .../fuego_test.sh                             |   6 +-
>  .../helpers/take_screenshot.py                | 145 ++++++++
>  .../screenshots/footer.png                    | Bin 0 -> 7371 bytes
>  .../screenshots/footer_mask.png               | Bin 0 -> 302 bytes
>  .../screenshots/full_screenshot.png           | Bin 0 -> 46323 bytes
>  .../screenshots/full_screenshot_mask.png      | Bin 0 -> 10717 bytes
>  .../screenshots/side-panel-tasks.png          | Bin 0 -> 9609 bytes
>  .../Functional.fuego_release_test/test_run.py | 347 ++++++++++++++++--
>  9 files changed, 639 insertions(+), 41 deletions(-)
>  create mode 100644
> engine/tests/Functional.fuego_release_test/README.md
>  create mode 100755
> engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/footer.png
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.p
> ng
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_
> mask.png
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/side-panel-
> tasks.png
> 
> --
> 2.17.0
> 
> _______________________________________________
> Fuego mailing list
> Fuego@lists.linuxfoundation.org
> https://lists.linuxfoundation.org/mailman/listinfo/fuego

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list
  2018-04-24 17:24 ` [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list Guilherme Campos Camargo
@ 2018-04-25 23:17   ` Tim.Bird
  2018-04-26 13:31     ` Guilherme Camargo
  0 siblings, 1 reply; 30+ messages in thread
From: Tim.Bird @ 2018-04-25 23:17 UTC (permalink / raw)
  To: guicc, fuego

Looks good.  My only comment (which is not specific to this patch or test case)
is that it would be good if each 'Check...' test in COMMANDS_TO_TEST
was an individual test case for the full release test.
 -- Tim

> -----Original Message-----
> From: Guilherme Campos Camargo
> This test will take a screenshot of 'tasks' element from the side-panel
> of the Jenkins UI (that contains the 'New Item', 'People', 'Build
> History' and the 'Manage Jenkins' buttons) and compare it with an image
> that's stored in the workdir, called 'side-panel-tasks.png'. The allowed
> threshold is 0.1 (10%).
> 
> Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> ---
>  engine/tests/Functional.fuego_release_test/test_run.py | 6 ++++++
>  1 file changed, 6 insertions(+)
> 
> diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> b/engine/tests/Functional.fuego_release_test/test_run.py
> index b380acd..2075862 100755
> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> @@ -725,6 +725,12 @@ def main():
>                'docker.default.Functional.hello_world"]'),
>          CheckText(By.ID, 'executors',
>                    text='docker.default.Functional.hello_world'),
> +
> +        # Compare screenshot of an element of Jenkins UI
> +        CheckScreenshot(By.ID, 'tasks',
> +                        rm_images_on_success=True,
> +                        ref_img='screenshots/side-panel-tasks.png',
> +                        threshold=0.1)
>      ]
> 
>      if not execute_tests(args.timeout):
> --
> 2.17.0
> 
> _______________________________________________
> Fuego mailing list
> Fuego@lists.linuxfoundation.org
> https://lists.linuxfoundation.org/mailman/listinfo/fuego

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different
  2018-04-24 17:24 ` [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different Guilherme Campos Camargo
@ 2018-04-25 23:20   ` Tim.Bird
  2018-04-26 12:53     ` Guilherme Camargo
  0 siblings, 1 reply; 30+ messages in thread
From: Tim.Bird @ 2018-04-25 23:20 UTC (permalink / raw)
  To: guicc, fuego



> -----Original Message-----
> From: Guilherme Campos Camargo
> 
> The `working_dir` is where all the test assets reside and also where all
> the results (for instance the resulting images for the CheckScreenshot
> test) will be stored after the tests.
> 
> The `install_dir` is where the install script can be found.
> 
> `install_dir` is a required argument, while `working_dir` is optional
> and defaults to `install_dir`.
> 
> On this patch we're also modifying fuego_test.sh to copy the required
> assets from the test directory to the buildzone.

I'm not sure why this is needed.  I hate duplicating the files unnecessarily.
We could have lots of them eventually.

Why can't the assets just be accessed from the TEST_HOME directory?
 -- Tim

> 
> Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> ---
>  .../Functional.fuego_release_test/fuego_test.sh |  6 ++++--
>  .../Functional.fuego_release_test/test_run.py   | 17 ++++++++++++++---
>  2 files changed, 18 insertions(+), 5 deletions(-)
> 
> diff --git a/engine/tests/Functional.fuego_release_test/fuego_test.sh
> b/engine/tests/Functional.fuego_release_test/fuego_test.sh
> index f0c872b..e87d6a4 100755
> --- a/engine/tests/Functional.fuego_release_test/fuego_test.sh
> +++ b/engine/tests/Functional.fuego_release_test/fuego_test.sh
> @@ -18,11 +18,13 @@ function test_build {
>      echo "Cloning fuego-core from
> ${fuego_core_repo}:${fuego_core_branch}"
>      git clone --depth 1 --single-branch --branch "${fuego_core_branch}" \
>          "${fuego_core_repo}" "${fuego_release_dir}/fuego-core"
> -    cd -
> +
> +    echo "Copying assets from ${TEST_HOME} to the buildzone."
> +    cp -r "${TEST_HOME}/screenshots" .
>  }
> 
>  function test_run {
> -    sudo -n ${TEST_HOME}/test_run.py "${fuego_release_dir}/fuego"
> +    sudo -n "${TEST_HOME}/test_run.py" "${fuego_release_dir}/fuego" -w .
>      if [ "${?}" = 0 ]; then
>          report "echo ok 1 fuego release test"
>      else
> diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> b/engine/tests/Functional.fuego_release_test/test_run.py
> index ed3c313..8b97bde 100755
> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> @@ -638,7 +638,12 @@ def main():
>          return tests_ok
> 
>      parser = argparse.ArgumentParser()
> -    parser.add_argument('working_dir', help="The working directory",
> type=str)
> +    parser.add_argument('install_dir', help="The directory where the install "
> +                        "script resides.", type=str)
> +    parser.add_argument('-w', '--working_dir', help="The working directory.
> "
> +                        "Location of the test assets and where test results "
> +                        "will be stored. Defaults to install_dir.",
> +                        default=None, type=str)
>      parser.add_argument('-s', '--install-script',
>                          help="The script that will be used to install the " +
>                          "docker image. Defaults to '%s'" %
> @@ -672,8 +677,10 @@ def main():
>                          default=True, action='store_false')
>      args = parser.parse_args()
> 
> -    LOGGER.debug("Changing working dir to '%s'", args.working_dir)
> -    os.chdir(args.working_dir)
> +    args.install_dir, args.working_dir = get_abs_working_dirs()
> +
> +    LOGGER.debug("Changing working dir to '%s'", args.install_dir)
> +    os.chdir(args.install_dir)
> 
>      container = FuegoContainer(args.install_script, args.image_name,
>                                 args.container_name, args.jenkins_port,
> @@ -690,6 +697,10 @@ def main():
>      if not selenium_session.start():
>          return 1
> 
> +    if os.getcwd != args.working_dir:
> +        LOGGER.debug("Changing working dir to '%s'", args.working_dir)
> +        os.chdir(args.working_dir)
> +
>      COMMANDS_TO_TEST = [
>          # Set Selenium Browser root
>          Visit(url=container.get_url()),
> --
> 2.17.0
> 
> _______________________________________________
> Fuego mailing list
> Fuego@lists.linuxfoundation.org
> https://lists.linuxfoundation.org/mailman/listinfo/fuego

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas
  2018-04-24 17:24 ` [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas Guilherme Campos Camargo
@ 2018-04-25 23:28   ` Tim.Bird
  2018-04-26 13:09     ` Guilherme Camargo
  0 siblings, 1 reply; 30+ messages in thread
From: Tim.Bird @ 2018-04-25 23:28 UTC (permalink / raw)
  To: guicc, fuego

> -----Original Message-----
> From: Guilherme Campos Camargo
> 
> The new mask-img-path argument that has been added to CheckScreenshot
> can be used to provide a mask image that will determine which parts of
> the screenshots will be compared.
> 
> The mask needs to be a Black and White .png image in which the Black
> areas will be ignored on the comparison, while the White areas will be
> compared normally.

How do you generate these?

It seems like it would be nice to be able to use a chroma-key color (like magenta),
for the to-ignore regions, and have all other colors be interpreted as white,
for the "to-compare" regions.  This would allow creation of the masks by
just drawing a rectangle of magenta on the reference image.
That is, you could pull in the mask image in the above-described format, and
convert all colors but magenta to white, and magenta to black, and then
use the mask image like you are doing now.

But maybe you have a workflow where it's already easy to create the masks.

One good thing about this is you could look at the mask and more easily tell
what part of the image you intend to ignore.
 -- Tim

> 
> A new CheckScreenshot has been added to check the 'page_generated'
> element of the Jenkins web-interface. That element is positioned in the
> lower right corner of the page and contains a DateTime field that
> changes continually.
> 
> The assets used as reference and mask have also been included in this
> commit.
> 
> Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> ---
>  .../screenshots/page-generated-mask.png       | Bin 0 -> 183 bytes
>  .../screenshots/page-generated.png            | Bin 0 -> 4583 bytes
>  .../Functional.fuego_release_test/test_run.py |  25 +++++++++++++-----
>  3 files changed, 19 insertions(+), 6 deletions(-)
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/page-generated-
> mask.png
>  create mode 100644
> engine/tests/Functional.fuego_release_test/screenshots/page-
> generated.png
> 
> diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-
> generated-mask.png
> b/engine/tests/Functional.fuego_release_test/screenshots/page-
> generated-mask.png
> new file mode 100644
> index
> 0000000000000000000000000000000000000000..a565ba4dd4b8cbd5ab4be9c70
> 50d8046f63e238a
> GIT binary patch
> literal 183
> zcmeAS@N?(olHy`uVBq!ia0y~yU{nCI`8e2sWPi}XA|S<<<n8Xl@E-
> &h>|H(?D8gCb
> z5n0T@z%2~Ij105pNB{-
> dOFVsD*&ne8i79f=*0Tx$3b}Z?IEG~0dwXRgFM|Qk;SIa~
> z|9=xV!PeoVu!%-)b%bgsN6kYo1_^-
> #2L?7434sPiMmClN2OtF(I`D*X)n68A7Wv8g
> QK=T+pUHx3vIVCg!0HVY$sQ>@~
> 
> literal 0
> HcmV?d00001
> 
> diff --git a/engine/tests/Functional.fuego_release_test/screenshots/page-
> generated.png
> b/engine/tests/Functional.fuego_release_test/screenshots/page-
> generated.png
> new file mode 100644
> index
> 0000000000000000000000000000000000000000..9b16d92e5cc3ec6c9f57723115
> e21f8f23f5eab9
> GIT binary patch
> literal 4583
> zcmV<D5g6`?P)<h;3K|Lk000e1NJLTq00AHX000gM0ssI205Dc1000rLNkl<Zc%1E
> 8
> zXF!ulx1Kizl2Aek5Q>6yWEH^%Hae&v0wOEc#j@BC*Ig?jNE5pvh=LRmP?|~+0
> RfS2
> z`>d-VDxDBI2}vLcy!Xer*+^6O?sxCE_xk+doik^iIp<6}GXY_98Q?p<<2(L6zyJWy
> z&+)%)(%+A7^^47BFPdjDz!~`4nI|9_&GSuzGM@67^1s~U1hrq;a6C&U_VJ7T
> ezDUD
> z^e12*Qw<#p<GG0g0EC_kPfJM$0FX!|Ep5%6UfXqav=IcEz~O%ua_;1o6qkgY4
> F21l
> z+T*pi`fbha>@-
> nP(Z9yf(cW?D#F@8ms(X5R(=$@!<>jE9#bTW~dG2A=qoJYU8Ab+s
> ze%mc4M;VXg@uMf#t|q>#sm0@Q#wK%jdTl3@$*?Lm`s&TJTYUbIm5t@D-
> 8)1?L;wIG
> zK__0kc-
> 7L};^XISZDT12OMF~H#_cR7gQ>1D#nWrM{tx;iB6i}~sprpMwludKIqGd?
> zZ7GnKm6V5vUFvA>Fqkp@p!Yt?C{xo@Z{Eqe+uYnjmLe~6uv@ob4F-
> c@v)SGUeBRf7
> z;BW?1RaG~8xEq_y9X&>6CFK{xFVoxTGYkz59NH`QM~DCbv{u@>)$Rr}rk^@{
> d_=?h
> zg~h%;{wRv#a5!aUrJpvfx3aeE?d@||>ZGWocs=DB27`fdcC>fF;cz+mnIjl%ES8=
> +
> zd)#2gbXZKekycgt@JF|m`+hw*dO@b&PR5IfL`TJzmsK$O7*s_CODl`jYgb`lwV
> nNv
> z`=$4i)032yl>+>NhKGm88_WM#peQ;tH1t2&BdwM8;`vJiLCQ-
> jCf<OB!RPaZiwFi|
> zGSAq@&wIq^UX8t0U*8xTe<e9R5k-
> e1!Y&UFjXNmq?H%?GORpwGU$}U_si`?UGy;xb
> ze%`&z+gazr&Rk81e*N-
> w;<f8guC1fB@4&B<RVNAeSyobhJux{b#Q#oShOx<9AMc~1
> z<Ef>s`P;r<RMjR63tL-
> S1C9o7+Wgba%oH+NGSL6n=xB6xb#L40aU(79(Bb{*sW<QD
> z<P8rG<8ZjO>(|6yi%z?p?6%g`_ppC&Z|{hPtu3tq{=pkJZMdDCMwXHc3J4LNo
> M+F3
> z>gZ~Z7Zzm|<-
> !MfIr*6`uFfGrC+Tf;D92(kFJHWZMm{Jmm6IQL=NgSUb7$YXf487G
> z_e$Jl3<i^vpIK0xTTq-UCNAc=-
> HX;r3qBr@nR7EZB#^;i*1mg>gUt{G!QpUHQsm`M
> z4!ODc91bTnIX&l2E~Ag3tgN(S*EXY>h6sWT4-
> ZF2#b#w>ixLQH*RMW(;%sg~Hi<;)
> z>gqm!=EC!5&qWD@6)sM$t6bprAOM5GI1zIC+0*B8auhQQ)5?mf%U3P}0IJ_
> shlNJe
> zeXN(0qioyZG1p`c0Km^DKuVh2P~X_q*(D(<;d}J3yn_71%Jp>(tu$I-@X^u-
> Wx03r
> zh0ShmYKgjZrN5u$x?(k1QZh0og3sq)jlFg+zhHnfuwdbQ&)qwSL?Vi!F<0WUGP
> 3a^
> zBI`D+5loSH?`kW{tKg11BG&nN1$t9;b@g=ea`VkC&7jcVH;^PI*4)(0WH8B6<oy
> SK
> zQ=lp|HZ*x|_i|d{SX}f#OI!1h&jG=5qEKWVotO9a_KhHKYojk%IFBMnK@h~k(
> mXjS
> z6$Rt2VLSUJf^QdB=lHk;Xn$@_{&FV=4Gnb!K~}GGi;Ibej;vVe3;@7kal#}tTAPlp
> zwyv%Yf*|(GmR^m$#$+<3rKN>+oL!s%0C*f;SXfwaUvH|exuqEvi}l#HWu>zly
> ^T&)
> zqzZ@5W7`&}EGsLs$aG=#+nU8oY(zvvX3jE#rm9R*;c~g19bMAW(kvD$E+&5U
> S~qEF
> zsltN$dV0EM7N$5H&SUGAmCkPM^meKu6#!6HQjWo3Oy(Is``7dFB7?)>L_|a!
> otH<1
> zN7mKV>+0zO0IY4S^79I&8%#qG<X&EZ?P8l7Nh#woi~t6Mf!PWUhXV+}<MF
> VVnw-w%
> z@`6JA2?PQFKuk<*_a09aML(M~!^6W5ic93=<cLJ#WYtNRqQZ0XGaXkf_xBCta
> 5w;f
> z{Jeq(#U)WOm*Nv+o;-
> dETcIfGd)QBbDxZ4m#)a^+S((|D<qt>fVd0?>0D#n6*F#PQ
> z6&2ox!tU;#y?YKgIWNz+lfL`cUBQ7NU0vO<s;csl_mP9~NwHJ)_2OdUCsr;itvG
> ae
> ze@a@CmX@Z#Y$q2-
> f8Rhhn>}f=%9gF0XU;NANl${iPFzg<@2}s4MT91&Co!1Jm@9E8
> zist7P6c*eMzZ7~kA-b%zTwr-
> qV^d~E_LxWdz5GH8OEYU*t49x?2#f&$R8~|SIeIwy
> zYNXLj!(%}wU|R-*F*GzBmk=$yx+cIf2fN3Qo^*D0F&K=Zg8OrfXMbgW`})-
> zH8nN(
> z{qbX+mX;=LsG+Ie+R{2WI5=KIGxJ5=UES5yHC!$?J1b|ZzMiC{<k$4rP~WJbsSb
> Nl
> zpem5blJ)fsD2k35Iuu3UzNwxvMOEOA*Umk5w#&D;Z#9{3tgbNy0Knl4BwtTu
> G8ki%
> z*EOIh%HeP#FJJN8wf$=>tSGPIa=9uh%CKl|VOCjQIXF1jMr)(d+Th{wH9QmG$
> >XOB
> z7tJ4WQ$P^p(*nDfU%1@P+1Apow505y_dW~;V`*g}D=UM=VjY&-
> ixLP;P0c8Z78eye
> zIyop%6^KOQx(#cfSsy-ptgCC-
> y8UNSQBkTQ)p><u$%6+Y7TEow2OHei5s5^qBGqB}
> zGAJy(S2R^$&)m`shr`V<G%zqUcv$raR@vBE$;e0p0Orh{T~kv#v9h&|B~1De1h
> KZU
> zf@UvsuoEQ^nwwijrz=?*cQ*g*E+<DJ5{d4cHWc44K~c1*pvck5K~YJOL?XFwT
> rY^Y
> zrIkhc?PMGdH-hBV%h$A4nyHzouC9)<ic&#-
> p`as6D|2YPi>veVXD@nsdQlVw2=b$w
> z3pDa;224X!gG3@ZJFK*`UEb5vw`QH&=p%GADl4iA?-
> g$T*&Qkma0W<XBv>UbCI$fD
> zjQ?^YEhB9*-
> `Hc*c1u&+jN4hi{Oa{JJpusO95#tW5){S6#n~)27$<a5m!mNl%o4lB
> z0&@Q#zo?jqZ96<>%`$?)p-
> ^N?DvBnnP8K3Z{v>C!0RZuF31;S|%73hxF+ppkIW2Qp
> zV!bTLKX`}dR+ULAP%cT9)SsqbRq-
> ${_nx_h=~ucrF&KRe3T2E`^ON(7Oc!qV+$ttU
> zA`*#El6yBlJ@r<1cQ*!u>Fnt2>+J&obar+LSmfp9;Rl^Y=MHi=tlbEUJRV;|Q(d?Z
> zCX+cZFrc6y55p(8tkdcA*RS5JU*iso9L~Vhsd}G6ASnTr2?WBx0B2%l85wE8tj)Q
> T
> zdn-NTPqTYQre6L1{Y)k^C?Euj#R33O6y**M0suNYx~NnI$fKyF^u>VY-
> OZml%TP*+
> zjH0Nel||m&eAiVju(=?uA`l2-
> Vq%@0odi*Wgt+)OJ%k(&J}Dw9l6Chs9*>K>6n*%h
> z@9A?Vpv~jqUr(Qf1f2*9379l_(x>PV32?3h0Qy-
> h005Cl7_a$_q?FQ<viQVUWfi5O
> z!s5Mq_D5fhl$DkJqNyYzNze-bfYskGSk)tta4o5<wEWDuQ-
> Wy*0FaU*OG%MeuU)la
> z?M8JCbwi^WBgk1S7WgAQhU9f0>q|?^E=7h5Ki`esx#Sh(!y?WT2%@8pVq075
> oSfXo
> zhDQHDzkz|VHys=f$LI3}%zQq7^gb&9S&H1z(J=-
> I{39wVLZSQ_%BIHV@X&~JVW%}T
> z)KL^&wQ?==ghHVRSm^XmS4KGnISEPe_{7-pb^-
> |r2_li$)<&0*kU$Wmz5Nq|yuAF(
> zSw?>TJ{T<gwE#@8xlf=hh-
> `CH%Y}2{VVBNBv)x?R!F!8v=*7jwB_t&L1N}6$G@u+t
> zP)?rG-
> O~fVyM^*SFxKF4IGp=M5BPk(i_?z?f^fM!CX@N@U9FCeHUNM|YlCXNy}kYY
> z{j##MtbW$FCLwio^}BZOl#(I?0300cH*NTt$KzqKSU3=)*`7apar8*Qk)wyFP5(h
> q
> ztD&J@Tl;?g!g&aSe0cvsNm*%vMgH-
> _$N3B9sj5vz5X8pT>f(in_q88nWMsZ*x!M%f
> zCy$>F4-
> aE77<wDMua7Zhikfh2;YafI)ST?xbD^gwa(~V=VFeULX|y)O&*f@URG&Wi
> z7xbFmMrSaXYHF&F9zO2s=w7>O1A-uIHiyUK{^+tMIW2L-
> 48&rw6pHMa)6#g}+%u=o
> zQ7Ce%YLjcK-;L=Pm8#I%N;8^i2<>TYX;q*qj2DDivu9@9&iZ-
> lW>HZB3>dU|>?OhG
> zuth{gV9r`mR!M86LAj~<qKw;F?eumIhm#PW1SvE%H53)8mo7%K+3caAp}LRt
> Z+?F>
> zA~7>HTNodgz~OM{ZS<_nY$&v_GJo;>MPXqPkH_P3xi4S56uk3{#@8sb*ep?k
> C^WmW
> zyo%OJgH}jMOSRGHg6BJXhozS;Mz+z~005mGoe!%Xp(tu*VRrXUF1%`{rKAh&
> udRI_
> zAD1xVB2ivefgnh1eAJb=$f(%MR}*8V8%!(6`;;XX7ZumkyyNkBS0ZC)7#av4uw!
> Cy
> zaFEC40s#0tp5Tt8tEZEDHxC~ESs8aGPo9Lw;{gDtPMi(!3mOsLH^0C2KH&4)zCA
> xo
> z)8}%zTrL;NZ5CT+W@I<iH+FV*-AGKawy}h>jmP5+4i2Iy%I6Oa4h}-^rt0felvlPi
> zw+syp6&4ir_p>Jbky8mUW<zre)9SZ16=jty7Ara`)@Y_7e3TLn-
> OaRHDamQajt7#Z
> z$b*A}a4f3dR+ki)c6N4kc6P<Z#M4^a^!4=s04x?OBK&esPcI~Y`=+|0tdh-
> UM@PjP
> z8qJVbkhfWEeeHT248Du2^R(&HA|k`SDbvH_@yAac^YJ}2K~i90WuAOJ<^B5)0
> |Nt(
> zA3Z55EH*V=IG(bjvjZNF^Eu>OQ&YoYu^Q?d&z}u@{p$BWUlE11b#%0?Z7est
> dr+xV
> zO)U*|^(j!ncCmGHQ}bqb4^aZaZMADjaVZXm!(cE0!AHYGBUZbv<8pbECaY}R{
> L_f;
> zckJ4B^7xsRPHu8?6jQTB@Y#n#kv)0#ctqIcbEhw0u^1g)?LEKkhQ<inBgo6Ys4O
> sB
> zK|w)FTXTxK8myT$%jlMZyraE~grr1La{R{4KPAK`{o=K^x2IQ5o?^euZm#hh8(X
> WU
> z#-<$}yQF2L=Pxip5Co?EjSY=inRiB9f%0?jEnaFvRip}pD^@y%o(<cwbrXt$B}*5d
> zKXak(W4(^9HoVaO;l&usjL+v=n}2!(T(xp7002JBdi~;g{_KU-uIq+|hPAXceU5rV
> zo{si*lX=F-=TzC5cbLq6?}JBRQCwU+Gv_7_hqGO5-
> O}9Rx!nu?4kqwox96{~UcLqZ
> z91A>tEbw^9i6E2t#!f37J32ad?cCGP>{nEz`uHAFR8kx<G4^=vg%8}p0U^NwA;(
> Vz
> z8=K5kR#Eo$IdC!jaz}f|3`2v%M-IYP!lAoyJ+-r=bKUAs6Jy>2lb~b%2nLgsc%$xP
> zJr;{qo1z*J><8~%@R7rQnVqz>l(LHQ;Ufpb!!C9Bbr{T;?(K5`gTW981o&7703eY_
> zcsyQKR%S$CKc7UO$>E2#w$>QcLU3F2Bag>Be9*VMt6ND)(R25XnX`>P;};qQF
> )^{Q
> zh|nuhF^Bg1^!4^BDp4)1EHt$=kZ=80DZ$xYU0ri%zfbDT>l0rLMkD!p>bsg+AHP
> H2
> z)asuCf8W5V`g$&|EB+niLqkLE>o;GCy$pXZ{*M2AeB1j8kH>ph^=No_n8{?uM
> 8z&x
> zIR7iJ7EO)KA3l6UQM9q4>DJAR1q<i>C2Rk`1B=DRC&qqXUH>4A`zkUfP!x@gj`
> #Ns
> z!sBtX=givTvGFStUlyz1-
> !Hhgr<Wu~va?@eW?}l5to=ve|IWXdeaC+v{{{QoPnL8U
> RPL==w002ovPDHLkV1i3q$F=|f
> 
> literal 0
> HcmV?d00001
> 
> diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> b/engine/tests/Functional.fuego_release_test/test_run.py
> index ba840b6..5fa2ca1 100755
> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> @@ -153,7 +153,7 @@ class CheckText(SeleniumCommand):
> 
> 
>  class CheckScreenshot(SeleniumCommand):
> -    def __init__(self, locator, pattern, ref_img, diff_img=None,
> +    def __init__(self, locator, pattern, ref_img, mask_img=None,
> diff_img=None,
>                   expected_result=True, threshold=0.0,
>                   rm_images_on_success=True):
>          def add_suffix(filename, suffix):
> @@ -166,6 +166,7 @@ class CheckScreenshot(SeleniumCommand):
>          self.pattern = pattern
>          self.locator = locator
>          self.reference_img_path = ref_img
> +        self.mask_img_path = mask_img
>          self.expected_result = expected_result
>          self.threshold = threshold
>          self.rm_images_on_success = rm_images_on_success
> @@ -178,8 +179,8 @@ class CheckScreenshot(SeleniumCommand):
>          self.test_img_path = add_suffix(ref_img, 'test')
> 
>      def compare_images(self, current_img_path, reference_img_path,
> -                       diff_img_path, threshold):
> -        cmd = ['magick',
> +                       mask_img_path, diff_img_path, threshold):
> +        cmd = ('magick',
>                 'compare',
>                 '-verbose',
>                 '-metric',
> @@ -188,10 +189,14 @@ class CheckScreenshot(SeleniumCommand):
>                 'Red',
>                 '-compose',
>                 'Src',
> +               '-read-mask' if mask_img_path else None,
> +               mask_img_path if mask_img_path else None,
>                 current_img_path,
>                 reference_img_path,
> -               diff_img_path
> -               ]
> +               diff_img_path,
> +               )
> +
> +        cmd = list(filter(None, cmd))
> 
>          LOGGER.debug('  Comparing images...')
>          LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
> @@ -246,6 +251,7 @@ class CheckScreenshot(SeleniumCommand):
> 
>          result = self.compare_images(self.test_img_path,
>                                       self.reference_img_path,
> +                                     self.mask_img_path,
>                                       self.diff_img_path,
>                                       self.threshold)
> 
> @@ -743,7 +749,14 @@ def main():
>          CheckScreenshot(By.ID, 'tasks',
>                          rm_images_on_success=True,
>                          ref_img='screenshots/side-panel-tasks.png',
> -                        threshold=0.1)
> +                        threshold=0.1),
> +
> +        # Compare screenshot of an element of Jenkins UI ignoring an area
> +        CheckScreenshot(By.CLASS_NAME, 'page_generated',
> +                        rm_images_on_success=False,
> +                        mask_img='screenshots/page-generated-mask.png',
> +                        ref_img='screenshots/page-generated.png',
> +                        threshold=0.1),
>      ]
> 
>      if not execute_tests(args.timeout):
> --
> 2.17.0
> 
> _______________________________________________
> Fuego mailing list
> Fuego@lists.linuxfoundation.org
> https://lists.linuxfoundation.org/mailman/listinfo/fuego

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 13/14] Improve logging
  2018-04-24 17:24 ` [Fuego] [PATCH 13/14] Improve logging Guilherme Campos Camargo
@ 2018-04-25 23:38   ` Tim.Bird
  2018-04-26 13:11     ` Guilherme Camargo
  0 siblings, 1 reply; 30+ messages in thread
From: Tim.Bird @ 2018-04-25 23:38 UTC (permalink / raw)
  To: guicc, fuego

Can we wrapper LOGGER.info and/or LOGGER.error, to have them emit something
like a testcase number (or better yet, number and description), so we can convert
some of these to individual testcase outputs?

What do you think?
 -- Tim

> -----Original Message-----
> From: fuego-bounces@lists.linuxfoundation.org [mailto:fuego-
> bounces@lists.linuxfoundation.org] On Behalf Of Guilherme Campos
> Camargo
> Sent: Tuesday, April 24, 2018 10:25 AM
> To: fuego@lists.linuxfoundation.org
> Subject: [Fuego] [PATCH 13/14] Improve logging
> 
> Use LOGGER.info for the messages that are required to identify which
> tests are running and if they passed.
> 
> Use Logger.error for test failures.
> 
> Use Logger.debug for additional information that may be useful while
> debugging the tests' execution.
> 
> Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> ---
>  .../Functional.fuego_release_test/test_run.py | 81 +++++++++++--------
>  1 file changed, 49 insertions(+), 32 deletions(-)
> 
> diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> b/engine/tests/Functional.fuego_release_test/test_run.py
> index a36014d..0fe9260 100755
> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> @@ -52,8 +52,8 @@ class SeleniumCommand:
>      def exec(self, selenium_ctx):
>          self.driver = selenium_ctx.driver
>          self.driver.refresh()
> -        LOGGER.debug("Executing Selenium Command '%s'",
> -                     self.__class__.__name__)
> +        LOGGER.info("Executing Selenium Command '%s'",
> +                    self.__class__.__name__)
> 
>      @staticmethod
>      def check_element_text(element, text):
> @@ -62,11 +62,13 @@ class SeleniumCommand:
>                  LOGGER.\
>                      debug("  Text '%s' matches element.text '%s'",
>                            text, element.text)
> +                LOGGER.info("  Matching text found")
>                  return True
>              else:
>                  LOGGER.\
>                      debug("  Text '%s' does not match element.text '%s'",
>                            text, element.text)
> +                LOGGER.error("  Matching text not found.")
>                  return False
>          except (selenium_exceptions.ElementNotVisibleException,
>                  selenium_exceptions.NoSuchAttributeException,):
> @@ -77,7 +79,7 @@ class SeleniumCommand:
>      def click_element(element):
>          try:
>              element.click()
> -            LOGGER.debug("  Element clicked")
> +            LOGGER.info("  Element clicked")
>              return True
>          except (selenium_exceptions.ElementClickInterceptedException,
>                  selenium_exceptions.ElementNotVisibleException,):
> @@ -124,17 +126,17 @@ class Visit(SeleniumCommand):
>      def exec(self, selenium_ctx):
>          super().exec(selenium_ctx)
> 
> -        LOGGER.debug("  Visiting '%s'", self.url)
> +        LOGGER.info("  Visiting '%s'", self.url)
>          self.driver.get(self.url)
> 
>          r = requests.get(self.url)
>          if r.status_code != self.expected_result:
> -            LOGGER.debug("  HTTP Status Code '%s' is different " +
> +            LOGGER.error("  HTTP Status Code '%s' is different " +
>                           "from the expected '%s'", r.status_cod, self.url)
>              return False
> 
> -        LOGGER.debug("  HTTP Status Code is same as expected '%s'",
> -                     r.status_code)
> +        LOGGER.info("  HTTP Status Code is same as expected '%s'",
> +                    r.status_code)
>          return True
> 
> 
> @@ -280,21 +282,21 @@ class CheckScreenshot(SeleniumCommand):
>          result = RESULTS_REGEX.search(stderr)
> 
>          if not result:
> -            LOGGER.error("   Error processing the output.")
> +            LOGGER.error("   Error processing the output")
>              return False
> 
>          difference = float(result.group(1))
>          if difference > threshold:
> -            LOGGER.debug("  Resulting difference (%s) above threshold (%s). ",
> +            LOGGER.error("  Resulting difference (%s) above threshold (%s)",
>                           difference, threshold)
> -            LOGGER.debug("  Element's screenshot does not match the
> reference."
> +            LOGGER.error("  Element's screenshot does not match the
> reference."
>                           "  See %s for a visual representation of the "
>                           "  differences.", diff_img_path)
>              return False
> 
> -        LOGGER.debug("  Resulting difference (%s) below threshold (%s).",
> -                     difference, threshold)
> -        LOGGER.debug("  Element's screenshot matches the reference.")
> +        LOGGER.info("  Resulting difference (%s) below threshold (%s)",
> +                    difference, threshold)
> +        LOGGER.info("  Element's screenshot matches the reference")
>          return True
> 
>      def exec(self, selenium_ctx):
> @@ -354,8 +356,8 @@ class Back(SeleniumCommand):
>      def exec(self, selenium_ctx):
>          super().exec(selenium_ctx)
> 
> -        LOGGER.debug("  Going back")
>          self.driver.back()
> +        LOGGER.info("  Went back")
> 
>          return True
> 
> @@ -378,7 +380,7 @@ class ShExpect():
>      def exec(self, pexpect_ctx):
>          self.client = pexpect_ctx.client
> 
> -        LOGGER.debug("Executing command '%s'", self.cmd)
> +        LOGGER.info("Executing command '%s'", self.cmd)
>          try:
>              self.client.sendline('%s=$(%s 2>&1)' %
>                                   (self.OUTPUT_VARIABLE, self.cmd))
> @@ -413,6 +415,9 @@ class ShExpect():
>          except pexpect.exceptions.EOF:
>              LOGGER.error("Lost connection with docker. Aborting")
>              return False
> +
> +        LOGGER.info("  Command result and command output matches " +
> +                    "the expected result. (Return code: %d)", result)
>          return True
> 
> 
> @@ -428,10 +433,10 @@ class FuegoContainer:
>      def delete(self):
>          if self.container:
>              if self.rm_after_test:
> -                LOGGER.debug("Removing Container")
> +                LOGGER.info("Removing Container")
>                  self.container.remove(force=True)
>              else:
> -                LOGGER.debug("Not Removing the test container")
> +                LOGGER.info("Not Removing the test container")
> 
>      def install(self):
>          self.docker_client = docker.APIClient()
> @@ -476,22 +481,25 @@ class FuegoContainer:
>                      mount['source'] = None
> 
>          cmd = './%s %s' % (self.install_script, self.image_name)
> -        LOGGER.debug("Running '%s' to install the docker image. " +
> -                     "This may take a while....", cmd)
> +        LOGGER.info("Running '%s' to install the docker image. " +
> +                    "This may take a while....", cmd)
>          status = subprocess.call(cmd, shell=True)
> 
>          LOGGER.debug("Install output code: %s", status)
> 
>          if status != 0:
> +            LOGGER.error("Installation Failed")
>              return None
> 
> +        LOGGER.info("Installation Complete")
> +        LOGGER.info("Creating the container '%s'..." % self.container_name)
> +
>          docker_client = docker.from_env()
>          containers = docker_client.containers.list(
>              all=True, filters={'name': self.container_name})
>          if containers:
> -            LOGGER.debug(
> -                "Erasing the container '%s', so a new one can be created",
> -                self.container_name)
> +            LOGGER.debug("Container already exists. Removing it, so a new " +
> +                         "one can be created")
>              containers[0].remove(force=True)
> 
>          mounts = [
> @@ -530,7 +538,7 @@ class FuegoContainer:
>                      for m in mounts],
>              name=self.container_name, command='/bin/bash')
> 
> -        LOGGER.debug("Container '%s' created", self.container_name)
> +        LOGGER.info("Container '%s' successfully created",
> self.container_name)
>          return container
> 
>      def is_running(self):
> @@ -584,14 +592,16 @@ class PexpectContainerSession():
>          self.timeout = timeout
> 
>      def start(self):
> -        LOGGER.debug(
> -            "Starting container '%s'", self.container.container_name)
> +        LOGGER.info(
> +            "Starting container '%s'...", self.container.container_name)
>          self.client = pexpect.spawnu(
>              '%s %s' % (self.start_script, self.container.container_name),
>              echo=False, timeout=self.timeout)
> 
>          PexpectContainerSession.set_ps1(self.client)
> 
> +        LOGGER.info("Container started with the ip '%s'",
> +                    self.container.get_ip())
>          if not self.wait_for_jenkins():
>              return False
> 
> @@ -629,14 +639,13 @@ class PexpectContainerSession():
>          container_addr = self.container.get_ip()
>          if container_addr is None:
>              return False
> -        LOGGER.debug("Trying to reach jenkins at container '%s' via " +
> -                     "the container's IP '%s' at port '%d'",
> -                     self.container.container_name,
> -                     container_addr, self.container.jenkins_port)
> +        LOGGER.info("Waiting for jenkins on '%s:%d'...",
> +                    container_addr, self.container.jenkins_port)
>          if not loop_until_timeout(ping_jenkins, timeout=60):
>              LOGGER.error("Could not connect to jenkins")
>              return False
> 
> +        LOGGER.info("Jenkins is running")
>          return True
> 
> 
> @@ -649,6 +658,7 @@ class SeleniumContainerSession():
>          self.viewport_height = viewport_height
> 
>      def start(self):
> +        LOGGER.info("Starting a Selenium Session on %s...", self.root_url)
>          options = webdriver.ChromeOptions()
>          options.add_argument('headless')
>          options.add_argument('no-sandbox')
> @@ -662,7 +672,7 @@ class SeleniumContainerSession():
> 
>          self.driver.get(self.root_url)
> 
> -        LOGGER.debug("Started a Selenium Session on %s", self.root_url)
> +        LOGGER.info("Selenium Session started successfully")
>          return True
> 
>      def __del__(self):
> @@ -696,7 +706,7 @@ def main():
>          return abs_install_dir, abs_working_dir
> 
>      def execute_tests(timeout):
> -        LOGGER.debug("Starting tests")
> +        LOGGER.info("Starting tests...")
> 
>          ctx_mapper = {
>              ShExpect: pexpect_session,
> @@ -709,10 +719,14 @@ def main():
>                  if isinstance(cmd, base_class):
>                      if not cmd.exec(ctx):
>                          tests_ok = False
> +                        LOGGER.error("  FAIL")
>                          break
> +                    LOGGER.info("  PASS")
> 
>          if tests_ok:
> -            LOGGER.debug("Tests finished.")
> +            LOGGER.info("All tests finished with SUCCESS")
> +        else:
> +            LOGGER.info("Tests finished with ERRORS")
> 
>          return tests_ok
> 
> @@ -770,11 +784,14 @@ def main():
>      pexpect_session = PexpectContainerSession(container, args.start_script,
>                                                args.timeout)
>      if not pexpect_session.start():
> +        LOGGER.error("Pexpect session failed to start")
>          return 1
> 
>      selenium_session = SeleniumContainerSession(container,
> viewport_width=1920,
>                                                  viewport_height=1080)
> +
>      if not selenium_session.start():
> +        LOGGER.error("Selenium session failed to start")
>          return 1
> 
>      if os.getcwd != args.working_dir:
> --
> 2.17.0
> 
> _______________________________________________
> Fuego mailing list
> Fuego@lists.linuxfoundation.org
> https://lists.linuxfoundation.org/mailman/listinfo/fuego

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test
  2018-04-25 23:10 ` Tim.Bird
@ 2018-04-26 12:14   ` Guilherme Camargo
  0 siblings, 0 replies; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-26 12:14 UTC (permalink / raw)
  To: Bird, Timothy; +Cc: fuego

[-- Attachment #1: Type: text/plain, Size: 6523 bytes --]

Thanks, Tim, will reply to those.

--
Guilherme

On Wed, Apr 25, 2018 at 8:10 PM, <Tim.Bird@sony.com> wrote:

> These look good.  I've applied all 14 patches, and pushed them to my
> master branch.
>
> I have a few questions, which I'll raise in response to individual patches.
>
> Thanks!
>  -- Tim
>
> > -----Original Message-----
> > From: Guilherme Campos Camargo
> > Hello, everyone.
> >
> > This series of patches adds a new test case class to the
> > fuego_release_test functional test. The name of this new test class is
> > "CheckScreenshot", and its purpose is to get a screenshot of a web-page
> > (in this specific case, Fuego's Jenkins' webpage) and compare it with a
> > reference image.
> >
> > Currently, CheckScreenshot supports:
> >  - Taking screenshot of an HTML element of a page and compare it with a
> >    reference image.
> >  - Taking screenshot of the full viewport (full page if it fits in the
> >    viewport) and also compare it with a reference image.
> >  - Take a screenshot (full or element) and compare only specific regions
> >    of interest with a reference image. The mask is a black-and-white
> >    image in which black regions are ignored by the comparison algorithm.
> >
> > On these patches we have also added a helper script (take_screenshot.py)
> > that may be used for collecting reference screenshots and a few test
> > cases to serve as example of usage.
> >
> > A README.md file has also been to explain the usage of
> > fuego_release_test and the helper script, including a few examples.
> >
> >
> > # Running
> >
> > Currently this test requires a modified version of Fuego to be executed,
> > given that it needs to install some dependencies and needs to map the
> > dockerd socket from the host to the fuego container.
> >
> > The modified version can be found in two different branches on
> > Profusion's fuego fork.
> >
> >  1 - Branch master: Just a few commits that are necessary for making
> >  this test work, applied on top of fuego/next. We plan to try to
> >  integrate these commits into fuego/next in the future.
> >
> >  2 - Branch fuego-base-image: A more complex change on fuego, that makes
> >  the necessary changes for allowing it to be shipped as a docker image
> >  through dockerhub.
> >
> > The steps for each one of the versions above are given below:
> >
> > ## Building the image (from the branch fuego-test)
> >
> > To run the test, execute the following commands:
> >
> > ```
> > git clone --branch master https://bitbucket.org/profusionmobi/fuego-
> > core.git
> > git clone --branch fuego-test https://bitbucket.org/
> profusionmobi/fuego.git
> > cd fuego
> > ./install fuego-to-test-fuego
> > ./fuego-host-scripts/docker-create-container.sh fuego-to-test-fuego
> fuego-
> > to-test-fuego-container
> > ./fuego-host-scripts/docker-start-container.sh fuego-to-test-fuego-
> > container
> > ```
> >
> > Then, add the fuego-test board and the Functional.fuegotest and start
> > the test through Jenkins (localhost:8080/fuego/)
> >
> > ```
> > ftc add-nodes fuego-test
> > ftc add-jobs -b fuego-test -t Functional.fuegotest
> > ```
> >
> > ## Using the modified Fuego Base Image from Dockerhub
> > (fuego-base-image):
> >
> > You can also use the fuego base image that's being developed in
> > Profusion's fuego-base-image branch in our fork:
> > https://bitbucket.org/profusionmobi/fuego/branch/fuego-base-image
> >
> > The image is already available on dockerhub and can be
> > downloaded/executed with:
> >
> > ```
> > docker pull fuegotest/fuego
> > docker run -it \
> >   -p 8080:8080 \
> >   -v $(pwd)/host_fuego_home:/var/fuego_home \
> >   -e JENKINS_UID=$(id -u) \
> >   -e JENKINS_GID=$(id -g) \
> >   -v /var/run/docker.sock:/var/run/docker.sock \
> >   fuegotest/fuego:latest
> > ```
> >
> > Wait for the shell to be available and add fuego-test board and
> > Functional.fuegotest as explained in the last section.
> >
> > ```
> > ftc add-nodes fuego-test
> > ftc add-jobs -b fuego-test -t Functional.fuegotest
> > ```
> >
> > You can also run the test in standalone mode (given that you have all
> > the dependencies installed in your system). See the test README.md for
> > more instructions.
> >
> > Thanks.
> >
> > Guilherme Campos Camargo (14):
> >   Add a SeleniumCommand that compares screenshots
> >   Minor style fix
> >   Add a CheckScreenshot command into the COMMANDS_TO_TEST list
> >   Increase the size of the webdriver viewport
> >   Allow working_dir and install_dirs to be different
> >   Prevent exception NameError when removing container
> >   Add an example reference screenshot
> >   Add helper script for taking element/full-page screenshots
> >   Add mask-img-path argument to CheckScreenshot for ignored areas
> >   Add a README.md
> >   Allow Full viewport Screenshots
> >   Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7
> >   Improve logging
> >   Set logging level to INFO by default
> >
> >  .../Functional.fuego_release_test/README.md   | 182 +++++++++
> >  .../fuego_test.sh                             |   6 +-
> >  .../helpers/take_screenshot.py                | 145 ++++++++
> >  .../screenshots/footer.png                    | Bin 0 -> 7371 bytes
> >  .../screenshots/footer_mask.png               | Bin 0 -> 302 bytes
> >  .../screenshots/full_screenshot.png           | Bin 0 -> 46323 bytes
> >  .../screenshots/full_screenshot_mask.png      | Bin 0 -> 10717 bytes
> >  .../screenshots/side-panel-tasks.png          | Bin 0 -> 9609 bytes
> >  .../Functional.fuego_release_test/test_run.py | 347 ++++++++++++++++--
> >  9 files changed, 639 insertions(+), 41 deletions(-)
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/README.md
> >  create mode 100755
> > engine/tests/Functional.fuego_release_test/helpers/take_screenshot.py
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/footer.png
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/footer_mask.png
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/full_screenshot.p
> > ng
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/full_screenshot_
> > mask.png
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/side-panel-
> > tasks.png
> >
> > --
> > 2.17.0
> >
> > _______________________________________________
> > Fuego mailing list
> > Fuego@lists.linuxfoundation.org
> > https://lists.linuxfoundation.org/mailman/listinfo/fuego
>

[-- Attachment #2: Type: text/html, Size: 8912 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different
  2018-04-25 23:20   ` Tim.Bird
@ 2018-04-26 12:53     ` Guilherme Camargo
  2018-04-26 18:00       ` Tim.Bird
  0 siblings, 1 reply; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-26 12:53 UTC (permalink / raw)
  To: Bird, Timothy; +Cc: fuego

[-- Attachment #1: Type: text/plain, Size: 2068 bytes --]

​​


On Wed, Apr 25, 2018 at 8:20 PM, <Tim.Bird@sony.com> wrote:

>
>
> > -----Original Message-----
> > From: Guilherme Campos Camargo
> >
> > The `working_dir` is where all the test assets reside and also where all
> > the results (for instance the resulting images for the CheckScreenshot
> > test) will be stored after the tests.
> >
> > The `install_dir` is where the install script can be found.
> >
> > `install_dir` is a required argument, while `working_dir` is optional
> > and defaults to `install_dir`.
> >
> > On this patch we're also modifying fuego_test.sh to copy the required
> > assets from the test directory to the buildzone.
>
> I'm not sure why this is needed.  I hate duplicating the files
> unnecessarily.
> We could have lots of them eventually.
>
> Why can't the assets just be accessed from the TEST_HOME directory?
>  -- Tim
>


​We currently have two working dirs: one in which fuego - and the
install script - resides  (install-dir) , and another (-w) in which the
test will look for assets and store things (as screenshot diff results,
etc.).

I don't like the idea of having things stored in the TEST_HOME,
given that it's the TEST_HOME is inside fuego-core and I also don't
think we should store these files in the install-dir, one option
for cleaning this up would be:

- Add a new argument to test_run.sh to separate the assets dir
(TEST_HOME/screenshots)
from where the captured screenshots and diffs will be stored), having
something like:

   $ test_run.py" "${fuego_release_dir}/fuego" -a ${assets_path} -w
${output_path}

What do you think?



> >  function test_run {
> > -    sudo -n ${TEST_HOME}/test_run.py "${fuego_release_dir}/fuego"
> > +
> ​​
> sudo -n "${TEST_HOME}/test_run.py" "${fuego_release_dir}/fuego" -w .
>

​We would do this instead:
​
​
sudo -n "${TEST_HOME}/test_run.py" "${fuego_release_dir}/fuego" -a
${TEST_HOME} -w .


>      if [ "${?}" = 0 ]; then
> >          report "echo ok 1 fuego release test"
> >      else
>

[-- Attachment #2: Type: text/html, Size: 7148 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas
  2018-04-25 23:28   ` Tim.Bird
@ 2018-04-26 13:09     ` Guilherme Camargo
  2018-04-26 13:39       ` Gustavo Sverzut Barbieri
  2018-04-26 18:07       ` Tim.Bird
  0 siblings, 2 replies; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-26 13:09 UTC (permalink / raw)
  To: Bird, Timothy; +Cc: fuego

[-- Attachment #1: Type: text/plain, Size: 14250 bytes --]

On Wed, Apr 25, 2018 at 8:28 PM, <Tim.Bird@sony.com> wrote:

> > -----Original Message-----
> > From: Guilherme Campos Camargo
> >
> > The new mask-img-path argument that has been added to CheckScreenshot
> > can be used to provide a mask image that will determine which parts of
> > the screenshots will be compared.
> >
> > The mask needs to be a Black and White .png image in which the Black
> > areas will be ignored on the comparison, while the White areas will be
> > compared normally.
>
> How do you generate these?
>

​Actually, it's pretty straightforward with Gimp (or any other image editor
with layers).

I just add a new transparent layer above the original image, paint black
the areas
that I want to remove​ (in the new layer) - right on top of the original
image - then
add a new white layer in between the two and export.

>
> It seems like it would be nice to be able to use a chroma-key color (like
> magenta),
> for the to-ignore regions, and have all other colors be interpreted as
> white,
> for the "to-compare" regions.  This would allow creation of the masks by
> just drawing a rectangle of magenta on the reference image.
> That is, you could pull in the mask image in the above-described format,
> and
> convert all colors but magenta to white, and magenta to black, and then
> use the mask image like you are doing now.​


​That's an option, but the problem with that is that magenta would need to
be a prohibited color on the original image.



> But maybe you have a workflow where it's already easy to create the masks.
>
> One good thing about this is you could look at the mask and more easily
> tell
> what part of the image you intend to ignore.
>  -- Tim
>
> >
> > A new CheckScreenshot has been added to check the 'page_generated'
> > element of the Jenkins web-interface. That element is positioned in the
> > lower right corner of the page and contains a DateTime field that
> > changes continually.
> >
> > The assets used as reference and mask have also been included in this
> > commit.
> >
> > Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> > ---
> >  .../screenshots/page-generated-mask.png       | Bin 0 -> 183 bytes
> >  .../screenshots/page-generated.png            | Bin 0 -> 4583 bytes
> >  .../Functional.fuego_release_test/test_run.py |  25 +++++++++++++-----
> >  3 files changed, 19 insertions(+), 6 deletions(-)
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/page-generated-
> > mask.png
> >  create mode 100644
> > engine/tests/Functional.fuego_release_test/screenshots/page-
> > generated.png
> >
> > diff --git a/engine/tests/Functional.fuego_release_test/
> screenshots/page-
> > generated-mask.png
> > b/engine/tests/Functional.fuego_release_test/screenshots/page-
> > generated-mask.png
> > new file mode 100644
> > index
> > 0000000000000000000000000000000000000000..a565ba4dd4b8cbd5ab4be9c70
> > 50d8046f63e238a
> > GIT binary patch
> > literal 183
> > zcmeAS@N?(olHy`uVBq!ia0y~yU{nCI`8e2sWPi}XA|S<<<n8Xl@E-
> > &h>|H(?D8gCb
> > z5n0T@z%2~Ij105pNB{-
> > dOFVsD*&ne8i79f=*0Tx$3b}Z?IEG~0dwXRgFM|Qk;SIa~
> > z|9=xV!PeoVu!%-)b%bgsN6kYo1_^-
> > #2L?7434sPiMmClN2OtF(I`D*X)n68A7Wv8g
> > QK=T+pUHx3vIVCg!0HVY$sQ>@~
> >
> > literal 0
> > HcmV?d00001
> >
> > diff --git a/engine/tests/Functional.fuego_release_test/
> screenshots/page-
> > generated.png
> > b/engine/tests/Functional.fuego_release_test/screenshots/page-
> > generated.png
> > new file mode 100644
> > index
> > 0000000000000000000000000000000000000000..9b16d92e5cc3ec6c9f57723115
> > e21f8f23f5eab9
> > GIT binary patch
> > literal 4583
> > zcmV<D5g6`?P)<h;3K|Lk000e1NJLTq00AHX000gM0ssI205Dc1000rLNkl<Zc%1E
> > 8
> > zXF!ulx1Kizl2Aek5Q>6yWEH^%Hae&v0wOEc#j@BC*Ig?jNE5pvh=LRmP?|~+0
> > RfS2
> > z`>d-VDxDBI2}vLcy!Xer*+^6O?sxCE_xk+doik^iIp<6}GXY_98Q?p<<2(L6zyJWy
> > z&+)%)(%+A7^^47BFPdjDz!~`4nI|9_&GSuzGM@67^1s~U1hrq;a6C&U_VJ7T
> > ezDUD
> > z^e12*Qw<#p<GG0g0EC_kPfJM$0FX!|Ep5%6UfXqav=IcEz~O%ua_;1o6qkgY4
> > F21l
> > z+T*pi`fbha>@-
> > nP(Z9yf(cW?D#F@8ms(X5R(=$@!<>jE9#bTW~dG2A=qoJYU8Ab+s
> > ze%mc4M;VXg@uMf#t|q>#sm0@Q#wK%jdTl3@$*?Lm`s&TJTYUbIm5t@D-
> > 8)1?L;wIG
> > zK__0kc-
> > 7L};^XISZDT12OMF~H#_cR7gQ>1D#nWrM{tx;iB6i}~sprpMwludKIqGd?
> > zZ7GnKm6V5vUFvA>Fqkp@p!Yt?C{xo@Z{Eqe+uYnjmLe~6uv@ob4F-
> > c@v)SGUeBRf7
> > z;BW?1RaG~8xEq_y9X&>6CFK{xFVoxTGYkz59NH`QM~DCbv{u@>)$Rr}rk^@{
> > d_=?h
> > zg~h%;{wRv#a5!aUrJpvfx3aeE?d@||>ZGWocs=DB27`fdcC>fF;cz+mnIjl%ES8=
> > +
> > zd)#2gbXZKekycgt@JF|m`+hw*dO@b&PR5IfL`TJzmsK$O7*s_CODl`jYgb`lwV
> > nNv
> > z`=$4i)032yl>+>NhKGm88_WM#peQ;tH1t2&BdwM8;`vJiLCQ-
> > jCf<OB!RPaZiwFi|
> > zGSAq@&wIq^UX8t0U*8xTe<e9R5k-
> > e1!Y&UFjXNmq?H%?GORpwGU$}U_si`?UGy;xb
> > ze%`&z+gazr&Rk81e*N-
> > w;<f8guC1fB@4&B<RVNAeSyobhJux{b#Q#oShOx<9AMc~1
> > z<Ef>s`P;r<RMjR63tL-
> > S1C9o7+Wgba%oH+NGSL6n=xB6xb#L40aU(79(Bb{*sW<QD
> > z<P8rG<8ZjO>(|6yi%z?p?6%g`_ppC&Z|{hPtu3tq{=pkJZMdDCMwXHc3J4LNo
> > M+F3
> > z>gZ~Z7Zzm|<-
> > !MfIr*6`uFfGrC+Tf;D92(kFJHWZMm{Jmm6IQL=NgSUb7$YXf487G
> > z_e$Jl3<i^vpIK0xTTq-UCNAc=-
> > HX;r3qBr@nR7EZB#^;i*1mg>gUt{G!QpUHQsm`M
> > z4!ODc91bTnIX&l2E~Ag3tgN(S*EXY>h6sWT4-
> > ZF2#b#w>ixLQH*RMW(;%sg~Hi<;)
> > z>gqm!=EC!5&qWD@6)sM$t6bprAOM5GI1zIC+0*B8auhQQ)5?mf%U3P}0IJ_
> > shlNJe
> > zeXN(0qioyZG1p`c0Km^DKuVh2P~X_q*(D(<;d}J3yn_71%Jp>(tu$I-@X^u-
> > Wx03r
> > zh0ShmYKgjZrN5u$x?(k1QZh0og3sq)jlFg+zhHnfuwdbQ&)qwSL?Vi!F<0WUGP
> > 3a^
> > zBI`D+5loSH?`kW{tKg11BG&nN1$t9;b@g=ea`VkC&7jcVH;^PI*4)(0WH8B6<oy
> > SK
> > zQ=lp|HZ*x|_i|d{SX}f#OI!1h&jG=5qEKWVotO9a_KhHKYojk%IFBMnK@h~k(
> > mXjS
> > z6$Rt2VLSUJf^QdB=lHk;Xn$@_{&FV=4Gnb!K~}GGi;Ibej;vVe3;@7kal#}tTAPlp
> > zwyv%Yf*|(GmR^m$#$+<3rKN>+oL!s%0C*f;SXfwaUvH|exuqEvi}l#HWu>zly
> > ^T&)
> > zqzZ@5W7`&}EGsLs$aG=#+nU8oY(zvvX3jE#rm9R*;c~g19bMAW(kvD$E+&5U
> > S~qEF
> > zsltN$dV0EM7N$5H&SUGAmCkPM^meKu6#!6HQjWo3Oy(Is``7dFB7?)>L_|a!
> > otH<1
> > zN7mKV>+0zO0IY4S^79I&8%#qG<X&EZ?P8l7Nh#woi~t6Mf!PWUhXV+}<MF
> > VVnw-w%
> > z@`6JA2?PQFKuk<*_a09aML(M~!^6W5ic93=<cLJ#WYtNRqQZ0XGaXkf_xBCta
> > 5w;f
> > z{Jeq(#U)WOm*Nv+o;-
> > dETcIfGd)QBbDxZ4m#)a^+S((|D<qt>fVd0?>0D#n6*F#PQ
> > z6&2ox!tU;#y?YKgIWNz+lfL`cUBQ7NU0vO<s;csl_mP9~NwHJ)_2OdUCsr;itvG
> > ae
> > ze@a@CmX@Z#Y$q2-
> > f8Rhhn>}f=%9gF0XU;NANl${iPFzg<@2}s4MT91&Co!1Jm@9E8
> > zist7P6c*eMzZ7~kA-b%zTwr-
> > qV^d~E_LxWdz5GH8OEYU*t49x?2#f&$R8~|SIeIwy
> > zYNXLj!(%}wU|R-*F*GzBmk=$yx+cIf2fN3Qo^*D0F&K=Zg8OrfXMbgW`})-
> > zH8nN(
> > z{qbX+mX;=LsG+Ie+R{2WI5=KIGxJ5=UES5yHC!$?J1b|ZzMiC{<k$4rP~WJbsSb
> > Nl
> > zpem5blJ)fsD2k35Iuu3UzNwxvMOEOA*Umk5w#&D;Z#9{3tgbNy0Knl4BwtTu
> > G8ki%
> > z*EOIh%HeP#FJJN8wf$=>tSGPIa=9uh%CKl|VOCjQIXF1jMr)(d+Th{wH9QmG$
> > >XOB
> > z7tJ4WQ$P^p(*nDfU%1@P+1Apow505y_dW~;V`*g}D=UM=VjY&-
> > ixLP;P0c8Z78eye
> > zIyop%6^KOQx(#cfSsy-ptgCC-
> > y8UNSQBkTQ)p><u$%6+Y7TEow2OHei5s5^qBGqB}
> > zGAJy(S2R^$&)m`shr`V<G%zqUcv$raR@vBE$;e0p0Orh{T~kv#v9h&|B~1De1h
> > KZU
> > zf@UvsuoEQ^nwwijrz=?*cQ*g*E+<DJ5{d4cHWc44K~c1*pvck5K~YJOL?XFwT
> > rY^Y
> > zrIkhc?PMGdH-hBV%h$A4nyHzouC9)<ic&#-
> > p`as6D|2YPi>veVXD@nsdQlVw2=b$w
> > z3pDa;224X!gG3@ZJFK*`UEb5vw`QH&=p%GADl4iA?-
> > g$T*&Qkma0W<XBv>UbCI$fD
> > zjQ?^YEhB9*-
> > `Hc*c1u&+jN4hi{Oa{JJpusO95#tW5){S6#n~)27$<a5m!mNl%o4lB
> > z0&@Q#zo?jqZ96<>%`$?)p-
> > ^N?DvBnnP8K3Z{v>C!0RZuF31;S|%73hxF+ppkIW2Qp
> > zV!bTLKX`}dR+ULAP%cT9)SsqbRq-
> > ${_nx_h=~ucrF&KRe3T2E`^ON(7Oc!qV+$ttU
> > zA`*#El6yBlJ@r<1cQ*!u>Fnt2>+J&obar+LSmfp9;Rl^Y=MHi=tlbEUJRV;|Q(d?Z
> > zCX+cZFrc6y55p(8tkdcA*RS5JU*iso9L~Vhsd}G6ASnTr2?WBx0B2%l85wE8tj)Q
> > T
> > zdn-NTPqTYQre6L1{Y)k^C?Euj#R33O6y**M0suNYx~NnI$fKyF^u>VY-
> > OZml%TP*+
> > zjH0Nel||m&eAiVju(=?uA`l2-
> > Vq%@0odi*Wgt+)OJ%k(&J}Dw9l6Chs9*>K>6n*%h
> > z@9A?Vpv~jqUr(Qf1f2*9379l_(x>PV32?3h0Qy-
> > h005Cl7_a$_q?FQ<viQVUWfi5O
> > z!s5Mq_D5fhl$DkJqNyYzNze-bfYskGSk)tta4o5<wEWDuQ-
> > Wy*0FaU*OG%MeuU)la
> > z?M8JCbwi^WBgk1S7WgAQhU9f0>q|?^E=7h5Ki`esx#Sh(!y?WT2%@8pVq075
> > oSfXo
> > zhDQHDzkz|VHys=f$LI3}%zQq7^gb&9S&H1z(J=-
> > I{39wVLZSQ_%BIHV@X&~JVW%}T
> > z)KL^&wQ?==ghHVRSm^XmS4KGnISEPe_{7-pb^-
> > |r2_li$)<&0*kU$Wmz5Nq|yuAF(
> > zSw?>TJ{T<gwE#@8xlf=hh-
> > `CH%Y}2{VVBNBv)x?R!F!8v=*7jwB_t&L1N}6$G@u+t
> > zP)?rG-
> > O~fVyM^*SFxKF4IGp=M5BPk(i_?z?f^fM!CX@N@U9FCeHUNM|YlCXNy}kYY
> > z{j##MtbW$FCLwio^}BZOl#(I?0300cH*NTt$KzqKSU3=)*`7apar8*Qk)wyFP5(h
> > q
> > ztD&J@Tl;?g!g&aSe0cvsNm*%vMgH-
> > _$N3B9sj5vz5X8pT>f(in_q88nWMsZ*x!M%f
> > zCy$>F4-
> > aE77<wDMua7Zhikfh2;YafI)ST?xbD^gwa(~V=VFeULX|y)O&*f@URG&Wi
> > z7xbFmMrSaXYHF&F9zO2s=w7>O1A-uIHiyUK{^+tMIW2L-
> > 48&rw6pHMa)6#g}+%u=o
> > zQ7Ce%YLjcK-;L=Pm8#I%N;8^i2<>TYX;q*qj2DDivu9@9&iZ-
> > lW>HZB3>dU|>?OhG
> > zuth{gV9r`mR!M86LAj~<qKw;F?eumIhm#PW1SvE%H53)8mo7%K+3caAp}LRt
> > Z+?F>
> > zA~7>HTNodgz~OM{ZS<_nY$&v_GJo;>MPXqPkH_P3xi4S56uk3{#@8sb*ep?k
> > C^WmW
> > zyo%OJgH}jMOSRGHg6BJXhozS;Mz+z~005mGoe!%Xp(tu*VRrXUF1%`{rKAh&
> > udRI_
> > zAD1xVB2ivefgnh1eAJb=$f(%MR}*8V8%!(6`;;XX7ZumkyyNkBS0ZC)7#av4uw!
> > Cy
> > zaFEC40s#0tp5Tt8tEZEDHxC~ESs8aGPo9Lw;{gDtPMi(!3mOsLH^0C2KH&4)zCA
> > xo
> > z)8}%zTrL;NZ5CT+W@I<iH+FV*-AGKawy}h>jmP5+4i2Iy%I6Oa4h}-^rt0felvlPi
> > zw+syp6&4ir_p>Jbky8mUW<zre)9SZ16=jty7Ara`)@Y_7e3TLn-
> > OaRHDamQajt7#Z
> > z$b*A}a4f3dR+ki)c6N4kc6P<Z#M4^a^!4=s04x?OBK&esPcI~Y`=+|0tdh-
> > UM@PjP
> > z8qJVbkhfWEeeHT248Du2^R(&HA|k`SDbvH_@yAac^YJ}2K~i90WuAOJ<^B5)0
> > |Nt(
> > zA3Z55EH*V=IG(bjvjZNF^Eu>OQ&YoYu^Q?d&z}u@{p$BWUlE11b#%0?Z7est
> > dr+xV
> > zO)U*|^(j!ncCmGHQ}bqb4^aZaZMADjaVZXm!(cE0!AHYGBUZbv<8pbECaY}R{
> > L_f;
> > zckJ4B^7xsRPHu8?6jQTB@Y#n#kv)0#ctqIcbEhw0u^1g)?LEKkhQ<inBgo6Ys4O
> > sB
> > zK|w)FTXTxK8myT$%jlMZyraE~grr1La{R{4KPAK`{o=K^x2IQ5o?^euZm#hh8(X
> > WU
> > z#-<$}yQF2L=Pxip5Co?EjSY=inRiB9f%0?jEnaFvRip}pD^@y%o(<cwbrXt$B}*5d
> > zKXak(W4(^9HoVaO;l&usjL+v=n}2!(T(xp7002JBdi~;g{_KU-uIq+|hPAXceU5rV
> > zo{si*lX=F-=TzC5cbLq6?}JBRQCwU+Gv_7_hqGO5-
> > O}9Rx!nu?4kqwox96{~UcLqZ
> > z91A>tEbw^9i6E2t#!f37J32ad?cCGP>{nEz`uHAFR8kx<G4^=vg%8}p0U^NwA;(
> > Vz
> > z8=K5kR#Eo$IdC!jaz}f|3`2v%M-IYP!lAoyJ+-r=bKUAs6Jy>2lb~b%2nLgsc%$xP
> > zJr;{qo1z*J><8~%@R7rQnVqz>l(LHQ;Ufpb!!C9Bbr{T;?(K5`gTW981o&7703eY_
> > zcsyQKR%S$CKc7UO$>E2#w$>QcLU3F2Bag>Be9*VMt6ND)(R25XnX`>P;};qQF
> > )^{Q
> > zh|nuhF^Bg1^!4^BDp4)1EHt$=kZ=80DZ$xYU0ri%zfbDT>l0rLMkD!p>bsg+AHP
> > H2
> > z)asuCf8W5V`g$&|EB+niLqkLE>o;GCy$pXZ{*M2AeB1j8kH>ph^=No_n8{?uM
> > 8z&x
> > zIR7iJ7EO)KA3l6UQM9q4>DJAR1q<i>C2Rk`1B=DRC&qqXUH>4A`zkUfP!x@gj`
> > #Ns
> > z!sBtX=givTvGFStUlyz1-
> > !Hhgr<Wu~va?@eW?}l5to=ve|IWXdeaC+v{{{QoPnL8U
> > RPL==w002ovPDHLkV1i3q$F=|f
> >
> > literal 0
> > HcmV?d00001
> >
> > diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> > b/engine/tests/Functional.fuego_release_test/test_run.py
> > index ba840b6..5fa2ca1 100755
> > --- a/engine/tests/Functional.fuego_release_test/test_run.py
> > +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> > @@ -153,7 +153,7 @@ class CheckText(SeleniumCommand):
> >
> >
> >  class CheckScreenshot(SeleniumCommand):
> > -    def __init__(self, locator, pattern, ref_img, diff_img=None,
> > +    def __init__(self, locator, pattern, ref_img, mask_img=None,
> > diff_img=None,
> >                   expected_result=True, threshold=0.0,
> >                   rm_images_on_success=True):
> >          def add_suffix(filename, suffix):
> > @@ -166,6 +166,7 @@ class CheckScreenshot(SeleniumCommand):
> >          self.pattern = pattern
> >          self.locator = locator
> >          self.reference_img_path = ref_img
> > +        self.mask_img_path = mask_img
> >          self.expected_result = expected_result
> >          self.threshold = threshold
> >          self.rm_images_on_success = rm_images_on_success
> > @@ -178,8 +179,8 @@ class CheckScreenshot(SeleniumCommand):
> >          self.test_img_path = add_suffix(ref_img, 'test')
> >
> >      def compare_images(self, current_img_path, reference_img_path,
> > -                       diff_img_path, threshold):
> > -        cmd = ['magick',
> > +                       mask_img_path, diff_img_path, threshold):
> > +        cmd = ('magick',
> >                 'compare',
> >                 '-verbose',
> >                 '-metric',
> > @@ -188,10 +189,14 @@ class CheckScreenshot(SeleniumCommand):
> >                 'Red',
> >                 '-compose',
> >                 'Src',
> > +               '-read-mask' if mask_img_path else None,
> > +               mask_img_path if mask_img_path else None,
> >                 current_img_path,
> >                 reference_img_path,
> > -               diff_img_path
> > -               ]
> > +               diff_img_path,
> > +               )
> > +
> > +        cmd = list(filter(None, cmd))
> >
> >          LOGGER.debug('  Comparing images...')
> >          LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
> > @@ -246,6 +251,7 @@ class CheckScreenshot(SeleniumCommand):
> >
> >          result = self.compare_images(self.test_img_path,
> >                                       self.reference_img_path,
> > +                                     self.mask_img_path,
> >                                       self.diff_img_path,
> >                                       self.threshold)
> >
> > @@ -743,7 +749,14 @@ def main():
> >          CheckScreenshot(By.ID, 'tasks',
> >                          rm_images_on_success=True,
> >                          ref_img='screenshots/side-panel-tasks.png',
> > -                        threshold=0.1)
> > +                        threshold=0.1),
> > +
> > +        # Compare screenshot of an element of Jenkins UI ignoring an
> area
> > +        CheckScreenshot(By.CLASS_NAME, 'page_generated',
> > +                        rm_images_on_success=False,
> > +                        mask_img='screenshots/page-generated-mask.png',
> > +                        ref_img='screenshots/page-generated.png',
> > +                        threshold=0.1),
> >      ]
> >
> >      if not execute_tests(args.timeout):
> > --
> > 2.17.0
> >
> > _______________________________________________
> > Fuego mailing list
> > Fuego@lists.linuxfoundation.org
> > https://lists.linuxfoundation.org/mailman/listinfo/fuego
>

[-- Attachment #2: Type: text/html, Size: 19591 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 13/14] Improve logging
  2018-04-25 23:38   ` Tim.Bird
@ 2018-04-26 13:11     ` Guilherme Camargo
  0 siblings, 0 replies; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-26 13:11 UTC (permalink / raw)
  To: Bird, Timothy; +Cc: fuego

[-- Attachment #1: Type: text/plain, Size: 13080 bytes --]

On Wed, Apr 25, 2018 at 8:38 PM, <Tim.Bird@sony.com> wrote:

> Can we wrapper LOGGER.info and/or LOGGER.error, to have them emit something
> like a testcase number (or better yet, number and description), so we can
> convert
> some of these to individual testcase outputs?
>

​I like the idea. We can do that.​

>
> What do you think?
>  -- Tim
>
> > -----Original Message-----
> > From: fuego-bounces@lists.linuxfoundation.org [mailto:fuego-
> > bounces@lists.linuxfoundation.org] On Behalf Of Guilherme Campos
> > Camargo
> > Sent: Tuesday, April 24, 2018 10:25 AM
> > To: fuego@lists.linuxfoundation.org
> > Subject: [Fuego] [PATCH 13/14] Improve logging
> >
> > Use LOGGER.info for the messages that are required to identify which
> > tests are running and if they passed.
> >
> > Use Logger.error for test failures.
> >
> > Use Logger.debug for additional information that may be useful while
> > debugging the tests' execution.
> >
> > Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> > ---
> >  .../Functional.fuego_release_test/test_run.py | 81 +++++++++++--------
> >  1 file changed, 49 insertions(+), 32 deletions(-)
> >
> > diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> > b/engine/tests/Functional.fuego_release_test/test_run.py
> > index a36014d..0fe9260 100755
> > --- a/engine/tests/Functional.fuego_release_test/test_run.py
> > +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> > @@ -52,8 +52,8 @@ class SeleniumCommand:
> >      def exec(self, selenium_ctx):
> >          self.driver = selenium_ctx.driver
> >          self.driver.refresh()
> > -        LOGGER.debug("Executing Selenium Command '%s'",
> > -                     self.__class__.__name__)
> > +        LOGGER.info("Executing Selenium Command '%s'",
> > +                    self.__class__.__name__)
> >
> >      @staticmethod
> >      def check_element_text(element, text):
> > @@ -62,11 +62,13 @@ class SeleniumCommand:
> >                  LOGGER.\
> >                      debug("  Text '%s' matches element.text '%s'",
> >                            text, element.text)
> > +                LOGGER.info("  Matching text found")
> >                  return True
> >              else:
> >                  LOGGER.\
> >                      debug("  Text '%s' does not match element.text
> '%s'",
> >                            text, element.text)
> > +                LOGGER.error("  Matching text not found.")
> >                  return False
> >          except (selenium_exceptions.ElementNotVisibleException,
> >                  selenium_exceptions.NoSuchAttributeException,):
> > @@ -77,7 +79,7 @@ class SeleniumCommand:
> >      def click_element(element):
> >          try:
> >              element.click()
> > -            LOGGER.debug("  Element clicked")
> > +            LOGGER.info("  Element clicked")
> >              return True
> >          except (selenium_exceptions.ElementClickInterceptedException,
> >                  selenium_exceptions.ElementNotVisibleException,):
> > @@ -124,17 +126,17 @@ class Visit(SeleniumCommand):
> >      def exec(self, selenium_ctx):
> >          super().exec(selenium_ctx)
> >
> > -        LOGGER.debug("  Visiting '%s'", self.url)
> > +        LOGGER.info("  Visiting '%s'", self.url)
> >          self.driver.get(self.url)
> >
> >          r = requests.get(self.url)
> >          if r.status_code != self.expected_result:
> > -            LOGGER.debug("  HTTP Status Code '%s' is different " +
> > +            LOGGER.error("  HTTP Status Code '%s' is different " +
> >                           "from the expected '%s'", r.status_cod,
> self.url)
> >              return False
> >
> > -        LOGGER.debug("  HTTP Status Code is same as expected '%s'",
> > -                     r.status_code)
> > +        LOGGER.info("  HTTP Status Code is same as expected '%s'",
> > +                    r.status_code)
> >          return True
> >
> >
> > @@ -280,21 +282,21 @@ class CheckScreenshot(SeleniumCommand):
> >          result = RESULTS_REGEX.search(stderr)
> >
> >          if not result:
> > -            LOGGER.error("   Error processing the output.")
> > +            LOGGER.error("   Error processing the output")
> >              return False
> >
> >          difference = float(result.group(1))
> >          if difference > threshold:
> > -            LOGGER.debug("  Resulting difference (%s) above threshold
> (%s). ",
> > +            LOGGER.error("  Resulting difference (%s) above threshold
> (%s)",
> >                           difference, threshold)
> > -            LOGGER.debug("  Element's screenshot does not match the
> > reference."
> > +            LOGGER.error("  Element's screenshot does not match the
> > reference."
> >                           "  See %s for a visual representation of the "
> >                           "  differences.", diff_img_path)
> >              return False
> >
> > -        LOGGER.debug("  Resulting difference (%s) below threshold
> (%s).",
> > -                     difference, threshold)
> > -        LOGGER.debug("  Element's screenshot matches the reference.")
> > +        LOGGER.info("  Resulting difference (%s) below threshold (%s)",
> > +                    difference, threshold)
> > +        LOGGER.info("  Element's screenshot matches the reference")
> >          return True
> >
> >      def exec(self, selenium_ctx):
> > @@ -354,8 +356,8 @@ class Back(SeleniumCommand):
> >      def exec(self, selenium_ctx):
> >          super().exec(selenium_ctx)
> >
> > -        LOGGER.debug("  Going back")
> >          self.driver.back()
> > +        LOGGER.info("  Went back")
> >
> >          return True
> >
> > @@ -378,7 +380,7 @@ class ShExpect():
> >      def exec(self, pexpect_ctx):
> >          self.client = pexpect_ctx.client
> >
> > -        LOGGER.debug("Executing command '%s'", self.cmd)
> > +        LOGGER.info("Executing command '%s'", self.cmd)
> >          try:
> >              self.client.sendline('%s=$(%s 2>&1)' %
> >                                   (self.OUTPUT_VARIABLE, self.cmd))
> > @@ -413,6 +415,9 @@ class ShExpect():
> >          except pexpect.exceptions.EOF:
> >              LOGGER.error("Lost connection with docker. Aborting")
> >              return False
> > +
> > +        LOGGER.info("  Command result and command output matches " +
> > +                    "the expected result. (Return code: %d)", result)
> >          return True
> >
> >
> > @@ -428,10 +433,10 @@ class FuegoContainer:
> >      def delete(self):
> >          if self.container:
> >              if self.rm_after_test:
> > -                LOGGER.debug("Removing Container")
> > +                LOGGER.info("Removing Container")
> >                  self.container.remove(force=True)
> >              else:
> > -                LOGGER.debug("Not Removing the test container")
> > +                LOGGER.info("Not Removing the test container")
> >
> >      def install(self):
> >          self.docker_client = docker.APIClient()
> > @@ -476,22 +481,25 @@ class FuegoContainer:
> >                      mount['source'] = None
> >
> >          cmd = './%s %s' % (self.install_script, self.image_name)
> > -        LOGGER.debug("Running '%s' to install the docker image. " +
> > -                     "This may take a while....", cmd)
> > +        LOGGER.info("Running '%s' to install the docker image. " +
> > +                    "This may take a while....", cmd)
> >          status = subprocess.call(cmd, shell=True)
> >
> >          LOGGER.debug("Install output code: %s", status)
> >
> >          if status != 0:
> > +            LOGGER.error("Installation Failed")
> >              return None
> >
> > +        LOGGER.info("Installation Complete")
> > +        LOGGER.info("Creating the container '%s'..." %
> self.container_name)
> > +
> >          docker_client = docker.from_env()
> >          containers = docker_client.containers.list(
> >              all=True, filters={'name': self.container_name})
> >          if containers:
> > -            LOGGER.debug(
> > -                "Erasing the container '%s', so a new one can be
> created",
> > -                self.container_name)
> > +            LOGGER.debug("Container already exists. Removing it, so a
> new " +
> > +                         "one can be created")
> >              containers[0].remove(force=True)
> >
> >          mounts = [
> > @@ -530,7 +538,7 @@ class FuegoContainer:
> >                      for m in mounts],
> >              name=self.container_name, command='/bin/bash')
> >
> > -        LOGGER.debug("Container '%s' created", self.container_name)
> > +        LOGGER.info("Container '%s' successfully created",
> > self.container_name)
> >          return container
> >
> >      def is_running(self):
> > @@ -584,14 +592,16 @@ class PexpectContainerSession():
> >          self.timeout = timeout
> >
> >      def start(self):
> > -        LOGGER.debug(
> > -            "Starting container '%s'", self.container.container_name)
> > +        LOGGER.info(
> > +            "Starting container '%s'...", self.container.container_name)
> >          self.client = pexpect.spawnu(
> >              '%s %s' % (self.start_script, self.container.container_name)
> ,
> >              echo=False, timeout=self.timeout)
> >
> >          PexpectContainerSession.set_ps1(self.client)
> >
> > +        LOGGER.info("Container started with the ip '%s'",
> > +                    self.container.get_ip())
> >          if not self.wait_for_jenkins():
> >              return False
> >
> > @@ -629,14 +639,13 @@ class PexpectContainerSession():
> >          container_addr = self.container.get_ip()
> >          if container_addr is None:
> >              return False
> > -        LOGGER.debug("Trying to reach jenkins at container '%s' via " +
> > -                     "the container's IP '%s' at port '%d'",
> > -                     self.container.container_name,
> > -                     container_addr, self.container.jenkins_port)
> > +        LOGGER.info("Waiting for jenkins on '%s:%d'...",
> > +                    container_addr, self.container.jenkins_port)
> >          if not loop_until_timeout(ping_jenkins, timeout=60):
> >              LOGGER.error("Could not connect to jenkins")
> >              return False
> >
> > +        LOGGER.info("Jenkins is running")
> >          return True
> >
> >
> > @@ -649,6 +658,7 @@ class SeleniumContainerSession():
> >          self.viewport_height = viewport_height
> >
> >      def start(self):
> > +        LOGGER.info("Starting a Selenium Session on %s...",
> self.root_url)
> >          options = webdriver.ChromeOptions()
> >          options.add_argument('headless')
> >          options.add_argument('no-sandbox')
> > @@ -662,7 +672,7 @@ class SeleniumContainerSession():
> >
> >          self.driver.get(self.root_url)
> >
> > -        LOGGER.debug("Started a Selenium Session on %s", self.root_url)
> > +        LOGGER.info("Selenium Session started successfully")
> >          return True
> >
> >      def __del__(self):
> > @@ -696,7 +706,7 @@ def main():
> >          return abs_install_dir, abs_working_dir
> >
> >      def execute_tests(timeout):
> > -        LOGGER.debug("Starting tests")
> > +        LOGGER.info("Starting tests...")
> >
> >          ctx_mapper = {
> >              ShExpect: pexpect_session,
> > @@ -709,10 +719,14 @@ def main():
> >                  if isinstance(cmd, base_class):
> >                      if not cmd.exec(ctx):
> >                          tests_ok = False
> > +                        LOGGER.error("  FAIL")
> >                          break
> > +                    LOGGER.info("  PASS")
> >
> >          if tests_ok:
> > -            LOGGER.debug("Tests finished.")
> > +            LOGGER.info("All tests finished with SUCCESS")
> > +        else:
> > +            LOGGER.info("Tests finished with ERRORS")
> >
> >          return tests_ok
> >
> > @@ -770,11 +784,14 @@ def main():
> >      pexpect_session = PexpectContainerSession(container,
> args.start_script,
> >                                                args.timeout)
> >      if not pexpect_session.start():
> > +        LOGGER.error("Pexpect session failed to start")
> >          return 1
> >
> >      selenium_session = SeleniumContainerSession(container,
> > viewport_width=1920,
> >                                                  viewport_height=1080)
> > +
> >      if not selenium_session.start():
> > +        LOGGER.error("Selenium session failed to start")
> >          return 1
> >
> >      if os.getcwd != args.working_dir:
> > --
> > 2.17.0
> >
> > _______________________________________________
> > Fuego mailing list
> > Fuego@lists.linuxfoundation.org
> > https://lists.linuxfoundation.org/mailman/listinfo/fuego
>

[-- Attachment #2: Type: text/html, Size: 18061 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list
  2018-04-25 23:17   ` Tim.Bird
@ 2018-04-26 13:31     ` Guilherme Camargo
  2018-04-26 18:17       ` Tim.Bird
  0 siblings, 1 reply; 30+ messages in thread
From: Guilherme Camargo @ 2018-04-26 13:31 UTC (permalink / raw)
  To: Bird, Timothy; +Cc: fuego

[-- Attachment #1: Type: text/plain, Size: 2166 bytes --]

On Wed, Apr 25, 2018 at 8:17 PM, <Tim.Bird@sony.com> wrote:

> Looks good.  My only comment (which is not specific to this patch or test
> case)
> is that it would be good if each 'Check...' test in COMMANDS_TO_TEST
> was an individual test case for the full release test.
>

Tim, not sure if I got it right. You mean that we should wrap each one
of the 'Check...' tests in a python script and call them individually
from within fuego_test.sh?

​Thanks​



>  -- Tim
>
> > -----Original Message-----
> > From: Guilherme Campos Camargo
> > This test will take a screenshot of 'tasks' element from the side-panel
> > of the Jenkins UI (that contains the 'New Item', 'People', 'Build
> > History' and the 'Manage Jenkins' buttons) and compare it with an image
> > that's stored in the workdir, called 'side-panel-tasks.png'. The allowed
> > threshold is 0.1 (10%).
> >
> > Signed-off-by: Guilherme Campos Camargo <guicc@profusion.mobi>
> > ---
> >  engine/tests/Functional.fuego_release_test/test_run.py | 6 ++++++
> >  1 file changed, 6 insertions(+)
> >
> > diff --git a/engine/tests/Functional.fuego_release_test/test_run.py
> > b/engine/tests/Functional.fuego_release_test/test_run.py
> > index b380acd..2075862 100755
> > --- a/engine/tests/Functional.fuego_release_test/test_run.py
> > +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> > @@ -725,6 +725,12 @@ def main():
> >                'docker.default.Functional.hello_world"]'),
> >          CheckText(By.ID, 'executors',
> >                    text='docker.default.Functional.hello_world'),
> > +
> > +        # Compare screenshot of an element of Jenkins UI
> > +        CheckScreenshot(By.ID, 'tasks',
> > +                        rm_images_on_success=True,
> > +                        ref_img='screenshots/side-panel-tasks.png',
> > +                        threshold=0.1)
> >      ]
> >
> >      if not execute_tests(args.timeout):
> > --
> > 2.17.0
> >
> > _______________________________________________
> > Fuego mailing list
> > Fuego@lists.linuxfoundation.org
> > https://lists.linuxfoundation.org/mailman/listinfo/fuego
>

[-- Attachment #2: Type: text/html, Size: 3724 bytes --]

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas
  2018-04-26 13:09     ` Guilherme Camargo
@ 2018-04-26 13:39       ` Gustavo Sverzut Barbieri
  2018-04-26 18:07       ` Tim.Bird
  1 sibling, 0 replies; 30+ messages in thread
From: Gustavo Sverzut Barbieri @ 2018-04-26 13:39 UTC (permalink / raw)
  To: Guilherme Camargo; +Cc: Bird, Timothy, fuego

On Thu, Apr 26, 2018 at 10:09 AM, Guilherme Camargo
<guicc@profusion.mobi> wrote:
>
>
> On Wed, Apr 25, 2018 at 8:28 PM, <Tim.Bird@sony.com> wrote:
>>
>> > -----Original Message-----
>> > From: Guilherme Campos Camargo
>> >
>> > The new mask-img-path argument that has been added to CheckScreenshot
>> > can be used to provide a mask image that will determine which parts of
>> > the screenshots will be compared.
>> >
>> > The mask needs to be a Black and White .png image in which the Black
>> > areas will be ignored on the comparison, while the White areas will be
>> > compared normally.
>>
>> How do you generate these?
>
>
> Actually, it's pretty straightforward with Gimp (or any other image editor
> with layers).
>
> I just add a new transparent layer above the original image, paint black the
> areas
> that I want to remove (in the new layer) - right on top of the original
> image - then
> add a new white layer in between the two and export.

IOW: just "draw" a hole in the image, lots of tools allow that, be the
complex layer-mask that Guilherme explained, or simply select the
region and "cut". If the image has a transparent layer, this will be
transparent (if it becomes white/black, then first add the
transparency to the image and repeat).

-- 
Gustavo Sverzut Barbieri
http://profusion.mobi embedded systems
--------------------------------------
MSN, GTalk, FaceTime: barbieri@gmail.com
Skype: gsbarbieri
Mobile: +55 (16) 99354-9890

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different
  2018-04-26 12:53     ` Guilherme Camargo
@ 2018-04-26 18:00       ` Tim.Bird
  0 siblings, 0 replies; 30+ messages in thread
From: Tim.Bird @ 2018-04-26 18:00 UTC (permalink / raw)
  To: guicc; +Cc: fuego

> -----Original Message-----
> From: Guilherme Camargo 
> 
> On Wed, Apr 25, 2018 at 8:20 PM, Tim Bird wrote:
> 	> -----Original Message-----
> 	> From: Guilherme Campos Camargo
> 	>
> 	> The `working_dir` is where all the test assets reside and also where
> all
> 	> the results (for instance the resulting images for the
> CheckScreenshot
> 	> test) will be stored after the tests.
> 	>
> 	> The `install_dir` is where the install script can be found.
> 	>
> 	> `install_dir` is a required argument, while `working_dir` is optional
> 	> and defaults to `install_dir`.
> 	>
> 	> On this patch we're also modifying fuego_test.sh to copy the
> required
> 	> assets from the test directory to the buildzone.
> 
> 	I'm not sure why this is needed.  I hate duplicating the files
> unnecessarily.
> 	We could have lots of them eventually.
> 
> 	Why can't the assets just be accessed from the TEST_HOME
> directory?
> 	 -- Tim
> 
> 
> 
> 
> ​We currently have two working dirs: one in which fuego - and the
> install script - resides  (install-dir) , and another (-w) in which the
> test will look for assets and store things (as screenshot diff results,
> etc.).
> 
> I don't like the idea of having things stored in the TEST_HOME,
> given that it's the TEST_HOME is inside fuego-core and I also don't
> think we should store these files in the install-dir, one option
> for cleaning this up would be:
> 
> - Add a new argument to test_run.sh to separate the assets dir
> (TEST_HOME/screenshots)
> from where the captured screenshots and diffs will be stored), having
> something like:
> 
>    $ test_run.py" "${fuego_release_dir}/fuego" -a ${assets_path} -w
> ${output_path}
> 
> What do you think?

OK - I understand the design better.  I didn't realize that we have artifacts we
are saving from the run.  What you have proposed looks good to me.
There are pros and cons to putting the reference assets in the same directory
as the artifacts generated from the run.  It's easier for users to look at them and
see what happened if they're all in the same directory.  But it's also nice to
not replicate the reference assets for every run of the test.

I would put the generated artifacts in the run directory (the log directory), not
the build directory.  (I didn't check to see where you're putting them now).

In any event, specifying the reference asset path seems like a good idea.

 -- Tim

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas
  2018-04-26 13:09     ` Guilherme Camargo
  2018-04-26 13:39       ` Gustavo Sverzut Barbieri
@ 2018-04-26 18:07       ` Tim.Bird
  1 sibling, 0 replies; 30+ messages in thread
From: Tim.Bird @ 2018-04-26 18:07 UTC (permalink / raw)
  To: guicc; +Cc: fuego



> -----Original Message-----
> From: Guilherme Camargo
> 
> On Wed, Apr 25, 2018 at 8:28 PM, <Tim.Bird@sony.com
> <mailto:Tim.Bird@sony.com> > wrote:
> 
> 
> 	> -----Original Message-----
> 	> From: Guilherme Campos Camargo
> 	>
> 	> The new mask-img-path argument that has been added to
> CheckScreenshot
> 	> can be used to provide a mask image that will determine which
> parts of
> 	> the screenshots will be compared.
> 	>
> 	> The mask needs to be a Black and White .png image in which the
> Black
> 	> areas will be ignored on the comparison, while the White areas will
> be
> 	> compared normally.
> 
> 	How do you generate these?
> 
> 
> 
> ​Actually, it's pretty straightforward with Gimp (or any other image editor with
> layers).
> 
> I just add a new transparent layer above the original image, paint black the
> areas
> that I want to remove​ (in the new layer) - right on top of the original image -
> then
> add a new white layer in between the two and export.

That seems pretty easy.
> 
> 
> 	It seems like it would be nice to be able to use a chroma-key color
> (like magenta),
> 	for the to-ignore regions, and have all other colors be interpreted as
> white,
> 	for the "to-compare" regions.  This would allow creation of the masks
> by
> 	just drawing a rectangle of magenta on the reference image.
> 	That is, you could pull in the mask image in the above-described
> format, and
> 	convert all colors but magenta to white, and magenta to black, and
> then
> 	use the mask image like you are doing now.​
> 
> 
> ​That's an option, but the problem with that is that magenta would need to
> be a prohibited color on the original image.
Yeah, but magenta is a fairly seldom-used color, and even if some other screen
pixels ended up not being compared, I don't think it would be a problem.
Most images are not going to have contiguous blocks of magenta in them.

> 
> 
> 
> 
> 	But maybe you have a workflow where it's already easy to create the
> masks.
> 
> 	One good thing about this is you could look at the mask and more
> easily tell
> 	what part of the image you intend to ignore.

Having the rectangle mask in the context of the original image, viewable without
a special tool, seems like it would be nice.

However, this was just brainstorming on my part.  If I find myself making lots
of masks and I think the workflow is cumbersome, I'll just try some things myself, to see
if it has the advantages I'm postulating.

> 	 -- Tim
> 
> 
> 	>
> 	> A new CheckScreenshot has been added to check the
> 'page_generated'
> 	> element of the Jenkins web-interface. That element is positioned
> in the
> 	> lower right corner of the page and contains a DateTime field that
> 	> changes continually.
> 	>
> 	> The assets used as reference and mask have also been included in
> this
> 	> commit.
> 	>
> 	> Signed-off-by: Guilherme Campos Camargo
> <guicc@profusion.mobi <mailto:guicc@profusion.mobi> >
> 	> ---
> 	>  .../screenshots/page-generated-mask.png       | Bin 0 -> 183 bytes
> 	>  .../screenshots/page-generated.png            | Bin 0 -> 4583 bytes
> 	>  .../Functional.fuego_release_test/test_run.py |  25
> +++++++++++++-----
> 	>  3 files changed, 19 insertions(+), 6 deletions(-)
> 	>  create mode 100644
> 	> engine/tests/Functional.fuego_release_test/screenshots/page-
> generated-
> 	> mask.png
> 	>  create mode 100644
> 	> engine/tests/Functional.fuego_release_test/screenshots/page-
> 	> generated.png
> 	>
> 	> diff --git
> a/engine/tests/Functional.fuego_release_test/screenshots/page-
> 	> generated-mask.png
> 	> b/engine/tests/Functional.fuego_release_test/screenshots/page-
> 	> generated-mask.png
> 	> new file mode 100644
> 	> index
> 	>
> 0000000000000000000000000000000000000000..a565ba4dd4b8cbd5ab4be9c70
> 	> 50d8046f63e238a
> 	> GIT binary patch
> 	> literal 183
> 	> zcmeAS@N?(olHy`uVBq!ia0y~yU{nCI`8e2sWPi}XA|S<<<n8Xl@E-
> 	> &h>|H(?D8gCb
> 	> z5n0T@z%2~Ij105pNB{-
> 	> dOFVsD*&ne8i79f=*0Tx$3b}Z?IEG~0dwXRgFM|Qk;SIa~
> 	> z|9=xV!PeoVu!%-)b%bgsN6kYo1_^-
> 	> #2L?7434sPiMmClN2OtF(I`D*X)n68A7Wv8g
> 	> QK=T+pUHx3vIVCg!0HVY$sQ>@~
> 	>
> 	> literal 0
> 	> HcmV?d00001
> 	>
> 	> diff --git
> a/engine/tests/Functional.fuego_release_test/screenshots/page-
> 	> generated.png
> 	> b/engine/tests/Functional.fuego_release_test/screenshots/page-
> 	> generated.png
> 	> new file mode 100644
> 	> index
> 
> 	>
> 0000000000000000000000000000000000000000..9b16d92e5cc3ec6c9f57723115
> 
> 	> e21f8f23f5eab9
> 	> GIT binary patch
> 	> literal 4583
> 	>
> zcmV<D5g6`?P)<h;3K|Lk000e1NJLTq00AHX000gM0ssI205Dc1000rLNkl<Zc%1E
> 	> 8
> 	>
> zXF!ulx1Kizl2Aek5Q>6yWEH^%Hae&v0wOEc#j@BC*Ig?jNE5pvh=LRmP?|~+0
> 	> RfS2
> 	> z`>d-
> VDxDBI2}vLcy!Xer*+^6O?sxCE_xk+doik^iIp<6}GXY_98Q?p<<2(L6zyJWy
> 	>
> z&+)%)(%+A7^^47BFPdjDz!~`4nI|9_&GSuzGM@67^1s~U1hrq;a6C&U_VJ7T
> 	> ezDUD
> 	>
> z^e12*Qw<#p<GG0g0EC_kPfJM$0FX!|Ep5%6UfXqav=IcEz~O%ua_;1o6qkgY4
> 	> F21l
> 	> z+T*pi`fbha>@-
> 	> nP(Z9yf(cW?D#F@8ms(X5R(=$@!<>jE9#bTW~dG2A=qoJYU8Ab+s
> 	>
> ze%mc4M;VXg@uMf#t|q>#sm0@Q#wK%jdTl3@$*?Lm`s&TJTYUbIm5t@D-
> 	> 8)1?L;wIG
> 	> zK__0kc-
> 	>
> 7L};^XISZDT12OMF~H#_cR7gQ>1D#nWrM{tx;iB6i}~sprpMwludKIqGd?
> 	>
> zZ7GnKm6V5vUFvA>Fqkp@p!Yt?C{xo@Z{Eqe+uYnjmLe~6uv@ob4F-
> 	> c@v)SGUeBRf7
> 	>
> z;BW?1RaG~8xEq_y9X&>6CFK{xFVoxTGYkz59NH`QM~DCbv{u@>)$Rr}rk^@{
> 	> d_=?h
> 	>
> zg~h%;{wRv#a5!aUrJpvfx3aeE?d@||>ZGWocs=DB27`fdcC>fF;cz+mnIjl%ES8=
> 	> +
> 	>
> zd)#2gbXZKekycgt@JF|m`+hw*dO@b&PR5IfL`TJzmsK$O7*s_CODl`jYgb`lwV
> 	> nNv
> 	> z`=$4i)032yl>+>NhKGm88_WM#peQ;tH1t2&BdwM8;`vJiLCQ-
> 	> jCf<OB!RPaZiwFi|
> 	> zGSAq@&wIq^UX8t0U*8xTe<e9R5k-
> 	> e1!Y&UFjXNmq?H%?GORpwGU$}U_si`?UGy;xb
> 	> ze%`&z+gazr&Rk81e*N-
> 	> w;<f8guC1fB@4&B<RVNAeSyobhJux{b#Q#oShOx<9AMc~1
> 	> z<Ef>s`P;r<RMjR63tL-
> 	> S1C9o7+Wgba%oH+NGSL6n=xB6xb#L40aU(79(Bb{*sW<QD
> 	>
> z<P8rG<8ZjO>(|6yi%z?p?6%g`_ppC&Z|{hPtu3tq{=pkJZMdDCMwXHc3J4LNo
> 	> M+F3
> 	> z>gZ~Z7Zzm|<-
> 	> !MfIr*6`uFfGrC+Tf;D92(kFJHWZMm{Jmm6IQL=NgSUb7$YXf487G
> 	> z_e$Jl3<i^vpIK0xTTq-UCNAc=-
> 	> HX;r3qBr@nR7EZB#^;i*1mg>gUt{G!QpUHQsm`M
> 	> z4!ODc91bTnIX&l2E~Ag3tgN(S*EXY>h6sWT4-
> 	> ZF2#b#w>ixLQH*RMW(;%sg~Hi<;)
> 	>
> z>gqm!=EC!5&qWD@6)sM$t6bprAOM5GI1zIC+0*B8auhQQ)5?mf%U3P}0IJ_
> 	> shlNJe
> 	> zeXN(0qioyZG1p`c0Km^DKuVh2P~X_q*(D(<;d}J3yn_71%Jp>(tu$I-
> @X^u-
> 	> Wx03r
> 	>
> zh0ShmYKgjZrN5u$x?(k1QZh0og3sq)jlFg+zhHnfuwdbQ&)qwSL?Vi!F<0WUGP
> 	> 3a^
> 	>
> zBI`D+5loSH?`kW{tKg11BG&nN1$t9;b@g=ea`VkC&7jcVH;^PI*4)(0WH8B6<oy
> 	> SK
> 	>
> zQ=lp|HZ*x|_i|d{SX}f#OI!1h&jG=5qEKWVotO9a_KhHKYojk%IFBMnK@h~k(
> 	> mXjS
> 	>
> z6$Rt2VLSUJf^QdB=lHk;Xn$@_{&FV=4Gnb!K~}GGi;Ibej;vVe3;@7kal#}tTAPlp
> 	>
> zwyv%Yf*|(GmR^m$#$+<3rKN>+oL!s%0C*f;SXfwaUvH|exuqEvi}l#HWu>zly
> 	> ^T&)
> 	>
> zqzZ@5W7`&}EGsLs$aG=#+nU8oY(zvvX3jE#rm9R*;c~g19bMAW(kvD$E+&5U
> 	> S~qEF
> 	>
> zsltN$dV0EM7N$5H&SUGAmCkPM^meKu6#!6HQjWo3Oy(Is``7dFB7?)>L_|a!
> 	> otH<1
> 	>
> zN7mKV>+0zO0IY4S^79I&8%#qG<X&EZ?P8l7Nh#woi~t6Mf!PWUhXV+}<MF
> 	> VVnw-w%
> 	>
> z@`6JA2?PQFKuk<*_a09aML(M~!^6W5ic93=<cLJ#WYtNRqQZ0XGaXkf_xBCta
> 	> 5w;f
> 	> z{Jeq(#U)WOm*Nv+o;-
> 	> dETcIfGd)QBbDxZ4m#)a^+S((|D<qt>fVd0?>0D#n6*F#PQ
> 	>
> z6&2ox!tU;#y?YKgIWNz+lfL`cUBQ7NU0vO<s;csl_mP9~NwHJ)_2OdUCsr;itvG
> 	> ae
> 	> ze@a@CmX@Z#Y$q2-
> 	> f8Rhhn>}f=%9gF0XU;NANl${iPFzg<@2}s4MT91&Co!1Jm@9E8
> 	> zist7P6c*eMzZ7~kA-b%zTwr-
> 	> qV^d~E_LxWdz5GH8OEYU*t49x?2#f&$R8~|SIeIwy
> 	> zYNXLj!(%}wU|R-
> *F*GzBmk=$yx+cIf2fN3Qo^*D0F&K=Zg8OrfXMbgW`})-
> 	> zH8nN(
> 	>
> z{qbX+mX;=LsG+Ie+R{2WI5=KIGxJ5=UES5yHC!$?J1b|ZzMiC{<k$4rP~WJbsSb
> 	> Nl
> 	>
> zpem5blJ)fsD2k35Iuu3UzNwxvMOEOA*Umk5w#&D;Z#9{3tgbNy0Knl4BwtTu
> 	> G8ki%
> 	>
> z*EOIh%HeP#FJJN8wf$=>tSGPIa=9uh%CKl|VOCjQIXF1jMr)(d+Th{wH9QmG$
> 	> >XOB
> 	> z7tJ4WQ$P^p(*nDfU%1@P+1Apow505y_dW~;V`*g}D=UM=VjY&-
> 	> ixLP;P0c8Z78eye
> 	> zIyop%6^KOQx(#cfSsy-ptgCC-
> 	> y8UNSQBkTQ)p><u$%6+Y7TEow2OHei5s5^qBGqB}
> 	>
> zGAJy(S2R^$&)m`shr`V<G%zqUcv$raR@vBE$;e0p0Orh{T~kv#v9h&|B~1De1h
> 	> KZU
> 	>
> zf@UvsuoEQ^nwwijrz=?*cQ*g*E+<DJ5{d4cHWc44K~c1*pvck5K~YJOL?XFwT
> 	> rY^Y
> 	> zrIkhc?PMGdH-hBV%h$A4nyHzouC9)<ic&#-
> 	> p`as6D|2YPi>veVXD@nsdQlVw2=b$w
> 	> z3pDa;224X!gG3@ZJFK*`UEb5vw`QH&=p%GADl4iA?-
> 	> g$T*&Qkma0W<XBv>UbCI$fD
> 	> zjQ?^YEhB9*-
> 	> `Hc*c1u&+jN4hi{Oa{JJpusO95#tW5){S6#n~)27$<a5m!mNl%o4lB
> 	> z0&@Q#zo?jqZ96<>%`$?)p-
> 	> ^N?DvBnnP8K3Z{v>C!0RZuF31;S|%73hxF+ppkIW2Qp
> 	> zV!bTLKX`}dR+ULAP%cT9)SsqbRq-
> 	> ${_nx_h=~ucrF&KRe3T2E`^ON(7Oc!qV+$ttU
> 	>
> zA`*#El6yBlJ@r<1cQ*!u>Fnt2>+J&obar+LSmfp9;Rl^Y=MHi=tlbEUJRV;|Q(d?Z
> 	>
> zCX+cZFrc6y55p(8tkdcA*RS5JU*iso9L~Vhsd}G6ASnTr2?WBx0B2%l85wE8tj)Q
> 	> T
> 	> zdn-
> NTPqTYQre6L1{Y)k^C?Euj#R33O6y**M0suNYx~NnI$fKyF^u>VY-
> 	> OZml%TP*+
> 	> zjH0Nel||m&eAiVju(=?uA`l2-
> 	> Vq%@0odi*Wgt+)OJ%k(&J}Dw9l6Chs9*>K>6n*%h
> 	> z@9A?Vpv~jqUr(Qf1f2*9379l_(x>PV32?3h0Qy-
> 	> h005Cl7_a$_q?FQ<viQVUWfi5O
> 	> z!s5Mq_D5fhl$DkJqNyYzNze-bfYskGSk)tta4o5<wEWDuQ-
> 	> Wy*0FaU*OG%MeuU)la
> 	>
> z?M8JCbwi^WBgk1S7WgAQhU9f0>q|?^E=7h5Ki`esx#Sh(!y?WT2%@8pVq075
> 	> oSfXo
> 	> zhDQHDzkz|VHys=f$LI3}%zQq7^gb&9S&H1z(J=-
> 	> I{39wVLZSQ_%BIHV@X&~JVW%}T
> 	> z)KL^&wQ?==ghHVRSm^XmS4KGnISEPe_{7-pb^-
> 	> |r2_li$)<&0*kU$Wmz5Nq|yuAF(
> 	> zSw?>TJ{T<gwE#@8xlf=hh-
> 	> `CH%Y}2{VVBNBv)x?R!F!8v=*7jwB_t&L1N}6$G@u+t
> 	> zP)?rG-
> 	>
> O~fVyM^*SFxKF4IGp=M5BPk(i_?z?f^fM!CX@N@U9FCeHUNM|YlCXNy}kYY
> 	>
> z{j##MtbW$FCLwio^}BZOl#(I?0300cH*NTt$KzqKSU3=)*`7apar8*Qk)wyFP5(h
> 	> q
> 	> ztD&J@Tl;?g!g&aSe0cvsNm*%vMgH-
> 	> _$N3B9sj5vz5X8pT>f(in_q88nWMsZ*x!M%f
> 	> zCy$>F4-
> 	>
> aE77<wDMua7Zhikfh2;YafI)ST?xbD^gwa(~V=VFeULX|y)O&*f@URG&Wi
> 	> z7xbFmMrSaXYHF&F9zO2s=w7>O1A-uIHiyUK{^+tMIW2L-
> 	> 48&rw6pHMa)6#g}+%u=o
> 	> zQ7Ce%YLjcK-;L=Pm8#I%N;8^i2<>TYX;q*qj2DDivu9@9&iZ-
> 	> lW>HZB3>dU|>?OhG
> 	>
> zuth{gV9r`mR!M86LAj~<qKw;F?eumIhm#PW1SvE%H53)8mo7%K+3caAp}LRt
> 	> Z+?F>
> 	>
> zA~7>HTNodgz~OM{ZS<_nY$&v_GJo;>MPXqPkH_P3xi4S56uk3{#@8sb*ep?k
> 	> C^WmW
> 	>
> zyo%OJgH}jMOSRGHg6BJXhozS;Mz+z~005mGoe!%Xp(tu*VRrXUF1%`{rKAh&
> 	> udRI_
> 	>
> zAD1xVB2ivefgnh1eAJb=$f(%MR}*8V8%!(6`;;XX7ZumkyyNkBS0ZC)7#av4uw!
> 	> Cy
> 	>
> zaFEC40s#0tp5Tt8tEZEDHxC~ESs8aGPo9Lw;{gDtPMi(!3mOsLH^0C2KH&4)zCA
> 	> xo
> 	> z)8}%zTrL;NZ5CT+W@I<iH+FV*-AGKawy}h>jmP5+4i2Iy%I6Oa4h}-
> ^rt0felvlPi
> 	> zw+syp6&4ir_p>Jbky8mUW<zre)9SZ16=jty7Ara`)@Y_7e3TLn-
> 	> OaRHDamQajt7#Z
> 	>
> z$b*A}a4f3dR+ki)c6N4kc6P<Z#M4^a^!4=s04x?OBK&esPcI~Y`=+|0tdh-
> 	> UM@PjP
> 	>
> z8qJVbkhfWEeeHT248Du2^R(&HA|k`SDbvH_@yAac^YJ}2K~i90WuAOJ<^B5)0
> 	> |Nt(
> 	>
> zA3Z55EH*V=IG(bjvjZNF^Eu>OQ&YoYu^Q?d&z}u@{p$BWUlE11b#%0?Z7est
> 	> dr+xV
> 	>
> zO)U*|^(j!ncCmGHQ}bqb4^aZaZMADjaVZXm!(cE0!AHYGBUZbv<8pbECaY}R{
> 	> L_f;
> 	>
> zckJ4B^7xsRPHu8?6jQTB@Y#n#kv)0#ctqIcbEhw0u^1g)?LEKkhQ<inBgo6Ys4O
> 	> sB
> 	>
> zK|w)FTXTxK8myT$%jlMZyraE~grr1La{R{4KPAK`{o=K^x2IQ5o?^euZm#hh8(X
> 	> WU
> 	> z#-
> <$}yQF2L=Pxip5Co?EjSY=inRiB9f%0?jEnaFvRip}pD^@y%o(<cwbrXt$B}*5d
> 	> zKXak(W4(^9HoVaO;l&usjL+v=n}2!(T(xp7002JBdi~;g{_KU-
> uIq+|hPAXceU5rV
> 	> zo{si*lX=F-=TzC5cbLq6?}JBRQCwU+Gv_7_hqGO5-
> 	> O}9Rx!nu?4kqwox96{~UcLqZ
> 	>
> z91A>tEbw^9i6E2t#!f37J32ad?cCGP>{nEz`uHAFR8kx<G4^=vg%8}p0U^NwA;(
> 	> Vz
> 	> z8=K5kR#Eo$IdC!jaz}f|3`2v%M-IYP!lAoyJ+-
> r=bKUAs6Jy>2lb~b%2nLgsc%$xP
> 	>
> zJr;{qo1z*J><8~%@R7rQnVqz>l(LHQ;Ufpb!!C9Bbr{T;?(K5`gTW981o&7703eY_
> 	>
> zcsyQKR%S$CKc7UO$>E2#w$>QcLU3F2Bag>Be9*VMt6ND)(R25XnX`>P;};qQF
> 	> )^{Q
> 	>
> zh|nuhF^Bg1^!4^BDp4)1EHt$=kZ=80DZ$xYU0ri%zfbDT>l0rLMkD!p>bsg+AHP
> 	> H2
> 	>
> z)asuCf8W5V`g$&|EB+niLqkLE>o;GCy$pXZ{*M2AeB1j8kH>ph^=No_n8{?uM
> 	> 8z&x
> 	>
> zIR7iJ7EO)KA3l6UQM9q4>DJAR1q<i>C2Rk`1B=DRC&qqXUH>4A`zkUfP!x@gj`
> 	> #Ns
> 	> z!sBtX=givTvGFStUlyz1-
> 	> !Hhgr<Wu~va?@eW?}l5to=ve|IWXdeaC+v{{{QoPnL8U
> 	> RPL==w002ovPDHLkV1i3q$F=|f
> 	>
> 	> literal 0
> 	> HcmV?d00001
> 	>
> 	> diff --git
> a/engine/tests/Functional.fuego_release_test/test_run.py
> 	> b/engine/tests/Functional.fuego_release_test/test_run.py
> 	> index ba840b6..5fa2ca1 100755
> 	> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> 	> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> 	> @@ -153,7 +153,7 @@ class CheckText(SeleniumCommand):
> 	>
> 	>
> 	>  class CheckScreenshot(SeleniumCommand):
> 	> -    def __init__(self, locator, pattern, ref_img, diff_img=None,
> 	> +    def __init__(self, locator, pattern, ref_img, mask_img=None,
> 	> diff_img=None,
> 	>                   expected_result=True, threshold=0.0,
> 	>                   rm_images_on_success=True):
> 	>          def add_suffix(filename, suffix):
> 	> @@ -166,6 +166,7 @@ class
> CheckScreenshot(SeleniumCommand):
> 	>          self.pattern = pattern
> 	>          self.locator = locator
> 	>          self.reference_img_path = ref_img
> 	> +        self.mask_img_path = mask_img
> 	>          self.expected_result = expected_result
> 	>          self.threshold = threshold
> 	>          self.rm_images_on_success = rm_images_on_success
> 	> @@ -178,8 +179,8 @@ class
> CheckScreenshot(SeleniumCommand):
> 	>          self.test_img_path = add_suffix(ref_img, 'test')
> 	>
> 	>      def compare_images(self, current_img_path,
> reference_img_path,
> 	> -                       diff_img_path, threshold):
> 	> -        cmd = ['magick',
> 	> +                       mask_img_path, diff_img_path, threshold):
> 	> +        cmd = ('magick',
> 	>                 'compare',
> 	>                 '-verbose',
> 	>                 '-metric',
> 	> @@ -188,10 +189,14 @@ class
> CheckScreenshot(SeleniumCommand):
> 	>                 'Red',
> 	>                 '-compose',
> 	>                 'Src',
> 	> +               '-read-mask' if mask_img_path else None,
> 	> +               mask_img_path if mask_img_path else None,
> 	>                 current_img_path,
> 	>                 reference_img_path,
> 	> -               diff_img_path
> 	> -               ]
> 	> +               diff_img_path,
> 	> +               )
> 	> +
> 	> +        cmd = list(filter(None, cmd))
> 	>
> 	>          LOGGER.debug('  Comparing images...')
> 	>          LOGGER.debug('    cmd: $ %s', ' '.join(cmd))
> 	> @@ -246,6 +251,7 @@ class
> CheckScreenshot(SeleniumCommand):
> 	>
> 	>          result = self.compare_images(self.test_img_path,
> 	>                                       self.reference_img_path,
> 	> +                                     self.mask_img_path,
> 	>                                       self.diff_img_path,
> 	>                                       self.threshold)
> 	>
> 	> @@ -743,7 +749,14 @@ def main():
> 	>          CheckScreenshot(By.ID, 'tasks',
> 	>                          rm_images_on_success=True,
> 	>                          ref_img='screenshots/side-panel-tasks.png',
> 	> -                        threshold=0.1)
> 	> +                        threshold=0.1),
> 	> +
> 	> +        # Compare screenshot of an element of Jenkins UI ignoring an
> area
> 	> +        CheckScreenshot(By.CLASS_NAME, 'page_generated',
> 	> +                        rm_images_on_success=False,
> 	> +                        mask_img='screenshots/page-generated-mask.png',
> 	> +                        ref_img='screenshots/page-generated.png',
> 	> +                        threshold=0.1),
> 	>      ]
> 	>
> 	>      if not execute_tests(args.timeout):
> 	> --
> 	> 2.17.0
> 	>
> 
> 	> _______________________________________________
> 	> Fuego mailing list
> 	> Fuego@lists.linuxfoundation.org
> <mailto:Fuego@lists.linuxfoundation.org>
> 	> https://lists.linuxfoundation.org/mailman/listinfo/fuego
> <https://lists.linuxfoundation.org/mailman/listinfo/fuego>
> 
> 


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list
  2018-04-26 13:31     ` Guilherme Camargo
@ 2018-04-26 18:17       ` Tim.Bird
  0 siblings, 0 replies; 30+ messages in thread
From: Tim.Bird @ 2018-04-26 18:17 UTC (permalink / raw)
  To: guicc; +Cc: fuego



> -----Original Message-----
> From: Guilherme Camargo
> On Wed, Apr 25, 2018 at 8:17 PM, Tim Bird wrote:
> 
> 
> 	Looks good.  My only comment (which is not specific to this patch or
> test case)
> 	is that it would be good if each 'Check...' test in
> COMMANDS_TO_TEST
> 	was an individual test case for the full release test.
> 
> 
> 
> Tim, not sure if I got it right. You mean that we should wrap each one
> of the 'Check...' tests in a python script and call them individually
> from within fuego_test.sh?

No. Nothing like that.  Just add some extra text to the output
(like I described in wrapping the LOGGER.info output), and so that
the test yields a testcase per major comparison operation.

e.g. add to CheckScreenshot a test description, and
have it output the actual result of the testcase in TAP format.
See below:

> 	> -----Original Message-----
> 	> From: Guilherme Campos Camargo
> 	> This test will take a screenshot of 'tasks' element from the side-
> panel
> 	> of the Jenkins UI (that contains the 'New Item', 'People', 'Build
> 	> History' and the 'Manage Jenkins' buttons) and compare it with an
> image
> 	> that's stored in the workdir, called 'side-panel-tasks.png'. The
> allowed
> 	> threshold is 0.1 (10%).
> 	>
> 	> Signed-off-by: Guilherme Campos Camargo
> <guicc@profusion.mobi <mailto:guicc@profusion.mobi> >
> 	> ---
> 	>  engine/tests/Functional.fuego_release_test/test_run.py | 6
> ++++++
> 	>  1 file changed, 6 insertions(+)
> 	>
> 	> diff --git
> a/engine/tests/Functional.fuego_release_test/test_run.py
> 	> b/engine/tests/Functional.fuego_release_test/test_run.py
> 	> index b380acd..2075862 100755
> 	> --- a/engine/tests/Functional.fuego_release_test/test_run.py
> 	> +++ b/engine/tests/Functional.fuego_release_test/test_run.py
> 	> @@ -725,6 +725,12 @@ def main():
> 	>                'docker.default.Functional.hello_world"]'),
> 	>          CheckText(By.ID, 'executors',
> 	>                    text='docker.default.Functional.hello_world'),
> 	> +
> 	> +        # Compare screenshot of an element of Jenkins UI
> 	> +        CheckScreenshot(By.ID, 'tasks',
> 	> +                        rm_images_on_success=True,
> 	> +                        ref_img='screenshots/side-panel-tasks.png',
> 	> +                        threshold=0.1)
Maybe add another parameter 'test_id', so that this looks like:
CheckScreenshot(By.ID, 'check tasks panel', 'tasks',
        rm_images_on_success=True,
       ref_img='screenshots/side-panel-tasks.png',     
       threshold=0.1)

Which would end up emitting:
ok 4 check tasks panel

(if the test passed, and this happened to be the 4th test, that is).

Then modify the parser.py to read all the TAP entries (it may already
do this).

Right now there's only a single testcase output for the entire release test,
but it would be nice to get more detailed results from this test (on a
testcase-by-testcase basis).

 -- Tim

I
> 	>      ]
> 	>
> 	>      if not execute_tests(args.timeout):
> 	> --
> 	> 2.17.0
> 	>
> 
> 	> _______________________________________________
> 	> Fuego mailing list
> 	> Fuego@lists.linuxfoundation.org
> <mailto:Fuego@lists.linuxfoundation.org>
> 	> https://lists.linuxfoundation.org/mailman/listinfo/fuego
> <https://lists.linuxfoundation.org/mailman/listinfo/fuego>
> 
> 


^ permalink raw reply	[flat|nested] 30+ messages in thread

end of thread, other threads:[~2018-04-26 18:17 UTC | newest]

Thread overview: 30+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2018-04-24 17:24 [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 01/14] Add a SeleniumCommand that compares screenshots Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 02/14] Minor style fix Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 03/14] Add a CheckScreenshot command into the COMMANDS_TO_TEST list Guilherme Campos Camargo
2018-04-25 23:17   ` Tim.Bird
2018-04-26 13:31     ` Guilherme Camargo
2018-04-26 18:17       ` Tim.Bird
2018-04-24 17:24 ` [Fuego] [PATCH 04/14] Increase the size of the webdriver viewport Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 05/14] Allow working_dir and install_dirs to be different Guilherme Campos Camargo
2018-04-25 23:20   ` Tim.Bird
2018-04-26 12:53     ` Guilherme Camargo
2018-04-26 18:00       ` Tim.Bird
2018-04-24 17:24 ` [Fuego] [PATCH 06/14] Prevent exception NameError when removing container Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 07/14] Add an example reference screenshot Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 08/14] Add helper script for taking element/full-page screenshots Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 09/14] Add mask-img-path argument to CheckScreenshot for ignored areas Guilherme Campos Camargo
2018-04-25 23:28   ` Tim.Bird
2018-04-26 13:09     ` Guilherme Camargo
2018-04-26 13:39       ` Gustavo Sverzut Barbieri
2018-04-26 18:07       ` Tim.Bird
2018-04-24 17:24 ` [Fuego] [PATCH 10/14] Add a README.md Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 11/14] Allow Full viewport Screenshots Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 12/14] Allow compare-with-mask to work with ImageMagick 6 and ImageMagick 7 Guilherme Campos Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 13/14] Improve logging Guilherme Campos Camargo
2018-04-25 23:38   ` Tim.Bird
2018-04-26 13:11     ` Guilherme Camargo
2018-04-24 17:24 ` [Fuego] [PATCH 14/14] Set logging level to INFO by default Guilherme Campos Camargo
2018-04-24 17:40 ` [Fuego] [PATCH 00/14] Add screenshot test to fuego-release-test Guilherme Camargo
2018-04-25 23:10 ` Tim.Bird
2018-04-26 12:14   ` Guilherme Camargo

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.