summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndres Gomez <agomez@igalia.com>2020-10-16 16:08:32 +0300
committerAndres Gomez <agomez@igalia.com>2020-11-02 22:22:33 +0200
commit1c9d03dcb310ad3f310ecf4a8a54eb9c15c678f9 (patch)
treec032705b0474339cefbfe3cf72bc5054c06c5ad3
parent536b568794406dcf72f8c06542d2e903b867155d (diff)
unittests: add tests for framework/replay
v2: - Added new dependencies to the CI build. v3: - Updated build tag in .gitlab-ci.yml. v4: - Use OSError instead of PIL.UnidentifiedImageError for backwards compatibility in image_checksum test. v5: - Do not use yoda style comparisons (Dylan). - Use newly added compare result enumeration. - Deal with AttributeError instead of TypeError from recent changes in query_traces_yaml. v6: - Use textwrap.dedent to enhance readability. Signed-off-by: Andres Gomez <agomez@igalia.com> Reviewed-by: Dylan Baker <dylan@pnwbakers.com> Part-of: <https://gitlab.freedesktop.org/mesa/piglit/-/merge_requests/353>
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab-ci/debian-install.sh4
-rw-r--r--tox.ini7
-rw-r--r--unittests/framework/replay/backends/test_apitrace.py500
-rw-r--r--unittests/framework/replay/backends/test_gfxreconstruct.py374
-rw-r--r--unittests/framework/replay/backends/test_package.py93
-rw-r--r--unittests/framework/replay/backends/test_renderdoc.py196
-rw-r--r--unittests/framework/replay/test_compare_replay.py313
-rw-r--r--unittests/framework/replay/test_download_utils.py110
-rw-r--r--unittests/framework/replay/test_image_checksum.py41
-rw-r--r--unittests/framework/replay/test_options.py62
-rw-r--r--unittests/framework/replay/test_query_traces_yaml.py202
12 files changed, 1900 insertions, 4 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4d383ca7e..4a1403a4f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,7 +14,7 @@
# repository's registry will be used there as well.
variables:
UPSTREAM_REPO: mesa/piglit
- DEBIAN_TAG: "2020-04-21"
+ DEBIAN_TAG: "2020-10-16"
DEBIAN_VERSION: buster-slim
DEBIAN_IMAGE: "$CI_REGISTRY_IMAGE/debian/$DEBIAN_VERSION:$DEBIAN_TAG"
WINDOWS_TAG: "2020-08-18"
diff --git a/.gitlab-ci/debian-install.sh b/.gitlab-ci/debian-install.sh
index 7412b7674..cc52710bc 100644
--- a/.gitlab-ci/debian-install.sh
+++ b/.gitlab-ci/debian-install.sh
@@ -48,13 +48,17 @@ apt-get install -y \
python3-mako \
python3-mock \
python3-numpy \
+ python3-pil \
python3-pip \
python3-psutil \
python3-pytest \
python3-pytest-mock \
python3-pytest-timeout \
+ python3-requests \
+ python3-requests-mock \
python3-setuptools \
python3-wheel \
+ python3-yaml \
tox \
waffle-utils
diff --git a/tox.ini b/tox.ini
index 1488a558c..67e688f84 100644
--- a/tox.ini
+++ b/tox.ini
@@ -22,14 +22,15 @@ deps =
{accel,noaccel,generator,streams}: pytest==3.2.5
functional: pytest>=3.9
pytest-mock==1.11.2
+ {accel,noaccel}: requests-mock
pytest-pythonpath
pytest-raises
pytest-timeout==1.2.1
{accel,noaccel,streams}: jsonschema
streams: jsonstreams>=0.4.1
- functional: pyyaml
- functional: requests
- functional: Pillow
+ {accel,noaccel,functional}: pyyaml
+ {accel,noaccel,functional}: requests
+ {accel,noaccel,functional}: Pillow
commands =
{accel,noaccel}: py.test -rw unittests/framework unittests/suites []
generator: py.test -rw unittests/generators []
diff --git a/unittests/framework/replay/backends/test_apitrace.py b/unittests/framework/replay/backends/test_apitrace.py
new file mode 100644
index 000000000..aa9ea6ddb
--- /dev/null
+++ b/unittests/framework/replay/backends/test_apitrace.py
@@ -0,0 +1,500 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's apitrace backend."""
+
+import pytest
+
+import os
+import subprocess
+
+from os import path
+
+from framework import core
+from framework import exceptions
+from framework.replay import backends
+from framework.replay.options import OPTIONS
+
+
+@pytest.yield_fixture
+def config(mocker):
+ conf = mocker.patch('framework.core.PIGLIT_CONFIG',
+ new_callable=core.PiglitConfig)
+ conf.add_section('replay')
+ yield conf
+
+class TestAPITraceBackend(object):
+ """Tests for the APITraceBackend class."""
+
+ def mock_apitrace_subprocess_run(self, cmd, stdout, env=None):
+ get_last_call_args = ['dump', '--calls=frame']
+ replay_retrace_args = ['--headless']
+ if cmd[1:-1] == get_last_call_args:
+ # GL get_last_call
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] == self.gl_last_frame_fails_trace_path:
+ ret.stdout = b'\n'
+ else:
+ ret.stdout = bytearray(
+ str(int(self.gl_trace_last_call) - 50) +
+ ' glXSwapBuffers(dpy = 0x56060e921f80, '
+ 'drawable = 31457282)\n'
+ '\n' +
+ self.gl_trace_last_call +
+ ' glXSwapBuffers(dpy = 0x56060e921f80, '
+ 'drawable = 31457282)\n',
+ 'utf-8')
+
+ return ret
+ elif cmd[1:2] == replay_retrace_args:
+ # GL replay
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] == self.gl_trace_path:
+ calls = cmd[2].split('=')[1]
+ prefix = cmd[3].split('=')[1]
+ ret.stdout = b''
+ for call in calls.split(','):
+ if call != self.gl_trace_wrong_call:
+ dump = prefix + call.zfill(10) + '.png'
+ with open(dump, 'w') as f:
+ f.write("content")
+ ret.stdout += bytearray('Wrote ' + dump + '\n',
+ 'utf-8')
+ else:
+ ret.stdout = b'\n'
+ if cmd[-1] == self.gl_replay_crashes_trace_path:
+ ret.returncode = 1
+
+ return ret
+ elif cmd[2:-1] == get_last_call_args:
+ # DXGI get_last_call
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] == self.dxgi_last_frame_fails_trace_path:
+ ret.stdout = b'\n'
+ else:
+ ret.stdout = bytearray(
+ str(int(self.dxgi_trace_last_call) - 50) +
+ ' IDXGISwapChain4::Present(this = 0x3de3b0, '
+ 'SyncInterval = 1, Flags = 0x0) = S_OK\n'
+ '\n' +
+ self.dxgi_trace_last_call +
+ ' IDXGISwapChain4::Present(this = 0x3de3b0, '
+ 'SyncInterval = 1, Flags = 0x0) = S_OK\n',
+ 'utf-8')
+ return ret
+ elif cmd[2:3] == replay_retrace_args:
+ # DXGI replay
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] == self.dxgi_trace_path:
+ calls = cmd[3].split('=')[1]
+ prefix = cmd[4].split('=')[1]
+ ret.stdout = b''
+ for call in calls.split(','):
+ dump = prefix + call.zfill(10) + '.png'
+ if call != self.dxgi_trace_wrong_call:
+ with open(dump, 'w') as f:
+ f.write("content")
+ ret.stdout += bytearray('Wrote ' + dump + '\n',
+ 'utf-8')
+ else:
+ ret.stdout = b'\n'
+ if cmd[-1] == self.dxgi_replay_crashes_trace_path:
+ ret.returncode = 1
+
+ return ret
+ else:
+ raise exceptions.PiglitFatalError(
+ 'Non treated cmd: {}'.format(cmd))
+
+ @pytest.fixture(autouse=True)
+ def setup(self, mocker, tmpdir):
+ """Setup for TestAPITraceBackend.
+
+ This set ups the basic environment for testing.
+ """
+
+ OPTIONS.device_name = 'test-device'
+ self.apitrace = 'apitrace'
+ self.eglretrace = 'eglretrace'
+ self.wine = 'wine'
+ self.d3dretrace = 'd3dretrace'
+ self.gl_trace_path = tmpdir.mkdir(
+ 'db-path').join('glxgears/glxgears-2.trace').strpath
+ self.gl_last_frame_fails_trace_path = tmpdir.join(
+ 'db-path',
+ 'last-frame/fails.trace').strpath
+ self.gl_replay_crashes_trace_path = tmpdir.join(
+ 'db-path',
+ 'replay/fails.trace').strpath
+ self.gl_trace_calls = '1211,1384'
+ self.gl_trace_last_call = '1413'
+ self.gl_trace_wrong_call = '1414'
+ self.output_dir = tmpdir.mkdir('results').strpath
+ self.dxgi_trace_path = tmpdir.join(
+ 'db-path',
+ 'Wicked-Engine/Tests:Cloth_Physics_Test.trace-dxgi').strpath
+ self.dxgi_last_frame_fails_trace_path = tmpdir.join(
+ 'db-path',
+ 'last-frame/fails.trace-dxgi').strpath
+ self.dxgi_replay_crashes_trace_path = tmpdir.join(
+ 'db-path',
+ 'replay/fails.trace-dxgi').strpath
+ self.dxgi_trace_calls = '235747,257964'
+ self.dxgi_trace_last_call = '273345'
+ self.dxgi_trace_wrong_call = '273346'
+ self.results_partial_path = path.join('trace', OPTIONS.device_name)
+ self.m_apitrace_subprocess_run = mocker.patch(
+ 'framework.replay.backends.apitrace.subprocess.run',
+ side_effect=self.mock_apitrace_subprocess_run)
+ self.tmpdir = tmpdir
+ self.mocker = mocker
+ mocker.patch.dict('os.environ')
+ self.env = os.environ
+ self.env.clear()
+
+ @pytest.mark.raises(exception=exceptions.PiglitFatalError)
+ def test_init_unsupported_trace(self):
+ """Tests for the init method.
+
+ Should raise an exception in case of creating with an unsupported trace
+ format.
+
+ """
+ test = backends.apitrace.APITraceBackend('unsupported_trace.gfxr')
+
+ @pytest.mark.parametrize('option, apitrace, eglretrace', [
+ (0, '/env/apitrace', '/env/eglretrace'),
+ (1, '/config/apitrace', '/config/eglretrace'),
+ (2, 'apitrace', 'eglretrace'),
+ ])
+ def test_dump_gl_options(self, option, apitrace, eglretrace, config):
+ """Tests for the dump method: basic with options.
+
+ Check basic GL dumps with different configurations. No specific output
+ directory is provided and leaving to the method to figure out the last
+ call from the trace file itself.
+
+ """
+ self.apitrace = apitrace
+ self.eglretrace = eglretrace
+ if option == 0:
+ self.env['PIGLIT_REPLAY_APITRACE_BINARY'] = self.apitrace
+ self.env['PIGLIT_REPLAY_EGLRETRACE_BINARY'] = self.eglretrace
+ elif option == 1:
+ config.set('replay', 'apitrace_bin', self.apitrace)
+ config.set('replay', 'eglretrace_bin', self.eglretrace)
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_gl_output(self):
+ """Tests for the dump method: explicit output directory.
+
+ Check a basic GL dump, specifying the output and leaving for the method
+ to figure out the last call from the trace file itself.
+
+ """
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ output_dir=self.output_dir)
+ assert test.dump()
+ snapshot_prefix = path.join(self.output_dir,
+ path.basename(trace_path) + '-')
+ m_calls = [self.mocker.call(
+ [self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_gl_calls(self):
+ """Tests for the dump method: explicit valid calls.
+
+ Check a basic GL dump, specifying valid calls to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.gl_trace_calls
+ trace_path = self.gl_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_apitrace_subprocess_run.assert_called_once_with(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_gl_wrong_call(self):
+ """Tests for the dump method: explicit invalid call.
+
+ Check a basic GL dump, specifying an invalid call to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.gl_trace_wrong_call
+ trace_path = self.gl_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_apitrace_subprocess_run.assert_called_once_with(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_gl_last_frame_fails(self):
+ """Tests for the dump method: the call to figure out the last frame fails.
+
+ Check a basic GL dump. The call to figure out the last frame fails.
+
+ """
+ calls = '-1'
+ trace_path = self.gl_last_frame_fails_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_gl_replay_crashes(self):
+ """Tests for the dump method: the replay call crashes.
+
+ Check a basic GL dump. The replay call crashes.
+
+ """
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_replay_crashes_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.eglretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ @pytest.mark.parametrize('option, wine, apitrace, d3dretrace', [
+ (0, '/env/wine', '/env/wine/apitrace', '/env/wine/d3dretrace'),
+ (1, '/config/wine', '/config/wine/apitrace', '/config/wine/d3dretrace'),
+ (2, 'wine', 'apitrace', 'd3dretrace'),
+ ])
+ def test_dump_dxgi_options(self, option, wine, apitrace, d3dretrace, config):
+ """Tests for the dump method: basic with options.
+
+ Check basic DXGI dumps with different configurations. No specific output
+ directory is provided and leaving to the method to figure out the last
+ call from the trace file itself.
+
+ """
+ self.wine = wine
+ self.apitrace = apitrace
+ self.d3dretrace = d3dretrace
+ if option == 0:
+ self.env['PIGLIT_REPLAY_WINE_BINARY'] = self.wine
+ self.env['PIGLIT_REPLAY_WINE_APITRACE_BINARY'] = self.apitrace
+ self.env['PIGLIT_REPLAY_WINE_D3DRETRACE_BINARY'] = self.d3dretrace
+ elif option == 1:
+ config.set('replay', 'wine_bin', self.wine)
+ config.set('replay', 'wine_apitrace_bin', self.apitrace)
+ config.set('replay', 'wine_d3dretrace_bin', self.d3dretrace)
+ calls = self.dxgi_trace_last_call
+ trace_path = self.dxgi_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.wine, self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_dxgi_output(self):
+ """Tests for the dump method: explicit output directory.
+
+ Check a basic DXGI dump, specifying the output and leaving for the method
+ to figure out the last call from the trace file itself.
+
+ """
+ calls = self.dxgi_trace_last_call
+ trace_path = self.dxgi_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ output_dir=self.output_dir)
+ assert test.dump()
+ snapshot_prefix = path.join(self.output_dir,
+ path.basename(trace_path) + '-')
+ m_calls = [self.mocker.call(
+ [self.wine, self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_dxgi_calls(self):
+ """Tests for the dump method: explicit valid calls.
+
+ Check a basic DXGI dump, specifying valid calls to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.dxgi_trace_calls
+ trace_path = self.dxgi_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_apitrace_subprocess_run.assert_called_once_with(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_dxgi_wrong_call(self):
+ """Tests for the dump method: explicit invalid call.
+
+ Check a basic DXGI dump, specifying an invalid call to dump. No
+ specific output directory is provided.
+
+ """
+ calls = self.dxgi_trace_wrong_call
+ trace_path = self.dxgi_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path,
+ calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_apitrace_subprocess_run.assert_called_once_with(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_dxgi_last_frame_fails(self):
+ """Tests for the dump method: the call to figure out the last frame fails.
+
+ Check a basic DXGI dump. The call to figure out the last frame fails.
+
+ """
+ calls = '-1'
+ trace_path = self.dxgi_last_frame_fails_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.wine, self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
+
+ def test_dump_dxgi_replay_crashes(self):
+ """Tests for the dump method: the replay call crashes.
+
+ Check a basic DXGI dump. The replay call crashes.
+
+ """
+ calls = self.dxgi_trace_last_call
+ trace_path = self.dxgi_replay_crashes_trace_path
+ test = backends.apitrace.APITraceBackend(trace_path)
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ m_calls = [self.mocker.call(
+ [self.wine, self.apitrace, 'dump', '--calls=frame', trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.wine, self.d3dretrace, '--headless',
+ '--snapshot=' + calls,
+ '--snapshot-prefix=' + snapshot_prefix, trace_path],
+ env=None, stdout=subprocess.PIPE)]
+ assert self.m_apitrace_subprocess_run.call_count == 2
+ self.m_apitrace_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call.zfill(10) + '.png')
diff --git a/unittests/framework/replay/backends/test_gfxreconstruct.py b/unittests/framework/replay/backends/test_gfxreconstruct.py
new file mode 100644
index 000000000..ae9b92dae
--- /dev/null
+++ b/unittests/framework/replay/backends/test_gfxreconstruct.py
@@ -0,0 +1,374 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's gfxreconstruct backend."""
+
+import pytest
+
+import os
+import subprocess
+import textwrap
+
+from os import path
+
+from framework import core
+from framework import exceptions
+from framework.replay import backends
+from framework.replay.options import OPTIONS
+
+
+@pytest.yield_fixture
+def config(mocker):
+ conf = mocker.patch('framework.core.PIGLIT_CONFIG',
+ new_callable=core.PiglitConfig)
+ conf.add_section('replay')
+ yield conf
+
+class TestGFXReconstructBackend(object):
+ """Tests for the GFXReconstructBackend class."""
+
+ def mock_gfxreconstruct_subprocess_run(self, cmd, stdout, env=None):
+ if cmd[0].endswith(self.gfxrecon_info):
+ # VK get_last_call
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] == self.vk_last_frame_fails_trace_path:
+ ret.stdout = b''
+ else:
+ ret.stdout = bytearray('Total frames: ' +
+ str(int(self.vk_trace_last_call) + 1) +
+ textwrap.dedent('''
+ Application info:
+ Application name: vkcube
+ Application version: 0
+ Engine name: vkcube
+ Engine version: 0
+ Target API version: 4194304 (1.0.0)
+
+ Physical device info:
+ Device name: TESTING DEVICE
+ Device ID: 0xabcd
+ Vendor ID: 0xefgh
+ Driver version: 83890275 (0x5001063)
+ API version: 4202627 (1.2.131)
+
+ Device memory allocation info:
+ Total allocations: 5
+ Min allocation size: 1216
+ Max allocation size: 540680
+
+ Pipeline info:
+ Total graphics pipelines: 1
+ Total compute pipelines: 0
+ '''),
+ 'utf-8')
+
+ return ret
+ elif cmd[0].endswith(self.gfxrecon_replay):
+ # VK replay
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if cmd[-1] != self.vk_replay_crashes_trace_path:
+ calls = env['VK_SCREENSHOT_FRAMES']
+ prefix = env['VK_SCREENSHOT_DIR']
+ ret.stdout = b''
+ if env['VK_INSTANCE_LAYERS'] == 'VK_LAYER_LUNARG_screenshot':
+ for call in calls.split(','):
+ if (call != self.vk_trace_wrong_call and
+ call != '-1'):
+ from PIL import Image
+ dump = path.join(prefix, call + '.ppm')
+ rgba = 'ff00ffff'
+ color = [int(rgba[0:2], 16), int(rgba[2:4], 16),
+ int(rgba[4:6], 16), int(rgba[6:8], 16)]
+ Image.frombytes('RGBA', (32, 32),
+ bytes(color * 32 * 32)).save(dump)
+ ret.stdout += bytearray('Screen Capture file '
+ 'is: ' + dump + '\n',
+ 'utf-8')
+ ret.stdout += bytearray('35.650065 fps, 0.280504 seconds, '
+ '10 frames, 1 loop, framerange 1-10\n',
+ 'utf-8')
+ else:
+ ret.stdout = b'\n'
+ ret.returncode = 1
+
+ return ret
+ else:
+ raise exceptions.PiglitFatalError(
+ 'Non treated cmd: {}'.format(cmd))
+
+ @pytest.fixture(autouse=True)
+ def setup(self, mocker, tmpdir):
+ """Setup for TestGFXReconstructBackend.
+
+ This set ups the basic environment for testing.
+ """
+
+ OPTIONS.device_name = 'test-device'
+ self.gfxrecon_info = 'gfxrecon-info'
+ self.gfxrecon_replay = 'gfxrecon-replay'
+ self.gfxrecon_replay_extra = ''
+ self.vk_trace_path = tmpdir.mkdir(
+ 'db-path').join('KhronosGroup-Vulkan-Tools/vkcube.gfxr').strpath
+ self.vk_last_frame_fails_trace_path = tmpdir.join(
+ 'db-path',
+ 'last-frame/fails.gfxr').strpath
+ self.vk_replay_crashes_trace_path = tmpdir.join(
+ 'db-path',
+ 'replay/fails.gfxr').strpath
+ self.vk_trace_calls = '2,5'
+ self.vk_trace_last_call = '9'
+ self.vk_trace_wrong_call = '10'
+ self.output_dir = tmpdir.mkdir('results').strpath
+ self.results_partial_path = path.join('trace', OPTIONS.device_name)
+ self.m_gfxreconstruct_subprocess_run = mocker.patch(
+ 'framework.replay.backends.gfxreconstruct.subprocess.run',
+ side_effect=self.mock_gfxreconstruct_subprocess_run)
+ self.tmpdir = tmpdir
+ self.mocker = mocker
+ mocker.patch.dict('os.environ')
+ self.env = os.environ
+ self.env.clear()
+
+ @pytest.mark.raises(exception=exceptions.PiglitFatalError)
+ def test_init_unsupported_trace(self):
+ """Tests for the init method.
+
+ Should raise an exception in case of creating with an unsupported trace
+ format.
+
+ """
+ test = backends.gfxreconstruct.GFXReconstructBackend(
+ 'unsupported_trace.trace')
+
+ @pytest.mark.parametrize('option, gfxrecon_info, gfxrecon_replay', [
+ (0, '/env/gfxrecon-info', '/env/gfxrecon-replay'),
+ (1, '/config/gfxrecon-info', '/config/gfxrecon-replay'),
+ (2, 'gfxrecon-info', 'gfxrecon-replay'),
+ ])
+ def test_dump_vk_options(self,
+ option, gfxrecon_info, gfxrecon_replay, config):
+ """Tests for the dump method: basic with options.
+
+ Check basic VK dumps with different configurations. No specific output
+ directory is provided and leaving to the method to figure out the last
+ call from the trace file itself.
+
+ """
+ self.gfxrecon_info = gfxrecon_info
+ self.gfxrecon_replay = gfxrecon_replay
+ if option == 0:
+ self.env['PIGLIT_REPLAY_GFXRECON_INFO_BINARY'] = self.gfxrecon_info
+ self.env[
+ 'PIGLIT_REPLAY_GFXRECON_REPLAY_BINARY'] = self.gfxrecon_replay
+ elif option == 1:
+ config.set('replay', 'gfxrecon-info_bin', self.gfxrecon_info)
+ config.set('replay', 'gfxrecon-replay_bin', self.gfxrecon_replay)
+ calls = self.vk_trace_last_call
+ trace_path = self.vk_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ m_calls = [self.mocker.call(
+ [self.gfxrecon_info, trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)]
+ assert self.m_gfxreconstruct_subprocess_run.call_count == 2
+ self.m_gfxreconstruct_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ @pytest.mark.parametrize('option, gfxrecon_replay_extra', [
+ (0, '-m remap'),
+ (1, '-m rebind'),
+ ])
+ def test_dump_vk_replay_extra_args(self,
+ option, gfxrecon_replay_extra, config):
+ """Tests for the dump method: basic with replay extra args.
+
+ Check basic VK dumps with different replay extra args. No specific
+ output directory and other options are provided and it is left to the
+ method to figure out the last call from the trace file itself.
+
+ """
+ if option == 0:
+ self.env[
+ 'PIGLIT_REPLAY_GFXRECON_REPLAY_EXTRA_ARGS'
+ ] = gfxrecon_replay_extra
+ elif option == 1:
+ config.set('replay', 'gfxrecon-replay_extra_args',
+ gfxrecon_replay_extra)
+ calls = self.vk_trace_last_call
+ trace_path = self.vk_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ m_calls = [self.mocker.call(
+ [self.gfxrecon_info, trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.gfxrecon_replay] +
+ gfxrecon_replay_extra.split() + [trace_path],
+ env=env, stdout=subprocess.PIPE)]
+ assert self.m_gfxreconstruct_subprocess_run.call_count == 2
+ self.m_gfxreconstruct_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_vk_output(self):
+ """Tests for the dump method: explicit output directory.
+
+ Check a basic VK dump, specifying the output and leaving for the method
+ to figure out the last call from the trace file itself.
+
+ """
+ calls = self.vk_trace_last_call
+ trace_path = self.vk_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(
+ trace_path, output_dir=self.output_dir)
+ assert test.dump()
+ snapshot_prefix = path.join(self.output_dir,
+ path.basename(trace_path) + '-')
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = self.output_dir
+ m_calls = [self.mocker.call(
+ [self.gfxrecon_info, trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)]
+ assert self.m_gfxreconstruct_subprocess_run.call_count == 2
+ self.m_gfxreconstruct_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_vk_calls(self):
+ """Tests for the dump method: explicit valid calls.
+
+ Check a basic VK dump, specifying valid calls to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.vk_trace_calls
+ trace_path = self.vk_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(
+ trace_path, calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ self.m_gfxreconstruct_subprocess_run.assert_called_once_with(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_vk_wrong_call(self):
+ """Tests for the dump method: explicit invalid call.
+
+ Check a basic VK dump, specifying an invalid call to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.vk_trace_wrong_call
+ trace_path = self.vk_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(
+ trace_path, calls=calls.split(','))
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ self.m_gfxreconstruct_subprocess_run.assert_called_once_with(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_vk_last_frame_fails(self):
+ """Tests for the dump method: the call to figure out the last frame fails.
+
+ Check a basic VK dump. The call to figure out the last frame fails.
+
+ """
+ calls = '-1'
+ trace_path = self.vk_last_frame_fails_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(trace_path)
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ m_calls = [self.mocker.call(
+ [self.gfxrecon_info, trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)]
+ assert self.m_gfxreconstruct_subprocess_run.call_count == 2
+ self.m_gfxreconstruct_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_vk_replay_crashes(self):
+ """Tests for the dump method: the replay call crashes.
+
+ Check a basic VK dump. The replay call crashes.
+
+ """
+ calls = self.vk_trace_last_call
+ trace_path = self.vk_replay_crashes_trace_path
+ test = backends.gfxreconstruct.GFXReconstructBackend(trace_path)
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ env = os.environ.copy()
+ env['VK_INSTANCE_LAYERS'] = 'VK_LAYER_LUNARG_screenshot'
+ env['VK_SCREENSHOT_FRAMES'] = calls
+ env['VK_SCREENSHOT_DIR'] = path.dirname(trace_path)
+ m_calls = [self.mocker.call(
+ [self.gfxrecon_info, trace_path],
+ stdout=subprocess.PIPE),
+ self.mocker.call(
+ [self.gfxrecon_replay, trace_path],
+ env=env, stdout=subprocess.PIPE)]
+ assert self.m_gfxreconstruct_subprocess_run.call_count == 2
+ self.m_gfxreconstruct_subprocess_run.assert_has_calls(m_calls)
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call + '.png')
diff --git a/unittests/framework/replay/backends/test_package.py b/unittests/framework/replay/backends/test_package.py
new file mode 100644
index 000000000..8f8e487d5
--- /dev/null
+++ b/unittests/framework/replay/backends/test_package.py
@@ -0,0 +1,93 @@
+# coding=utf-8
+#
+# Copyright (c) 2014, 2016 Intel Corporation
+# Copyright © 2019-2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+""" Tests for replayer's backend package """
+
+import pytest
+
+from framework.replay import backends
+
+# pylint: disable=no-self-use
+
+
+
+# Helpers
+
+
+class TestBackend():
+ """A dummy test backend"""
+
+ def __init__(self, trace_path, output_dir=None, calls=[]):
+ self._trace_path = trace_path
+
+ def dump(self):
+ return [self._trace_path]
+
+# Prevent pytest from trying to collect TestBackend as tests:
+TestBackend.__test__ = False
+
+@pytest.yield_fixture
+def mock_backend(mocker, backend):
+ """Add an extra backend for testing."""
+ mocker.patch.dict(
+ backends.DUMPBACKENDS,
+ {'test_backend': backends.register.Registry(
+ extensions=['.test_backend'],
+ backend=backend,
+ )})
+ yield
+
+
+# Tests
+
+
+class TestDump(object):
+ """Tests for the dump function."""
+
+ @pytest.mark.parametrize("backend", [
+ (TestBackend),
+ ])
+ def test_basic(self, mock_backend): # pylint: disable=unused-argument
+ """backends.dump: works as expected."""
+ p = 'foo.test_backend'
+ test = backends.dump(p)
+ assert [p] == test
+
+ @pytest.mark.raises(exception=backends.DumpBackendError)
+ def test_unknown(self):
+ """backends.dump(): An error is raised if no backend is registered for an
+ extension.
+ """
+ backends.dump('foo.test_extension')
+
+ @pytest.mark.raises(exception=backends.DumpBackendNotImplementedError)
+ @pytest.mark.parametrize("backend", [
+ (None),
+ ])
+ def test_notimplemented(self, mock_backend): # pylint: disable=unused-argument
+ """backends.dump(): An error is raised if a dumper isn't properly
+ implmented.
+ """
+ backends.dump('foo.test_backend')
diff --git a/unittests/framework/replay/backends/test_renderdoc.py b/unittests/framework/replay/backends/test_renderdoc.py
new file mode 100644
index 000000000..f78bb944a
--- /dev/null
+++ b/unittests/framework/replay/backends/test_renderdoc.py
@@ -0,0 +1,196 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's renderdoc backend."""
+
+import pytest
+
+import os
+import subprocess
+
+from os import path
+
+from framework import core
+from framework import exceptions
+from framework.replay import backends
+from framework.replay.options import OPTIONS
+
+
+@pytest.yield_fixture
+def config(mocker):
+ conf = mocker.patch('framework.core.PIGLIT_CONFIG',
+ new_callable=core.PiglitConfig)
+ conf.add_section('replay')
+ yield conf
+
+class TestRenderDocBackend(object):
+ """Tests for the RenderDocBackend class."""
+
+ def mock_renderdoc_subprocess_run(self, cmd, stdout, env=None):
+ ret = subprocess.CompletedProcess(cmd, 0)
+ if len(cmd) > 3:
+ calls = cmd[3:]
+ else:
+ calls = [self.gl_trace_last_call]
+ if (cmd[1] == self.gl_trace_path and
+ calls != [self.gl_trace_wrong_call]):
+ prefix = path.join(cmd[2], path.basename(cmd[1]))
+ ret.stdout = b''
+ for call in calls:
+ if call != self.gl_trace_wrong_call:
+ dump = prefix + '-' + call + '.png'
+ with open(dump, 'w') as f:
+ f.write("content")
+ if call == self.gl_trace_last_call:
+ call_text = 'End of Capture'
+ else:
+ call_text = 'glDrawArrays(4)'
+ ret.stdout += bytearray('Saving image at eventId ' + call +
+ ': ' + call_text + ' to ' + dump +
+ '\n', 'utf-8')
+ else:
+ ret.stdout = b''
+ ret.returncode = 1
+
+ return ret
+
+ @pytest.fixture(autouse=True)
+ def setup(self, mocker, tmpdir):
+ """Setup for TestRenderDocBackend.
+
+ This set ups the basic environment for testing.
+ """
+
+ OPTIONS.device_name = 'test-device'
+ self.renderdoc = 'renderdoc/renderdoc_dump_images.py'
+ self.gl_trace_path = tmpdir.mkdir(
+ 'db-path').join('glmark2/desktop.rdc').strpath
+ self.gl_replay_crashes_trace_path = tmpdir.join(
+ 'db-path',
+ 'replay/fails.rdc').strpath
+ self.gl_trace_calls = '47,332'
+ self.gl_trace_last_call = '340'
+ self.gl_trace_wrong_call = '333'
+ self.output_dir = tmpdir.mkdir('results').strpath
+ self.results_partial_path = path.join('trace', OPTIONS.device_name)
+ self.m_renderdoc_subprocess_run = mocker.patch(
+ 'framework.replay.backends.abstract.subprocess.run',
+ side_effect=self.mock_renderdoc_subprocess_run)
+ self.tmpdir = tmpdir
+ self.mocker = mocker
+
+ @pytest.mark.raises(exception=exceptions.PiglitFatalError)
+ def test_init_unsupported_trace(self):
+ """Tests for the init method.
+
+ Should raise an exception in case of creating with an unsupported trace
+ format.
+
+ """
+ test = backends.renderdoc.RenderDocBackend('unsupported_trace.gfxr')
+
+ def test_dump_gl_options(self):
+ """Tests for the dump method: basic with options.
+
+ Check basic GL dumps with different configurations. No specific output
+ directory is provided and leaving to the method to figure out the last
+ call from the trace file itself.
+
+ """
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_trace_path
+ test = backends.renderdoc.RenderDocBackend(trace_path)
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_renderdoc_subprocess_run.assert_called_once()
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_gl_output(self):
+ """Tests for the dump method: explicit output directory.
+
+ Check a basic GL dump, specifying the output and leaving for the method
+ to figure out the last call from the trace file itself.
+
+ """
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_trace_path
+ test = backends.renderdoc.RenderDocBackend(trace_path,
+ output_dir=self.output_dir)
+ assert test.dump()
+ snapshot_prefix = path.join(self.output_dir,
+ path.basename(trace_path) + '-')
+ self.m_renderdoc_subprocess_run.assert_called_once()
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_gl_calls(self):
+ """Tests for the dump method: explicit valid calls.
+
+ Check a basic GL dump, specifying valid calls to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.gl_trace_calls
+ trace_path = self.gl_trace_path
+ test = backends.renderdoc.RenderDocBackend(trace_path,
+ calls=calls.split(','))
+ assert test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_renderdoc_subprocess_run.assert_called_once()
+ for call in calls.split(','):
+ assert path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_gl_wrong_call(self):
+ """Tests for the dump method: explicit invalid call.
+
+ Check a basic GL dump, specifying an invalid call to dump. No specific
+ output directory is provided.
+
+ """
+ calls = self.gl_trace_wrong_call
+ trace_path = self.gl_trace_path
+ test = backends.renderdoc.RenderDocBackend(trace_path,
+ calls=calls.split(','))
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_renderdoc_subprocess_run.assert_called_once()
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call + '.png')
+
+ def test_dump_gl_replay_crashes(self):
+ """Tests for the dump method: the replay call crashes.
+
+ Check a basic GL dump. The replay call crashes.
+
+ """
+ calls = self.gl_trace_last_call
+ trace_path = self.gl_replay_crashes_trace_path
+ test = backends.renderdoc.RenderDocBackend(trace_path)
+ assert not test.dump()
+ snapshot_prefix = trace_path + '-'
+ self.m_renderdoc_subprocess_run.assert_called_once()
+ for call in calls.split(','):
+ assert not path.exists(snapshot_prefix + call + '.png')
diff --git a/unittests/framework/replay/test_compare_replay.py b/unittests/framework/replay/test_compare_replay.py
new file mode 100644
index 000000000..47142ebba
--- /dev/null
+++ b/unittests/framework/replay/test_compare_replay.py
@@ -0,0 +1,313 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's compare_replay module."""
+
+import pytest
+
+import contextlib
+import io
+import os
+
+from os import path
+
+from framework import exceptions
+from framework.replay import backends
+from framework.replay import compare_replay
+from framework.replay.options import OPTIONS
+
+
+class TestCompareReplay(object):
+ """Tests for compare_replay methods."""
+
+ @staticmethod
+ def _create_dump(trace_path, results_path, calls):
+ p = path.join(results_path,
+ path.basename(trace_path) + '-' + str(calls) + '.png')
+ os.makedirs(path.dirname(p), exist_ok=True)
+ with open(p, 'w') as f:
+ f.write('content')
+
+ @staticmethod
+ def mock_backends_dump(trace_path, results_path, calls):
+ if trace_path.endswith('KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr'):
+ TestCompareReplay._create_dump(trace_path, results_path, 99)
+
+ return True
+ elif trace_path.endswith('pathfinder/demo.trace'):
+ TestCompareReplay._create_dump(trace_path, results_path, 78)
+
+ return True
+ elif trace_path.endswith('Wicked-Engine/Tests:Cloth_Physics_Test.trace-dxgi'):
+ return False
+ elif trace_path.endswith('unimplemented/backend.vktrace'):
+ raise backends.DumpBackendNotImplementedError(
+ 'DumpBackend for "vktrace" is not implemented')
+ elif trace_path.endswith('unexisting/back.end'):
+ raise backends.DumpBackendError(
+ 'No module supports file extensions "end"')
+ else:
+ raise exceptions.PiglitFatalError(
+ 'Non treated trace path: {}'.format(trace_path))
+
+ @staticmethod
+ def mock_hexdigest_from_image(image_file):
+ if image_file.endswith(
+ 'KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr-99.png'):
+ return '917cbbf4f09dd62ea26d247a1c70c16e'
+ elif image_file.endswith(
+ 'pathfinder/demo.trace-78.png'):
+ return 'e624d76c70cc3c532f4f54439e13659a'
+ else:
+ raise exceptions.PiglitFatalError(
+ 'Non treated image file: {}'.format(image_file))
+
+ @staticmethod
+ def mock_qty_load_yaml(yaml_file):
+ if yaml_file == 'empty.yml':
+ return {}
+ elif yaml_file == 'no-device.yml':
+ return {"traces":
+ [{"path": "glmark2/desktop-blur-radius=5:effect=blur:passes=1:separable=true:windows=4.rdc",
+ "expectations": [{"device": "gl-vmware-llvmpipe",
+ "checksum": "8867f3a41f180626d0d4b7661ff5c0f4"}]},]}
+ elif yaml_file == 'one-trace.yml':
+ return {"traces":
+ [{"path": "KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr",
+ "expectations": [{"device": OPTIONS.device_name,
+ "checksum": "917cbbf4f09dd62ea26d247a1c70c16e"}]},]}
+ elif yaml_file == 'two-traces.yml':
+ return {"traces":
+ [{"path": "pathfinder/demo.trace",
+ "expectations": [{"device": OPTIONS.device_name,
+ "checksum": "e624d76c70cc3c532f4f54439e13659a"}]},
+ {"path": "KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr",
+ "expectations": [{"device": OPTIONS.device_name,
+ "checksum": "917cbbf4f09dd62ea26d247a1c70c16e"}]},]}
+ else:
+ raise exceptions.PiglitFatalError(
+ 'Non treated YAML file: {}'.format(yaml_file))
+
+ @pytest.fixture(autouse=True)
+ def setup(self, mocker, tmpdir):
+ """Setup for TestCompareReplay.
+
+ This create the basic environment for testing.
+ """
+
+ OPTIONS.device_name = 'test-device'
+ OPTIONS.db_path = tmpdir.mkdir('db-path').strpath
+ OPTIONS.results_path = tmpdir.mkdir('results').strpath
+ self.trace_path = 'KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr'
+ self.exp_checksum = '917cbbf4f09dd62ea26d247a1c70c16e'
+ self.results_partial_path = path.join('results/trace',
+ OPTIONS.device_name)
+ self.m_qty_load_yaml = mocker.patch(
+ 'framework.replay.compare_replay.qty.load_yaml',
+ side_effect=TestCompareReplay.mock_qty_load_yaml)
+ self.m_ensure_file = mocker.patch(
+ 'framework.replay.compare_replay.ensure_file',
+ return_value=None)
+ self.m_backends_dump = mocker.patch(
+ 'framework.replay.compare_replay.backends.dump',
+ side_effect=TestCompareReplay.mock_backends_dump)
+ self.m_hexdigest_from_image = mocker.patch(
+ 'framework.replay.compare_replay.hexdigest_from_image',
+ side_effect=TestCompareReplay.mock_hexdigest_from_image)
+ self.tmpdir = tmpdir
+
+ def test_from_yaml_empty(self):
+ """compare_replay.from_yaml: compare using an empty YAML file"""
+
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.from_yaml('empty.yml')
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_called_once()
+ s = f.getvalue()
+ assert s == ''
+
+ def test_from_yaml_no_device(self):
+ """compare_replay.from_yaml: compare using a YAML without expectations for the used device"""
+
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.from_yaml('no-device.yml')
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_called_once()
+ s = f.getvalue()
+ assert s == ''
+
+ def test_from_yaml_one_trace(self):
+ """compare_replay.from_yaml: compare using a YAML with just one expectation for the used device"""
+
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.from_yaml('one-trace.yml')
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_called_once()
+ self.m_ensure_file.assert_called_once()
+ self.m_backends_dump.assert_called_once()
+ self.m_hexdigest_from_image.assert_called_once()
+ assert not self.tmpdir.join(self.results_partial_path,
+ self.trace_path + '-99.png').check()
+ assert not self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s == ('[check_image]\n'
+ ' actual: ' + self.exp_checksum + '\n'
+ ' expected: ' + self.exp_checksum + '\n'
+ '[check_image] Images match for:\n'
+ ' ' + self.trace_path + '\n'
+ '\n')
+
+ def test_from_yaml_two_traces(self):
+ """compare_replay.from_yaml: compare using a YAML with more than one expectation for the used device"""
+
+ second_trace_path = 'pathfinder/demo.trace'
+ second_exp_checksum = 'e624d76c70cc3c532f4f54439e13659a'
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.from_yaml('two-traces.yml')
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_called_once()
+ assert self.m_ensure_file.call_count == 2
+ assert self.m_backends_dump.call_count == 2
+ assert self.m_hexdigest_from_image.call_count == 2
+ assert not self.tmpdir.join(self.results_partial_path,
+ second_trace_path + '-78.png').check()
+ assert not self.tmpdir.join(self.results_partial_path,
+ path.dirname(second_trace_path),
+ second_exp_checksum + '.png').check()
+ assert not self.tmpdir.join(self.results_partial_path,
+ self.trace_path + '-99.png').check()
+ assert not self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s == ('[check_image]\n'
+ ' actual: ' + second_exp_checksum + '\n'
+ ' expected: ' + second_exp_checksum + '\n'
+ '[check_image] Images match for:\n'
+ ' ' + second_trace_path + '\n'
+ '\n'
+ '[check_image]\n'
+ ' actual: ' + self.exp_checksum + '\n'
+ ' expected: ' + self.exp_checksum + '\n'
+ '[check_image] Images match for:\n'
+ ' ' + self.trace_path + '\n'
+ '\n')
+
+ def test_trace_success(self):
+ """compare_replay.trace: compare a trace successfully"""
+
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.trace(self.trace_path, self.exp_checksum)
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_not_called()
+ self.m_ensure_file.assert_called_once()
+ self.m_backends_dump.assert_called_once()
+ self.m_hexdigest_from_image.assert_called_once()
+ assert not self.tmpdir.join(self.results_partial_path,
+ self.trace_path + '-99.png').check()
+ assert not self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s.endswith('PIGLIT: {"result": "pass"}\n')
+
+ def test_trace_success_keep_image(self):
+ """compare_replay.trace: compare a trace successfully and set the option to keep the dumped image"""
+
+ OPTIONS.keep_image = True
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.trace(self.trace_path, self.exp_checksum)
+ is compare_replay.Result.MATCH)
+ self.m_qty_load_yaml.assert_not_called()
+ self.m_ensure_file.assert_called_once()
+ self.m_backends_dump.assert_called_once()
+ self.m_hexdigest_from_image.assert_called_once()
+ assert not self.tmpdir.join(self.results_partial_path,
+ self.trace_path + '-99.png').check()
+ assert self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s.endswith('PIGLIT: {"result": "pass"}\n')
+
+ def test_trace_fail(self):
+ """compare_replay.trace: fail comparing a trace"""
+
+ wrong_checksum = '917cbbf4f09dd62ea26d247a1c70c16f'
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.trace(self.trace_path, wrong_checksum)
+ is compare_replay.Result.DIFFER)
+ self.m_qty_load_yaml.assert_not_called()
+ self.m_ensure_file.assert_called_once()
+ self.m_backends_dump.assert_called_once()
+ self.m_hexdigest_from_image.assert_called_once()
+ assert not self.tmpdir.join(self.results_partial_path,
+ self.trace_path + '-99.png').check()
+ assert self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s.endswith('PIGLIT: '
+ '{"images": [{'
+ '"image_desc": "' + self.trace_path + '", '
+ '"image_ref": "' + wrong_checksum + '.png", '
+ '"image_render": "' +
+ self.tmpdir.join(self.results_partial_path,
+ path.dirname(self.trace_path),
+ self.exp_checksum +
+ '.png').strpath +
+ '"}], "result": "fail"}\n')
+
+ @pytest.mark.parametrize('trace_path', [
+ ('Wicked-Engine/Tests:Cloth_Physics_Test.trace-dxgi'),
+ ('unimplemented/backend.vktrace'),
+ ('unexisting/back.end'),
+ ])
+ def test_trace_dump_crash(self, trace_path):
+ """compare_replay.trace: dump crashes or fails comparing a trace"""
+
+ third_exp_checksum = '6b6d27df609b8d086cc3335e6d103581'
+ f = io.StringIO()
+ with contextlib.redirect_stdout(f):
+ assert (compare_replay.trace(trace_path, third_exp_checksum)
+ is compare_replay.Result.FAILURE)
+ self.m_qty_load_yaml.assert_not_called()
+ self.m_ensure_file.assert_called_once()
+ self.m_backends_dump.assert_called_once()
+ self.m_hexdigest_from_image.assert_not_called()
+ assert not self.tmpdir.join(self.results_partial_path,
+ path.dirname(trace_path),
+ third_exp_checksum + '.png').check()
+ s = f.getvalue()
+ assert s.endswith('PIGLIT: {"result": "crash"}\n')
diff --git a/unittests/framework/replay/test_download_utils.py b/unittests/framework/replay/test_download_utils.py
new file mode 100644
index 000000000..6170ac0db
--- /dev/null
+++ b/unittests/framework/replay/test_download_utils.py
@@ -0,0 +1,110 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's download_utils module."""
+
+import pytest
+
+import os
+import requests
+import requests_mock
+
+from os import path
+
+from framework import exceptions
+from framework.replay import download_utils
+from framework.replay.options import OPTIONS
+
+
+class TestDownloadUtils(object):
+ """Tests for download_utils methods."""
+
+ @pytest.fixture(autouse=True)
+ def setup(self, requests_mock, tmpdir):
+ self.url = 'https://unittest.piglit.org/'
+ self.trace_path = 'KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr'
+ self.full_url = self.url + self.trace_path
+ self.trace_file = tmpdir.join(self.trace_path)
+ OPTIONS.set_download_url(self.url)
+ OPTIONS.download['force'] = False
+ OPTIONS.db_path = tmpdir.strpath
+ requests_mock.get(self.full_url, text='remote')
+
+ @staticmethod
+ def check_same_file(path_local, expected_content, expected_mtime=None):
+ assert path_local.read() == expected_content
+ if expected_mtime is not None:
+ m = path_local.mtime()
+ assert m == expected_mtime
+
+ def test_ensure_file_exists(self):
+ """download_utils.ensure_file: Check an existing file doesn't get overwritten"""
+
+ os.makedirs(path.dirname(self.trace_file), exist_ok=True)
+ self.trace_file.write("local")
+ m = self.trace_file.mtime()
+ download_utils.ensure_file(self.trace_path)
+ TestDownloadUtils.check_same_file(self.trace_file, "local", m)
+
+ def test_ensure_file_not_exists(self):
+ """download_utils.ensure_file: Check a non existing file gets downloaded"""
+
+ assert not self.trace_file.check()
+ download_utils.ensure_file(self.trace_path)
+ TestDownloadUtils.check_same_file(self.trace_file, "remote")
+
+ def test_ensure_file_exists_force_download(self):
+ """download_utils.ensure_file: Check an existing file gets overwritten when forced"""
+
+ OPTIONS.download['force'] = True
+ os.makedirs(path.dirname(self.trace_file), exist_ok=True)
+ self.trace_file.write("local")
+ m = self.trace_file.mtime()
+ download_utils.ensure_file(self.trace_path)
+ TestDownloadUtils.check_same_file(self.trace_file, "remote")
+
+ @pytest.mark.raises(exception=exceptions.PiglitFatalError)
+ def test_ensure_file_not_exists_no_url(self):
+ """download_utils.ensure_file: Check an exception raises when not passing an URL for a non existing file"""
+
+ OPTIONS.set_download_url("")
+ assert not self.trace_file.check()
+ download_utils.ensure_file(self.trace_path)
+
+ @pytest.mark.raises(exception=requests.exceptions.HTTPError)
+ def test_ensure_file_not_exists_404(self, requests_mock):
+ """download_utils.ensure_file: Check an exception raises when an URL returns a 404"""
+
+ requests_mock.get(self.full_url, text='Not Found', status_code=404)
+ assert not self.trace_file.check()
+ download_utils.ensure_file(self.trace_path)
+
+ @pytest.mark.raises(exception=requests.exceptions.ConnectTimeout)
+ def test_ensure_file_not_exists_timeout(self, requests_mock):
+ """download_utils.ensure_file: Check an exception raises when an URL returns a Connect Timeout"""
+
+ requests_mock.get(self.full_url, exc=requests.exceptions.ConnectTimeout)
+ assert not self.trace_file.check()
+ download_utils.ensure_file(self.trace_path)
diff --git a/unittests/framework/replay/test_image_checksum.py b/unittests/framework/replay/test_image_checksum.py
new file mode 100644
index 000000000..2c9f2edbb
--- /dev/null
+++ b/unittests/framework/replay/test_image_checksum.py
@@ -0,0 +1,41 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's image_checksum module."""
+
+import pytest
+
+import PIL
+
+from framework.replay import image_checksum
+
+
+@pytest.mark.raises(exception=OSError)
+def test_hexdigest_from_image_UnidentifiedImageError(tmpdir):
+ """image_checksum.hexdigest_from_image: Check an exception raises when a non image is passed"""
+
+ f = tmpdir.join("image.png")
+ f.write("content")
+ image_checksum.hexdigest_from_image(f.strpath)
diff --git a/unittests/framework/replay/test_options.py b/unittests/framework/replay/test_options.py
new file mode 100644
index 000000000..1102e29a2
--- /dev/null
+++ b/unittests/framework/replay/test_options.py
@@ -0,0 +1,62 @@
+# coding=utf-8
+#
+# Copyright (c) 2015-2016, 2019 Intel Corporation
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's options module."""
+
+import pytest
+
+from urllib.parse import urlparse
+
+from framework.replay import options
+
+# pylint: disable=protected-access
+# pylint: disable=invalid-name
+# pylint: disable=no-self-use
+
+
+def test_options_clear():
+ """options.Options.clear(): resests options values to init state."""
+ baseline = options._Options()
+
+ test = options._Options()
+ test.device_name = "dummy-device"
+ test.keep_image = True
+ test.clear()
+
+ assert list(iter(baseline)) == list(iter(test))
+
+
+@pytest.mark.parametrize("url, expected", [
+ ("this://is.a.valid/url", urlparse("this://is.a.valid/url")),
+ ("this://is.not.a[/valid/url", None),
+])
+def test_options_set_download_url(url, expected):
+ """options.Options.set_download_url(): safely sets the parsed download url."""
+ o = options._Options()
+
+ o.set_download_url(url)
+
+ assert o.download["url"] == expected
diff --git a/unittests/framework/replay/test_query_traces_yaml.py b/unittests/framework/replay/test_query_traces_yaml.py
new file mode 100644
index 000000000..e131f4721
--- /dev/null
+++ b/unittests/framework/replay/test_query_traces_yaml.py
@@ -0,0 +1,202 @@
+# coding=utf-8
+#
+# Copyright © 2020 Valve Corporation.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+
+"""Tests for replayer's query_traces_yaml module."""
+
+import pytest
+
+from framework import exceptions
+
+from framework.replay import query_traces_yaml as qty
+
+
+YAML_DATA = [["", {}],
+ ["""
+- First
+- Second
+""",
+ ["First", "Second"]],
+ ["""
+a: 1
+b:
+ c: 2
+""",
+ {"a": 1, "b": {"c": 2}}],
+]
+
+TRACES_DATA = {"traces":
+ [{"path": "glmark2/desktop-blur-radius=5:effect=blur:passes=1:separable=true:windows=4.rdc",
+ "expectations": [{"device": "gl-vmware-llvmpipe",
+ "checksum": "8867f3a41f180626d0d4b7661ff5c0f4"}]},
+ {"path": "glxgears/glxgears-2.trace",
+ "expectations": [{"device": "gl-vmware-llvmpipe",
+ "checksum": "f8eba0fec6e3e0af9cb09844bc73bdc7"},
+ {"device": "gl-virgl",
+ "checksum": "f8eba0fec6e3e0af9cb09844bc73bdc7"}]},
+ {"path": "pathfinder/demo.trace",
+ "expectations": [{"device": "gl-vmware-llvmpipe",
+ "checksum": "e624d76c70cc3c532f4f54439e13659a"}]},
+ {"path": "KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr",
+ "expectations": [{"device": "vk-amd-polaris10",
+ "checksum": "917cbbf4f09dd62ea26d247a1c70c16e"}]},]}
+
+@pytest.mark.raises(exception=exceptions.PiglitFatalError)
+def test_load_yaml_PiglitFatalError():
+ """query_traces_yaml.load_yaml: Raise PiglitFatalError on invalid YAML"""
+ y = qty.load_yaml("*** this is not YAML ***")
+
+
+def test_load_yaml_basic():
+ """query_traces_yaml.load_yaml: Load some basic YAML documents"""
+
+ for i in YAML_DATA:
+ y = qty.load_yaml(i[0])
+
+ assert i[1] == y
+
+
+@pytest.mark.raises(exception=TypeError)
+@pytest.mark.parametrize("trace", [
+ ([]),
+ ({"expectations": "one"}),
+ ({"expectations": ["one", "another"]}),
+])
+def test_trace_devices_TypeError(trace):
+ """query_traces_yaml.trace_devicess: Raise TypeError on invalid trace"""
+ d = qty.trace_devices(trace)
+
+
+@pytest.mark.parametrize("trace, expected", [
+ ({}, []),
+ ({"one": 1}, []),
+ ({"expectations": [{"one": 1}]}, []),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe"}]},
+ ["gl-vmware-lvmpipe"]),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe"},
+ {"device": "vk-intel-anv", "checksum": "a checksum"}]},
+ ["gl-vmware-lvmpipe", "vk-intel-anv"]),
+])
+def test_trace_devices_basic(trace, expected):
+ """query_traces_yaml.trace_devicess: Get devices from some basic traces"""
+ assert expected == qty.trace_devices(trace)
+
+
+@pytest.mark.raises(exception=TypeError)
+@pytest.mark.parametrize("trace, device", [
+ ([], None),
+ ({"expectations": "one"}, None),
+ ({"expectations": ["one", "another"]}, None),
+])
+def test_trace_checksum_TypeError(trace, device):
+ """query_traces_yaml.trace_checksum: Raise TypeError on invalid trace"""
+ c = qty.trace_checksum(trace, device)
+
+
+@pytest.mark.parametrize("trace, device, expected", [
+ ({}, None, ''),
+ ({"one": 1}, None, ''),
+ ({"expectations": [{"one": 1}]}, None, ''),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe"}]}, "gl-vmware-lvmpipe", ''),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe", "checksum": "a checksum"}]},
+ "gl-vmware-lvmpipe", "a checksum"),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe", "checksum": "a checksum"},
+ {"device": "vk-intel-anv", "checksum": "another checksum"}]},
+ "vk-intel-anv", "another checksum"),
+ ({"expectations": [{"device": "gl-vmware-lvmpipe", "checksum": "a checksum"},
+ {"device": "vk-intel-anv", "checksum": "another checksum"},
+ {"device": "vk-intel-anv", "checksum": "yet another checksum"}]},
+ "vk-intel-anv", "another checksum"),
+])
+def test_trace_checksum_basic(trace, device, expected):
+ """query_traces_yaml.trace_checksum: Get checksum from some basic traces"""
+ assert expected == qty.trace_checksum(trace, device)
+
+
+@pytest.mark.raises(exception=TypeError)
+@pytest.mark.parametrize("yaml", [
+ (8),
+ ({"traces-db": "one"}),
+])
+def test_download_url_TypeError(yaml):
+ """query_traces_yaml.download_url: Raise TypeError on invalid YAML"""
+ u = qty.download_url(yaml)
+
+
+@pytest.mark.parametrize("yaml, expected", [
+ ({}, None),
+ ({"traces-db": {"one": 1}}, None),
+ ({"traces-db": {"download-url": "an url"}}, "an url"),
+ ({"traces-db": {"one": 1, "download-url": "an url"}}, "an url"),
+])
+def test_download_url_basic(yaml, expected):
+ """query_yamls_yaml.download_url: Get download url from some basic YAML with a trace-db entry"""
+ assert expected == qty.download_url(yaml)
+
+
+@pytest.mark.raises(exception=AttributeError)
+@pytest.mark.parametrize("yaml, ext, device, checksum", [
+ (8, ".trace", "gl-vmware-llvmpipe", True),
+ (TRACES_DATA, {}, None, False),
+])
+def test_traces_AttributeError(yaml, ext, device, checksum):
+ """query_traces_yaml.traces: Raise AttributeError on invalid parameters"""
+ t = list(qty.traces(yaml, ext, device, checksum))
+
+
+@pytest.mark.raises(exception=TypeError)
+@pytest.mark.parametrize("yaml, ext, device, checksum", [
+ ({"traces": "one"}, ".trace", "gl-vmware-llvmpipe", True),
+ ({"traces": ["one"]}, ".trace", "gl-vmware-llvmpipe", True),
+ ({"traces": [{"path": 8}]}, ".trace", "gl-vmware-llvmpipe", True),
+])
+def test_traces_TypeError(yaml, ext, device, checksum):
+ """query_traces_yaml.traces: Raise TypeError on invalid YAML"""
+ t = list(qty.traces(yaml, ext, device, checksum))
+
+
+@pytest.mark.parametrize("yaml, ext, device, checksum, expected", [
+ ({}, ".trace", "gl-vmware-llvmpipe", True, []),
+ ({"traces": [{}]}, ".trace", "gl-vmware-llvmpipe", True, []),
+ (TRACES_DATA, "", "gl-virgl", False, []),
+ (TRACES_DATA, ".rdc", "gl-virgl", False, []),
+ (TRACES_DATA, ".trace", "gl-virgl", False,
+ [{"path": "glxgears/glxgears-2.trace"}]),
+ (TRACES_DATA, ".rdc,.trace", "gl-vmware-llvmpipe", False,
+ [{"path": "glmark2/desktop-blur-radius=5:effect=blur:passes=1:separable=true:windows=4.rdc"},
+ {"path": "glxgears/glxgears-2.trace"},
+ {"path": "pathfinder/demo.trace"}]),
+ (TRACES_DATA, ".trace", None, False,
+ [{"path": "glxgears/glxgears-2.trace"},
+ {"path": "pathfinder/demo.trace"}]),
+ (TRACES_DATA, ".gfxr", "vk-amd-polaris10", True,
+ [{"checksum": "917cbbf4f09dd62ea26d247a1c70c16e",
+ "path": "KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr"}]),
+ (TRACES_DATA, ".gfxr", None, True,
+ [{"checksum": "",
+ "path": "KhronosGroup-Vulkan-Tools/amd/polaris10/vkcube.gfxr"}]),
+])
+def test_traces_basic(yaml, ext, device, checksum, expected):
+ """query_traces_yaml.traces: Get traces lists from some basic yamls"""
+ assert expected == list(qty.traces(yaml, ext, device, checksum))