Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
safetynet_compare.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# Copyright 2017 The PDFium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Compares the performance of two versions of the pdfium code."""
6
7import argparse
8import functools
9import glob
10import json
11import multiprocessing
12import os
13import re
14import shutil
15import subprocess
16import sys
17import tempfile
18
19from common import GetBooleanGnArg
20from common import PrintErr
21from common import RunCommandPropagateErr
22from githelper import GitHelper
23from safetynet_conclusions import ComparisonConclusions
24from safetynet_conclusions import PrintConclusionsDictHumanReadable
25from safetynet_conclusions import RATING_IMPROVEMENT
26from safetynet_conclusions import RATING_REGRESSION
27from safetynet_image import ImageComparison
28
29
30def RunSingleTestCaseParallel(this, run_label, build_dir, test_case):
31 result = this.RunSingleTestCase(run_label, build_dir, test_case)
32 return (test_case, result)
33
34
36 """A comparison between two branches of pdfium."""
37
38 def __init__(self, args):
39 self.git = GitHelper()
40 self.args = args
41 self._InitPaths()
42
43 def _InitPaths(self):
44 if self.args.this_repo:
45 self.safe_script_dir = self.args.build_dir
46 else:
47 self.safe_script_dir = os.path.join('testing', 'tools')
48
49 self.safe_measure_script_path = os.path.abspath(
50 os.path.join(self.safe_script_dir, 'safetynet_measure.py'))
51
52 input_file_re = re.compile('^.+[.]pdf$')
53 self.test_cases = []
54 for input_path in self.args.input_paths:
55 if os.path.isfile(input_path):
56 self.test_cases.append(input_path)
57 elif os.path.isdir(input_path):
58 for file_dir, _, filename_list in os.walk(input_path):
59 for input_filename in filename_list:
60 if input_file_re.match(input_filename):
61 file_path = os.path.join(file_dir, input_filename)
62 if os.path.isfile(file_path):
63 self.test_cases.append(file_path)
64
65 self.after_build_dir = self.args.build_dir
66 if self.args.build_dir_before:
67 self.before_build_dir = self.args.build_dir_before
68 else:
70
71 def Run(self):
72 """Runs comparison by checking out branches, building and measuring them.
73
74 Returns:
75 Exit code for the script.
76 """
77 if self.args.this_repo:
79
80 if self.args.branch_after:
81 if self.args.this_repo:
82 before, after = self._ProfileTwoOtherBranchesInThisRepo(
83 self.args.branch_before, self.args.branch_after)
84 else:
85 before, after = self._ProfileTwoOtherBranches(self.args.branch_before,
86 self.args.branch_after)
87 elif self.args.branch_before:
88 if self.args.this_repo:
89 before, after = self._ProfileCurrentAndOtherBranchInThisRepo(
90 self.args.branch_before)
91 else:
92 before, after = self._ProfileCurrentAndOtherBranch(
93 self.args.branch_before)
94 else:
95 if self.args.this_repo:
97 else:
98 before, after = self._ProfileLocalChangesAndCurrentBranch()
99
100 conclusions = self._DrawConclusions(before, after)
101 conclusions_dict = conclusions.GetOutputDict()
102 conclusions_dict.setdefault('metadata', {})['profiler'] = self.args.profiler
103
104 self._PrintConclusions(conclusions_dict)
105
106 self._CleanUp(conclusions)
107
108 if self.args.png_dir:
109 image_comparison = ImageComparison(
110 self.after_build_dir, self.args.png_dir, ('before', 'after'),
111 self.args.num_workers, self.args.png_threshold)
112 image_comparison.Run(open_in_browser=not self.args.machine_readable)
113
114 return 0
115
117 """Freezes a version of the measuring script.
118
119 This is needed to make sure we are comparing the pdfium library changes and
120 not script changes that may happen between the two branches.
121 """
122 self.__FreezeFile(os.path.join('testing', 'tools', 'safetynet_measure.py'))
123 self.__FreezeFile(os.path.join('testing', 'tools', 'common.py'))
124
125 def __FreezeFile(self, filename):
126 RunCommandPropagateErr(['cp', filename, self.safe_script_dir],
127 exit_status_on_error=1)
128
129 def _ProfileTwoOtherBranchesInThisRepo(self, before_branch, after_branch):
130 """Profiles two branches that are not the current branch.
131
132 This is done in the local repository and changes may not be restored if the
133 script fails or is interrupted.
134
135 after_branch does not need to descend from before_branch, they will be
136 measured the same way
137
138 Args:
139 before_branch: One branch to profile.
140 after_branch: Other branch to profile.
141
142 Returns:
143 A tuple (before, after), where each of before and after is a dict
144 mapping a test case name to the profiling values for that test case
145 in the given branch.
146 """
147 branch_to_restore = self.git.GetCurrentBranchName()
148
149 self._StashLocalChanges()
150
151 self._CheckoutBranch(after_branch)
153 after = self._MeasureCurrentBranch('after', self.after_build_dir)
154
155 self._CheckoutBranch(before_branch)
157 before = self._MeasureCurrentBranch('before', self.before_build_dir)
158
159 self._CheckoutBranch(branch_to_restore)
161
162 return before, after
163
164 def _ProfileTwoOtherBranches(self, before_branch, after_branch):
165 """Profiles two branches that are not the current branch.
166
167 This is done in new, cloned repositories, therefore it is safer but slower
168 and requires downloads.
169
170 after_branch does not need to descend from before_branch, they will be
171 measured the same way
172
173 Args:
174 before_branch: One branch to profile.
175 after_branch: Other branch to profile.
176
177 Returns:
178 A tuple (before, after), where each of before and after is a dict
179 mapping a test case name to the profiling values for that test case
180 in the given branch.
181 """
182 after = self._ProfileSeparateRepo('after', self.after_build_dir,
183 after_branch)
184 before = self._ProfileSeparateRepo('before', self.before_build_dir,
185 before_branch)
186 return before, after
187
189 """Profiles the current branch (with uncommitted changes) and another one.
190
191 This is done in the local repository and changes may not be restored if the
192 script fails or is interrupted.
193
194 The current branch does not need to descend from other_branch.
195
196 Args:
197 other_branch: Other branch to profile that is not the current.
198
199 Returns:
200 A tuple (before, after), where each of before and after is a dict
201 mapping a test case name to the profiling values for that test case
202 in the given branch. The current branch is considered to be "after" and
203 the other branch is considered to be "before".
204 """
205 branch_to_restore = self.git.GetCurrentBranchName()
206
208 after = self._MeasureCurrentBranch('after', self.after_build_dir)
209
210 self._StashLocalChanges()
211
212 self._CheckoutBranch(other_branch)
214 before = self._MeasureCurrentBranch('before', self.before_build_dir)
215
216 self._CheckoutBranch(branch_to_restore)
218
219 return before, after
220
221 def _ProfileCurrentAndOtherBranch(self, other_branch):
222 """Profiles the current branch (with uncommitted changes) and another one.
223
224 This is done in new, cloned repositories, therefore it is safer but slower
225 and requires downloads.
226
227 The current branch does not need to descend from other_branch.
228
229 Args:
230 other_branch: Other branch to profile that is not the current. None will
231 compare to the same branch.
232
233 Returns:
234 A tuple (before, after), where each of before and after is a dict
235 mapping a test case name to the profiling values for that test case
236 in the given branch. The current branch is considered to be "after" and
237 the other branch is considered to be "before".
238 """
240 after = self._MeasureCurrentBranch('after', self.after_build_dir)
241
242 before = self._ProfileSeparateRepo('before', self.before_build_dir,
243 other_branch)
244
245 return before, after
246
248 """Profiles the current branch with and without uncommitted changes.
249
250 This is done in the local repository and changes may not be restored if the
251 script fails or is interrupted.
252
253 Returns:
254 A tuple (before, after), where each of before and after is a dict
255 mapping a test case name to the profiling values for that test case
256 using the given version. The current branch without uncommitted changes is
257 considered to be "before" and with uncommitted changes is considered to be
258 "after".
259 """
261 after = self._MeasureCurrentBranch('after', self.after_build_dir)
262
263 pushed = self._StashLocalChanges()
264 if not pushed and not self.args.build_dir_before:
265 PrintErr('Warning: No local changes to compare')
266
267 before_build_dir = self.before_build_dir
268
269 self._BuildCurrentBranch(before_build_dir)
270 before = self._MeasureCurrentBranch('before', before_build_dir)
271
273
274 return before, after
275
277 """Profiles the current branch with and without uncommitted changes.
278
279 This is done in new, cloned repositories, therefore it is safer but slower
280 and requires downloads.
281
282 Returns:
283 A tuple (before, after), where each of before and after is a dict
284 mapping a test case name to the profiling values for that test case
285 using the given version. The current branch without uncommitted changes is
286 considered to be "before" and with uncommitted changes is considered to be
287 "after".
288 """
289 return self._ProfileCurrentAndOtherBranch(other_branch=None)
290
291 def _ProfileSeparateRepo(self, run_label, relative_build_dir, branch):
292 """Profiles a branch in a a temporary git repository.
293
294 Args:
295 run_label: String to differentiate this version of the code in output
296 files from other versions.
297 relative_build_dir: Path to the build dir in the current working dir to
298 clone build args from.
299 branch: Branch to checkout in the new repository. None will
300 profile the same branch checked out in the original repo.
301 Returns:
302 A dict mapping each test case name to the profiling values for that
303 test case.
304 """
305 build_dir = self._CreateTempRepo('repo_%s' % run_label, relative_build_dir,
306 branch)
307
308 self._BuildCurrentBranch(build_dir)
309 return self._MeasureCurrentBranch(run_label, build_dir)
310
311 def _CreateTempRepo(self, dir_name, relative_build_dir, branch):
312 """Clones a temporary git repository out of the current working dir.
313
314 Args:
315 dir_name: Name for the temporary repository directory
316 relative_build_dir: Path to the build dir in the current working dir to
317 clone build args from.
318 branch: Branch to checkout in the new repository. None will keep checked
319 out the same branch as the local repo.
320 Returns:
321 Path to the build directory of the new repository.
322 """
323 cwd = os.getcwd()
324
325 repo_dir = tempfile.mkdtemp(suffix='-%s' % dir_name)
326 src_dir = os.path.join(repo_dir, 'pdfium')
327
328 self.git.CloneLocal(os.getcwd(), src_dir)
329
330 if branch is not None:
331 os.chdir(src_dir)
332 self.git.Checkout(branch)
333
334 os.chdir(repo_dir)
335 PrintErr('Syncing...')
336
337 cmd = [
338 'gclient', 'config', '--unmanaged',
339 'https://pdfium.googlesource.com/pdfium.git'
340 ]
341 if self.args.cache_dir:
342 cmd.append('--cache-dir=%s' % self.args.cache_dir)
343 RunCommandPropagateErr(cmd, exit_status_on_error=1)
344
345 RunCommandPropagateErr(['gclient', 'sync', '--force'],
346 exit_status_on_error=1)
347
348 PrintErr('Done.')
349
350 build_dir = os.path.join(src_dir, relative_build_dir)
351 os.makedirs(build_dir)
352 os.chdir(src_dir)
353
354 source_gn_args = os.path.join(cwd, relative_build_dir, 'args.gn')
355 dest_gn_args = os.path.join(build_dir, 'args.gn')
356 shutil.copy(source_gn_args, dest_gn_args)
357
358 RunCommandPropagateErr(['gn', 'gen', relative_build_dir],
359 exit_status_on_error=1)
360
361 os.chdir(cwd)
362
363 return build_dir
364
365 def _CheckoutBranch(self, branch):
366 PrintErr("Checking out branch '%s'" % branch)
367 self.git.Checkout(branch)
368
370 PrintErr('Stashing local changes')
371 return self.git.StashPush()
372
374 PrintErr('Restoring local changes')
375 self.git.StashPopAll()
376
377 def _BuildCurrentBranch(self, build_dir):
378 """Synchronizes and builds the current version of pdfium.
379
380 Args:
381 build_dir: String with path to build directory
382 """
383 PrintErr('Syncing...')
384 RunCommandPropagateErr(['gclient', 'sync', '--force'],
385 exit_status_on_error=1)
386 PrintErr('Done.')
387
388 PrintErr('Building...')
389 cmd = ['ninja', '-C', build_dir, 'pdfium_test']
390 if GetBooleanGnArg('use_goma', build_dir):
391 cmd.extend(['-j', '250'])
392 RunCommandPropagateErr(cmd, stdout_has_errors=True, exit_status_on_error=1)
393 PrintErr('Done.')
394
395 def _MeasureCurrentBranch(self, run_label, build_dir):
396 PrintErr('Measuring...')
397 if self.args.num_workers > 1 and len(self.test_cases) > 1:
398 results = self._RunAsync(run_label, build_dir)
399 else:
400 results = self._RunSync(run_label, build_dir)
401 PrintErr('Done.')
402
403 return results
404
405 def _RunSync(self, run_label, build_dir):
406 """Profiles the test cases synchronously.
407
408 Args:
409 run_label: String to differentiate this version of the code in output
410 files from other versions.
411 build_dir: String with path to build directory
412
413 Returns:
414 A dict mapping each test case name to the profiling values for that
415 test case.
416 """
417 results = {}
418
419 for test_case in self.test_cases:
420 result = self.RunSingleTestCase(run_label, build_dir, test_case)
421 if result is not None:
422 results[test_case] = result
423
424 return results
425
426 def _RunAsync(self, run_label, build_dir):
427 """Profiles the test cases asynchronously.
428
429 Uses as many workers as configured by --num-workers.
430
431 Args:
432 run_label: String to differentiate this version of the code in output
433 files from other versions.
434 build_dir: String with path to build directory
435
436 Returns:
437 A dict mapping each test case name to the profiling values for that
438 test case.
439 """
440 results = {}
441 pool = multiprocessing.Pool(self.args.num_workers)
442 worker_func = functools.partial(RunSingleTestCaseParallel, self, run_label,
443 build_dir)
444
445 try:
446 # The timeout is a workaround for http://bugs.python.org/issue8296
447 # which prevents KeyboardInterrupt from working.
448 one_year_in_seconds = 3600 * 24 * 365
449 worker_results = (
450 pool.map_async(worker_func, self.test_cases).get(one_year_in_seconds))
451 for worker_result in worker_results:
452 test_case, result = worker_result
453 if result is not None:
454 results[test_case] = result
455 except KeyboardInterrupt:
456 pool.terminate()
457 sys.exit(1)
458 else:
459 pool.close()
460
461 pool.join()
462
463 return results
464
465 def RunSingleTestCase(self, run_label, build_dir, test_case):
466 """Profiles a single test case.
467
468 Args:
469 run_label: String to differentiate this version of the code in output
470 files from other versions.
471 build_dir: String with path to build directory
472 test_case: Path to the test case.
473
474 Returns:
475 The measured profiling value for that test case.
476 """
477 command = [
478 self.safe_measure_script_path, test_case,
479 '--build-dir=%s' % build_dir
480 ]
481
482 if self.args.interesting_section:
483 command.append('--interesting-section')
484
485 if self.args.profiler:
486 command.append('--profiler=%s' % self.args.profiler)
487
488 profile_file_path = self._GetProfileFilePath(run_label, test_case)
489 if profile_file_path:
490 command.append('--output-path=%s' % profile_file_path)
491
492 if self.args.png_dir:
493 command.append('--png')
494
495 if self.args.pages:
496 command.extend(['--pages', self.args.pages])
497
498 output = RunCommandPropagateErr(command)
499
500 if output is None:
501 return None
502
503 if self.args.png_dir:
504 self._MoveImages(test_case, run_label)
505
506 # Get the time number as output, making sure it's just a number
507 output = output.strip()
508 if re.match('^[0-9]+$', output):
509 return int(output)
510
511 return None
512
513 def _MoveImages(self, test_case, run_label):
514 png_dir = os.path.join(self.args.png_dir, run_label)
515 if not os.path.exists(png_dir):
516 os.makedirs(png_dir)
517
518 test_case_dir, test_case_filename = os.path.split(test_case)
519 test_case_png_matcher = '%s.*.png' % test_case_filename
520 for output_png in glob.glob(
521 os.path.join(test_case_dir, test_case_png_matcher)):
522 shutil.move(output_png, png_dir)
523
524 def _GetProfileFilePath(self, run_label, test_case):
525 if self.args.output_dir:
526 output_filename = (
527 'callgrind.out.%s.%s' % (test_case.replace('/', '_'), run_label))
528 return os.path.join(self.args.output_dir, output_filename)
529 return None
530
531 def _DrawConclusions(self, times_before_branch, times_after_branch):
532 """Draws conclusions comparing results of test runs in two branches.
533
534 Args:
535 times_before_branch: A dict mapping each test case name to the
536 profiling values for that test case in the branch to be considered
537 as the baseline.
538 times_after_branch: A dict mapping each test case name to the
539 profiling values for that test case in the branch to be considered
540 as the new version.
541
542 Returns:
543 ComparisonConclusions with all test cases processed.
544 """
545 conclusions = ComparisonConclusions(self.args.threshold_significant)
546
547 for test_case in sorted(self.test_cases):
548 before = times_before_branch.get(test_case)
549 after = times_after_branch.get(test_case)
550 conclusions.ProcessCase(test_case, before, after)
551
552 return conclusions
553
554 def _PrintConclusions(self, conclusions_dict):
555 """Prints the conclusions as the script output.
556
557 Depending on the script args, this can output a human or a machine-readable
558 version of the conclusions.
559
560 Args:
561 conclusions_dict: Dict to print returned from
562 ComparisonConclusions.GetOutputDict().
563 """
564 if self.args.machine_readable:
565 print(json.dumps(conclusions_dict))
566 else:
567 PrintConclusionsDictHumanReadable(
568 conclusions_dict, colored=True, key=self.args.case_order)
569
570 def _CleanUp(self, conclusions):
571 """Removes profile output files for uninteresting cases.
572
573 Cases without significant regressions or improvements and considered
574 uninteresting.
575
576 Args:
577 conclusions: A ComparisonConclusions.
578 """
579 if not self.args.output_dir:
580 return
581
582 if self.args.profiler != 'callgrind':
583 return
584
585 for case_result in conclusions.GetCaseResults().values():
586 if case_result.rating not in [RATING_REGRESSION, RATING_IMPROVEMENT]:
587 self._CleanUpOutputFile('before', case_result.case_name)
588 self._CleanUpOutputFile('after', case_result.case_name)
589
590 def _CleanUpOutputFile(self, run_label, case_name):
591 """Removes one profile output file.
592
593 If the output file does not exist, fails silently.
594
595 Args:
596 run_label: String to differentiate a version of the code in output
597 files from other versions.
598 case_name: String identifying test case for which to remove the output
599 file.
600 """
601 try:
602 os.remove(self._GetProfileFilePath(run_label, case_name))
603 except OSError:
604 pass
605
606
607def main():
608 parser = argparse.ArgumentParser()
609 parser.add_argument(
610 'input_paths',
611 nargs='+',
612 help='pdf files or directories to search for pdf files '
613 'to run as test cases')
614 parser.add_argument(
615 '--branch-before',
616 help='git branch to use as "before" for comparison. '
617 'Omitting this will use the current branch '
618 'without uncommitted changes as the baseline.')
619 parser.add_argument(
620 '--branch-after',
621 help='git branch to use as "after" for comparison. '
622 'Omitting this will use the current branch '
623 'with uncommitted changes.')
624 parser.add_argument(
625 '--build-dir',
626 default=os.path.join('out', 'Release'),
627 help='relative path from the base source directory '
628 'to the build directory')
629 parser.add_argument(
630 '--build-dir-before',
631 help='relative path from the base source directory '
632 'to the build directory for the "before" branch, if '
633 'different from the build directory for the '
634 '"after" branch')
635 parser.add_argument(
636 '--cache-dir',
637 default=None,
638 help='directory with a new or preexisting cache for '
639 'downloads. Default is to not use a cache.')
640 parser.add_argument(
641 '--this-repo',
642 action='store_true',
643 help='use the repository where the script is instead of '
644 'checking out a temporary one. This is faster and '
645 'does not require downloads, but although it '
646 'restores the state of the local repo, if the '
647 'script is killed or crashes the changes can remain '
648 'stashed and you may be on another branch.')
649 parser.add_argument(
650 '--profiler',
651 default='callgrind',
652 help='which profiler to use. Supports callgrind, '
653 'perfstat, and none. Default is callgrind.')
654 parser.add_argument(
655 '--interesting-section',
656 action='store_true',
657 help='whether to measure just the interesting section or '
658 'the whole test harness. Limiting to only the '
659 'interesting section does not work on Release since '
660 'the delimiters are optimized out')
661 parser.add_argument(
662 '--pages',
663 help='selects some pages to be rendered. Page numbers '
664 'are 0-based. "--pages A" will render only page A. '
665 '"--pages A-B" will render pages A to B '
666 '(inclusive).')
667 parser.add_argument(
668 '--num-workers',
669 default=multiprocessing.cpu_count(),
670 type=int,
671 help='run NUM_WORKERS jobs in parallel')
672 parser.add_argument(
673 '--output-dir', help='directory to write the profile data output files')
674 parser.add_argument(
675 '--png-dir',
676 default=None,
677 help='outputs pngs to the specified directory that can '
678 'be compared with a static html generated. Will '
679 'affect performance measurements.')
680 parser.add_argument(
681 '--png-threshold',
682 default=0.0,
683 type=float,
684 help='Requires --png-dir. Threshold above which a png '
685 'is considered to have changed.')
686 parser.add_argument(
687 '--threshold-significant',
688 default=0.02,
689 type=float,
690 help='variations in performance above this factor are '
691 'considered significant')
692 parser.add_argument(
693 '--machine-readable',
694 action='store_true',
695 help='whether to get output for machines. If enabled the '
696 'output will be a json with the format specified in '
697 'ComparisonConclusions.GetOutputDict(). Default is '
698 'human-readable.')
699 parser.add_argument(
700 '--case-order',
701 default=None,
702 help='what key to use when sorting test cases in the '
703 'output. Accepted values are "after", "before", '
704 '"ratio" and "rating". Default is sorting by test '
705 'case path.')
706
707 args = parser.parse_args()
708
709 # Always start at the pdfium src dir, which is assumed to be two level above
710 # this script.
711 pdfium_src_dir = os.path.join(
712 os.path.dirname(__file__), os.path.pardir, os.path.pardir)
713 os.chdir(pdfium_src_dir)
714
715 git = GitHelper()
716
717 if args.branch_after and not args.branch_before:
718 PrintErr('--branch-after requires --branch-before to be specified.')
719 return 1
720
721 if args.branch_after and not git.BranchExists(args.branch_after):
722 PrintErr('Branch "%s" does not exist' % args.branch_after)
723 return 1
724
725 if args.branch_before and not git.BranchExists(args.branch_before):
726 PrintErr('Branch "%s" does not exist' % args.branch_before)
727 return 1
728
729 if args.output_dir:
730 args.output_dir = os.path.expanduser(args.output_dir)
731 if not os.path.isdir(args.output_dir):
732 PrintErr('"%s" is not a directory' % args.output_dir)
733 return 1
734
735 if args.png_dir:
736 args.png_dir = os.path.expanduser(args.png_dir)
737 if not os.path.isdir(args.png_dir):
738 PrintErr('"%s" is not a directory' % args.png_dir)
739 return 1
740
741 if args.threshold_significant <= 0.0:
742 PrintErr('--threshold-significant should receive a positive float')
743 return 1
744
745 if args.png_threshold:
746 if not args.png_dir:
747 PrintErr('--png-threshold requires --png-dir to be specified.')
748 return 1
749
750 if args.png_threshold <= 0.0:
751 PrintErr('--png-threshold should receive a positive float')
752 return 1
753
754 if args.pages:
755 if not re.match(r'^\d+(-\d+)?$', args.pages):
756 PrintErr('Supported formats for --pages are "--pages 7" and '
757 '"--pages 3-6"')
758 return 1
759
760 run = CompareRun(args)
761 return run.Run()
762
763
764if __name__ == '__main__':
765 sys.exit(main())
_ProfileSeparateRepo(self, run_label, relative_build_dir, branch)
_CleanUpOutputFile(self, run_label, case_name)
_ProfileTwoOtherBranches(self, before_branch, after_branch)
_GetProfileFilePath(self, run_label, test_case)
RunSingleTestCase(self, run_label, build_dir, test_case)
_CreateTempRepo(self, dir_name, relative_build_dir, branch)
_DrawConclusions(self, times_before_branch, times_after_branch)
_MeasureCurrentBranch(self, run_label, build_dir)
_ProfileTwoOtherBranchesInThisRepo(self, before_branch, after_branch)
list append(new Employee("Blackpool", "Stephen"))
RunSingleTestCaseParallel(this, run_label, build_dir, test_case)
static QDBusError::ErrorType get(const char *name)
QDebug print(QDebug debug, QSslError::SslError error)