File: check_pixel_test_flakiness.py

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (349 lines) | stat: -rwxr-xr-x 13,621 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/usr/bin/env python
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Detect flakiness in the Skia Gold based pixel tests.

This script runs the specified Skia Gold pixel tests multiple times and compares
screenshots generated by test runs. The flakiness is detected if pixel test
code generates different screenshots in different iterations.

Because screenshots are compared through MD5, this script should only check
the pixel tests that use precise matching.

This script only checks whether the screenshots under the same names change in
different iterations. This script does NOT check whether screenshots are
expected. Therefore, please ensure the screenshots are correct before running
this script.

During execution, this script creates directories for temporary data. Those
directories' names contain special characters to ensure uniqueness. This
script guarantees to delete those directories at the end of execution.
Users can control the paths of those temporary directories via the option
--root_dir.

* Example usages:

./tools/pixel_test/check_pixel_test_flakiness.py --gtest_filter=\
DemoAshPixelDiffTest.VerifyTopLevelWidgets --test_target=out/debug\
/ash_pixeltests --output_dir=../var

The command above should be executed at the chromium source directory whose
absolute file path looks like .../chromium/src. This command checks
DemoAshPixelDiffTest.VerifyTopLevelWidgets by running ash_pixeltests under the
directory .../chromium/src/out/debug. If flakiness is detected, the flaky test's
screenshots are saved under .../chromium/var. If the directory specified by
--output_dir does not exist and meanwhile the flakiness is detected, the script
will create one. If the --output_dir is not specified, the flaky test's
screenshots are not saved.

./tools/pixel_test/check_pixel_test_flakiness.py --gtest_filter=\
DemoAshPixelDiffTest.VerifyTopLevelWidgets --test_target=out/debug/\
ash_pixeltests --root_dir=../.. --output_dir=var

The command above is similar to the previous one. But difference is that this
command uses the option --root_dir to designate the root path for outputs
(including the temporary data and the saved screenshots when flakiness is
detected). In this example, the absolute path of the output directory is
.../chromium/../var rather than .../chromium/var.

./tools/pixel_test/check_pixel_test_flakiness.py --gtest_filter=\
*PersonalizationAppIntegrationPixel* --test_target=out/debug/browser_tests
--root_dir=/tmp/skia_gold --output_dir=var --browser-ui-tests-verify-pixels
--enable-pixel-output-in-tests

Finally, the above command runs the browser_tests target and adds extra
arguments necessary for experimental browser pixel tests to run properly.

* options:

--test_target: it specifies the path to the executable file of pixel tests. It
is a relative file path from the current working directory. The test target can
be any test executable based on Skia Gold.

--root_dir: it specifies the root path for outputs (including the temporary data
 and the saved screenshots when flakiness is detected). It is a relative file
 path from the current working directory.

--log_mode: its value can only be 'none', 'error_only' and 'all'. 'none' means
that the log generated by gTest runs does not show; 'error_only' means that
only error messages from gTest runs are printed; 'all' shows all logs.
'none' is used by default.

--gtest_repeat: it specifies the count of repeated runs. Use ten by default.

Any additional unknown args, such as --browser-ui-test-verify-pixels, are passed
to the gtest runner.
"""

import argparse
import hashlib
import pathlib
import shutil
import subprocess

# Constants used for color print.
_OK_GREEN = '\033[92m'
_FAIL_RED = '\033[91m'
_ENDC = '\033[0m'

# Used by the directory to host screenshots generated in each iteration. Add
# some special characters to make this name unique.
_TEMP_DIRECTORY_NAME_BASE = '@@check_pixel_test_flakiness!#'


class FlakyScreenshotError(Exception):
  """One of the screenshots has been detected to be flaky."""


class MissingScreenshotsError(Exception):
  """There were no screenshots found."""


def _get_md5(path):
  """Returns the Md5 digest of the specified file."""
  if not path.is_absolute():
    raise ValueError(f'{path} must be absolute')

  with path.open(mode='rb') as target_file:
    return hashlib.md5(target_file.read()).hexdigest()


def _compare_with_last_iteration(screenshots, prev_temp_dir, temp_dir,
                                 names_hash_mappings, flaky_screenshot_dir):
  """Compares the screenshots generated in the current iteration with those
  from the previous iteration. If flakiness is detected, returns a flaky
  screenshot's name. Otherwise, returns an empty string.

  Args:
    screenshots: A list of screenshot Paths.
    prev_temp_dir: The absolute file path to the directory that hosts the
      screenshots generated by the previous iteration.
    temp_dir: The absolute file path to the directory that hosts the screenshots
      generated by the current iteration.
    names_hash_mappings: The mappings from screenshot names to hash code.
    flaky_screenshot_dir: The absolute file path to the directory used to host
      flaky screenshots. If it is None, flaky screenshots are not written into
      files.

  Returns: None

  Raises:
    FlakyScreenshotError: if screenshots from prev_temp_dir do not match
      temp_dir
    """

  if prev_temp_dir is None:
    raise TypeError('prev_temp_dir is required to be a valid Path')

  if not screenshots:
    raise ValueError('screenshots must be non-empty')

  for screenshot in screenshots:
    # The screenshot hash code does not change so no flakiness is detected
    # on `screenshot`.
    if names_hash_mappings[screenshot.name] == _get_md5(screenshot):
      continue

    if flaky_screenshot_dir is not None:
      # Delete the output directory if it already exists.
      if flaky_screenshot_dir.exists():
        shutil.rmtree(flaky_screenshot_dir)

      flaky_screenshot_dir.mkdir(parents=True)

      # Move the screenshot generated by the last iteration to the dest
      # directory.
      shutil.move(
          prev_temp_dir / screenshot.name, flaky_screenshot_dir /
          f'{screenshot.stem}_Version_1{screenshot.suffix}')

      # Move the screenshot generated by the current iteration to the dest
      # directory.
      shutil.move(
          screenshot, flaky_screenshot_dir /
          f'{screenshot.stem}_Version_2{screenshot.suffix}')

    raise FlakyScreenshotError(
        f'{_FAIL_RED}[Failure]{_ENDC} Detect flakiness in: {screenshot.name}')

  # No flakiness detected.
  return None


def _analyze_screenshots(prev_temp_dir, temp_dir, names_hash_mappings,
                         flaky_screenshot_dir):
  """Analyzes the screenshots generated by one iteration.

  Args:
    prev_temp_dir: The absolute file path to the directory that hosts the
      screenshots generated by the previous iteration, or None if this is the
      first iteration.
    temp_dir: The absolute file path to the directory that hosts the screenshots
      generated by the current iteration.
    names_hash_mappings: The mappings from screenshot names to hash code.
    flaky_screenshot_dir: The absolute file path to the directory used to host
      flaky screenshots. If it is None, flaky screenshots are not written into
      files.

  Returns: None

  Raises:
    FlakyScreenshotError
    MissingScreenshotsError
  """
  screenshots = list(temp_dir.iterdir())
  if not screenshots:
    raise MissingScreenshotsError(
        f'{_FAIL_RED}[Failure]{_ENDC} no screenshots are generated in the '
        'specified tests: are you using the correct test filter?')

  # For the first iteration, nothing to compare with. Therefore, fill
  # `names_hash_mappings` and return.
  if prev_temp_dir is None:
    for screenshot in screenshots:
      names_hash_mappings[screenshot.name] = _get_md5(screenshot)
    return

  _compare_with_last_iteration(screenshots, prev_temp_dir, temp_dir,
                               names_hash_mappings, flaky_screenshot_dir)


def main():
  parser = argparse.ArgumentParser(
      description='Detect flakiness in the Skia Gold based pixel tests by '
      'running the specified pixel test executable file multiple iterations '
      'and comparing screenshots generated by neighboring iterations through '
      'file hash code. Warning: this script can only be used to detect '
      'flakiness in the pixel tests that use precise comparison.')
  parser.add_argument(
      '--test_target',
      type=str,
      required=True,
      help='a '
      'relative file path from the current working directory, or absolute file '
      'path, to the test executable based on Skia Gold, such as ash_pixeltests')
  parser.add_argument('--gtest_repeat',
                      type=int,
                      default=10,
                      help='the count of the repeated runs. The default value '
                      'is ten.')
  parser.add_argument('--root_dir',
                      type=str,
                      default='',
                      help='a relative file path from the current working '
                      'directory, or an absolute path, to the root directory '
                      'that hosts output data including the screenshots '
                      'generated in each iteration and the detected flaky '
                      'screenshots')
  parser.add_argument('--output_dir',
                      type=str,
                      help='a relative path starting from the output root path '
                      'specified by --root_dir or the current working '
                      'directory if --root_dir is omitted. It specifies a '
                      'directory used to host the flaky screenshots if any.')
  parser.add_argument('--log_mode',
                      choices=['none', 'error_only', 'all'],
                      default='none',
                      help='the option to control the log output during test '
                      'runs. `none` means that the log generated by test runs '
                      'does not show; `error_only` means that only error logs '
                      'are printed; `all` shows all logs. `none` is used by '
                      'default.')
  [known_args, unknown_args] = parser.parse_known_args()

  # Calculate the absolute path to the pixel test executable file.
  executable_full_path = pathlib.Path(known_args.test_target).resolve()

  # Leverage testing/xvfb.py to set up Aura env.
  xvfb_py_full_path = pathlib.Path('testing/xvfb.py').resolve()

  # Calculate the absolute path to the directory that hosts output data.
  output_root_path = pathlib.Path(known_args.root_dir).resolve()

  # Skip the Skia Gold functionality. Because this script compares images
  # through hash code.
  pixel_test_command_base = [
      str(xvfb_py_full_path),
      str(executable_full_path), '--bypass-skia-gold-functionality'
  ]

  # Pass unknown args to gtest.
  if unknown_args:
    pixel_test_command_base += unknown_args

  # Print the command to run pixel tests.
  full_command = ' '.join(pixel_test_command_base)
  print(f'{_OK_GREEN}[Begin]{_ENDC} {full_command}')

  # Configure log output.
  std_out_mode = subprocess.DEVNULL
  if known_args.log_mode == 'all':
    std_out_mode = None
  std_err_mode = None
  if known_args.log_mode == 'none':
    std_err_mode = subprocess.DEVNULL

  # Cache the screenshot host directory used in the last iteration. It updates
  # at the end of each iteration.
  prev_temp_dir = None

  # Similar to `prev_temp_dir` but it caches data for the active
  # iteration.
  temp_dir = None

  # Mappings screenshot names to hash code.
  names_hash_mappings = {}

  # Calculate the directory path for saving flaky screenshots.
  flaky_screenshot_dir = None
  if known_args.output_dir is not None:
    flaky_screenshot_dir = output_root_path / known_args.output_dir

  try:

    for i in range(known_args.gtest_repeat):
      # Calculate the absolute path to the screenshot host directory used for
      # this iteration. Recreate the host directory if it already exists.
      temp_dir = output_root_path / f'{_TEMP_DIRECTORY_NAME_BASE}{i}'

      if temp_dir.exists():
        shutil.rmtree(temp_dir)
      temp_dir.mkdir(parents=True)

      # Append the option so that the screenshots generated in pixel tests are
      # written into `temp_dir`.
      pixel_test_command = pixel_test_command_base[:]
      pixel_test_command.append(
          f'--skia-gold-local-png-write-directory={temp_dir}')

      # Run pixel tests.
      subprocess.run(pixel_test_command,
                     stdout=std_out_mode,
                     stderr=std_err_mode,
                     check=True)

      _analyze_screenshots(prev_temp_dir, temp_dir, names_hash_mappings,
                           flaky_screenshot_dir)

      print(f'{_OK_GREEN}[OK]{_ENDC} the iteration {i} succeeds')

      # Delete the temporary data directory used by the previous loop iteration
      # before overwriting it.
      if prev_temp_dir is not None:
        shutil.rmtree(prev_temp_dir)

      prev_temp_dir = temp_dir
    else:
      # The for loop has finished without exceptions.
      print(f'{_OK_GREEN}[Success]{_ENDC} no flakiness is detected')

  finally:
    # ensure that temp data are removed.
    for dir_to_rm in (prev_temp_dir, temp_dir):
      if dir_to_rm is not None and dir_to_rm.exists():
        shutil.rmtree(dir_to_rm)


if __name__ == '__main__':
  main()