Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(50)

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/merge_results.py

Issue 2825253003: webkitpy: Improve performance of merge-results script. (Closed)
Patch Set: Fixing for review. Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/merge_results_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2017 The Chromium Authors. All rights reserved. 1 # Copyright 2017 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """Classes for merging layout tests results directories together. 5 """Classes for merging layout tests results directories together.
6 6
7 This is split into three parts: 7 This is split into three parts:
8 8
9 * Generic code to merge JSON data together. 9 * Generic code to merge JSON data together.
10 * Generic code to merge directories together. 10 * Generic code to merge directories together.
(...skipping 10 matching lines...) Expand all
21 * Helper functions can be provided to deal with merging specific file objects. 21 * Helper functions can be provided to deal with merging specific file objects.
22 * Helper functions are called when a given Match object returns true for the 22 * Helper functions are called when a given Match object returns true for the
23 filenames. 23 filenames.
24 * The default helper functions only merge if file contents match or the file 24 * The default helper functions only merge if file contents match or the file
25 only exists in one input directory. 25 only exists in one input directory.
26 26
27 The quickest way to understand how the mergers, helper functions and match 27 The quickest way to understand how the mergers, helper functions and match
28 objects work together is to look at the unit tests. 28 objects work together is to look at the unit tests.
29 """ 29 """
30 30
31 import collections
32 import itertools
31 import json 33 import json
32 import logging 34 import logging
33 import pprint 35 import pprint
34 import re 36 import re
35 import types 37 import types
36 38
37 from webkitpy.common.system.filesystem import FileSystem 39 from webkitpy.common.system.filesystem import FileSystem
38 40
39 41
40 _log = logging.getLogger(__name__) 42 _log = logging.getLogger(__name__)
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
87 def __init__(self, value): 89 def __init__(self, value):
88 self.value = value 90 self.value = value
89 91
90 def __call__(self, obj, name=None): 92 def __call__(self, obj, name=None):
91 return obj == self.value 93 return obj == self.value
92 94
93 95
94 class MergeFailure(Exception): 96 class MergeFailure(Exception):
95 """Base exception for merge failing.""" 97 """Base exception for merge failing."""
96 98
97 def __init__(self, msg, name, obj_a, obj_b): 99 def __init__(self, msg, name, objs):
98 emsg = ( 100 emsg = (
99 "Failure merging {name}: " 101 "Failure merging {name}: "
100 " {msg}\nTrying to merge {a} and {b}." 102 " {msg}\nTrying to merge {objs}."
101 ).format( 103 ).format(
102 name=name, 104 name=name,
103 msg=msg, 105 msg=msg,
104 a=obj_a, 106 objs=objs,
105 b=obj_b,
106 ) 107 )
107 Exception.__init__(self, emsg) 108 Exception.__init__(self, emsg)
108 109
109 @classmethod 110 @classmethod
110 def assert_type_eq(cls, name, obj_a, obj_b): 111 def assert_type_eq(cls, name, objs):
111 if type(obj_a) != type(obj_b): 112 obj_0 = objs[0]
112 raise cls("Types don't match", name, obj_a, obj_b) 113 for obj_n in objs[1:]:
114 if type(obj_0) != type(obj_n):
115 raise cls("Types don't match", name, (obj_0, obj_n))
113 116
114 117
115 class Merger(object): 118 class Merger(object):
116 """Base class for merger objects.""" 119 """Base class for merger objects."""
117 120
118 def __init__(self): 121 def __init__(self):
119 self.helpers = [] 122 self.helpers = []
120 123
121 def add_helper(self, match_func, merge_func): 124 def add_helper(self, match_func, merge_func):
122 """Add function which merges values. 125 """Add function which merges values.
123 126
124 match_func and merge_func are dependent on the merger object type. 127 match_func and merge_func are dependent on the merger object type.
125 When the function returns true, the merge_func will be called. 128 When the function returns true, the merge_func will be called.
126 129
127 Helpers are searched in last added, first checked order. This allows 130 Helpers are searched in last added, first checked order. This allows
128 more specific helpers to be added after more generic helpers. 131 more specific helpers to be added after more generic helpers.
129 """ 132 """
130 self.helpers.append((match_func, merge_func)) 133 self.helpers.append((match_func, merge_func))
131 134
132 135
133 class JSONMerger(Merger): 136 class JSONMerger(Merger):
134 """Merge two JSON-like objects. 137 """Merge JSON-like objects.
135 138
136 For adding helpers; 139 For adding helpers;
137 140
138 match_func is a function of form 141 match_func is a function of form
139 def f(obj, name=None) -> bool 142 def f(obj, name=None) -> bool
140 When the function returns true, the merge_func will be called. 143 When the function returns true, the merge_func will be called.
141 144
142 merge_func is a function of the form 145 merge_func is a function of the form
143 def f(obj_a, obj_b, name=None) -> obj_merged 146 def f(list_of_objs, name=None) -> obj_merged
144 Merge functions should *never* modify the input arguments. 147 Merge functions should *never* modify the input arguments.
145 """ 148 """
146 149
147 def __init__(self): 150 def __init__(self):
148 Merger.__init__(self) 151 Merger.__init__(self)
149 152
150 self.add_helper( 153 self.add_helper(
151 TypeMatch(types.ListType, types.TupleType), self.merge_listlike) 154 TypeMatch(types.ListType, types.TupleType), self.merge_listlike)
152 self.add_helper( 155 self.add_helper(
153 TypeMatch(types.DictType), self.merge_dictlike) 156 TypeMatch(types.DictType), self.merge_dictlike)
154 157
155 def fallback_matcher(self, obj_a, obj_b, name=None): 158 def fallback_matcher(self, objs, name=None):
156 raise MergeFailure( 159 raise MergeFailure(
157 "No merge helper found!", name, obj_a, obj_b) 160 "No merge helper found!", name, objs)
158 161
159 def merge_equal(self, obj_a, obj_b, name=None): 162 def merge_equal(self, objs, name=None):
160 """Merge two equal objects together.""" 163 """Merge equal objects together."""
161 if obj_a != obj_b: 164 obj_0 = objs[0]
162 raise MergeFailure( 165 for obj_n in objs[1:]:
163 "Unable to merge!", name, obj_a, obj_b) 166 if obj_0 != obj_n:
164 return obj_a 167 raise MergeFailure(
168 "Unable to merge!", name, (obj_0, obj_n))
169 return obj_0
165 170
166 def merge_listlike(self, list_a, list_b, name=None): # pylint: disable=unus ed-argument 171 def merge_listlike(self, lists, name=None): # pylint: disable=unused-argume nt
167 """Merge two things which are "list like" (tuples, lists, sets).""" 172 """Merge things which are "list like" (tuples, lists, sets)."""
168 assert type(list_a) == type(list_b), ( 173 MergeFailure.assert_type_eq(name, lists)
169 "Types of %r and %r don't match, refusing to merge." % ( 174 output = list(lists[0])
170 list_a, list_b)) 175 for list_n in lists[1:]:
171 output = list(list_a) 176 output.extend(list_n)
172 output.extend(list_b) 177 return lists[0].__class__(output)
173 return list_a.__class__(output)
174 178
175 def merge_dictlike(self, dict_a, dict_b, name=None): 179 def merge_dictlike(self, dicts, name=None):
176 """Merge two things which are dictionaries.""" 180 """Merge things which are dictionaries."""
177 assert type(dict_a) == type(dict_b), ( 181 MergeFailure.assert_type_eq(name, dicts)
178 "Types of %r and %r don't match, refusing to merge." % ( 182
179 dict_a, dict_b)) 183 ordered_keys = collections.OrderedDict.fromkeys(
180 dict_out = dict_a.__class__({}) 184 itertools.chain(*(d.iterkeys() for d in dicts)))
181 for key in dict_a.keys() + dict_b.keys(): 185
182 if key in dict_a and key in dict_b: 186 dict_out = dicts[0].__class__({})
187 for key in ordered_keys:
188 values_to_merge = []
189 for dobj in dicts:
190 if key in dobj:
191 values_to_merge.append(dobj[key])
192
193 if len(values_to_merge) == 1:
194 dict_out[key] = values_to_merge[0]
195 elif len(values_to_merge) > 1:
183 dict_out[key] = self.merge( 196 dict_out[key] = self.merge(
184 dict_a[key], dict_b[key], 197 values_to_merge,
185 name=join_name(name, key)) 198 name=join_name(name, key))
186 elif key in dict_a:
187 dict_out[key] = dict_a[key]
188 elif key in dict_b:
189 dict_out[key] = dict_b[key]
190 else: 199 else:
191 assert False 200 assert False, "Key %s not found in any inputs!" % (key,)
192 return dict_out 201 return dict_out
193 202
194 def merge(self, obj_a, obj_b, name=""): 203 def merge(self, objs, name=""):
195 """Generic merge function. 204 """Generic merge function.
196 205
197 name is a string representing the current key value separated by 206 name is a string representing the current key value separated by
198 semicolons. For example, if file.json had the following; 207 semicolons. For example, if file.json had the following;
199 208
200 {'key1': {'key2': 3}} 209 {'key1': {'key2': 3}}
201 210
202 Then the name of the value 3 is 'file.json:key1:key2' 211 Then the name of the value 3 is 'file.json:key1:key2'
203 """ 212 """
204 if obj_a is None and obj_b is None: 213 objs = [o for o in objs if o is not None]
214
215 if not objs:
205 return None 216 return None
206 elif obj_b is None:
207 return obj_a
208 elif obj_a is None:
209 return obj_b
210 217
211 MergeFailure.assert_type_eq(name, obj_a, obj_b) 218 MergeFailure.assert_type_eq(name, objs)
212 219
213 # Try the merge helpers. 220 # Try the merge helpers.
214 for match_func, merge_func in reversed(self.helpers): 221 for match_func, merge_func in reversed(self.helpers):
215 if match_func(obj_a, name): 222 for obj in objs:
216 return merge_func(obj_a, obj_b, name=name) 223 if match_func(obj, name):
217 if match_func(obj_b, name): 224 return merge_func(objs, name=name)
218 return merge_func(obj_a, obj_b, name=name)
219 225
220 return self.fallback_matcher(obj_a, obj_b, name=name) 226 return self.fallback_matcher(objs, name=name)
221 227
222 228
223 # Classes for recursively merging a directory together. 229 # Classes for recursively merging a directory together.
224 # ------------------------------------------------------------------------ 230 # ------------------------------------------------------------------------
225 231
226 232
227 class FilenameMatch(object): 233 class FilenameMatch(object):
228 """Match based on name matching a regex.""" 234 """Match based on name matching a regex."""
229 235
230 def __init__(self, regex): 236 def __init__(self, regex):
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
268 for filename in to_merge[1:]: 274 for filename in to_merge[1:]:
269 other_data = self.filesystem.read_binary_file(filename) 275 other_data = self.filesystem.read_binary_file(filename)
270 if data != other_data: 276 if data != other_data:
271 nonmatching.append(filename) 277 nonmatching.append(filename)
272 278
273 if nonmatching: 279 if nonmatching:
274 raise MergeFailure( 280 raise MergeFailure(
275 '\n'.join( 281 '\n'.join(
276 ['File contents don\'t match:'] + nonmatching), 282 ['File contents don\'t match:'] + nonmatching),
277 out_filename, 283 out_filename,
278 to_merge[0], to_merge[1:]) 284 to_merge)
279 285
280 self.filesystem.write_binary_file(out_filename, data) 286 self.filesystem.write_binary_file(out_filename, data)
281 287
282 288
283 class MergeFilesLinesSorted(MergeFiles): 289 class MergeFilesLinesSorted(MergeFiles):
284 """Merge and sort the files of the given files.""" 290 """Merge and sort the files of the given files."""
285 291
286 def __call__(self, out_filename, to_merge): 292 def __call__(self, out_filename, to_merge):
287 lines = [] 293 lines = []
288 for filename in to_merge: 294 for filename in to_merge:
(...skipping 26 matching lines...) Expand all
315 output. 321 output.
316 """ 322 """
317 323
318 def __init__(self, filesystem, json_data_merger=None, json_data_value_overri des=None): 324 def __init__(self, filesystem, json_data_merger=None, json_data_value_overri des=None):
319 MergeFiles.__init__(self, filesystem) 325 MergeFiles.__init__(self, filesystem)
320 self._json_data_merger = json_data_merger or JSONMerger() 326 self._json_data_merger = json_data_merger or JSONMerger()
321 self._json_data_value_overrides = json_data_value_overrides or {} 327 self._json_data_value_overrides = json_data_value_overrides or {}
322 328
323 def __call__(self, out_filename, to_merge): 329 def __call__(self, out_filename, to_merge):
324 try: 330 try:
325 before_a, output_data, after_a = self.load_jsonp( 331 before_0, new_json_data_0, after_0 = self.load_jsonp(
326 self.filesystem.open_binary_file_for_reading(to_merge[0])) 332 self.filesystem.open_binary_file_for_reading(to_merge[0]))
327 except ValueError as e: 333 except ValueError as e:
328 raise MergeFailure(e.message, to_merge[0], None, None) 334 raise MergeFailure(e.message, to_merge[0], None)
329 335
330 for filename in to_merge[1:]: 336 input_data = [new_json_data_0]
337 for filename_n in to_merge[1:]:
331 try: 338 try:
332 before_b, new_json_data, after_b = self.load_jsonp( 339 beforen, new_json_data_n, aftern = self.load_jsonp(
qyearsley 2017/04/19 22:40:20 Using an underscore (before_n/after_n) may look a
mithro 2017/04/20 02:02:45 Done now. This was actually the reason I didn't s
333 self.filesystem.open_binary_file_for_reading(filename)) 340 self.filesystem.open_binary_file_for_reading(filename_n))
334 except ValueError as e: 341 except ValueError as e:
335 raise MergeFailure(e.message, filename, None, None) 342 raise MergeFailure(e.message, filename_n, None)
336 343
337 if before_a != before_b: 344 if before_0 != beforen:
338 raise MergeFailure( 345 raise MergeFailure(
339 "jsonp starting data from %s doesn't match." % filename, 346 "jsonp starting data from %s doesn't match." % filename_n,
340 out_filename, 347 out_filename,
341 before_a, before_b) 348 [before_0, beforen])
342 349
343 if after_a != after_b: 350 if after_0 != aftern:
344 raise MergeFailure( 351 raise MergeFailure(
345 "jsonp ending data from %s doesn't match." % filename, 352 "jsonp ending data from %s doesn't match." % filename_n,
346 out_filename, 353 out_filename,
347 after_a, after_b) 354 [after_0, aftern])
348 355
349 output_data = self._json_data_merger.merge(output_data, new_json_dat a, filename) 356 input_data.append(new_json_data_n)
350 357
358 output_data = self._json_data_merger.merge(input_data, name=out_filename )
351 output_data.update(self._json_data_value_overrides) 359 output_data.update(self._json_data_value_overrides)
352 360
353 self.dump_jsonp( 361 self.dump_jsonp(
354 self.filesystem.open_binary_file_for_writing(out_filename), 362 self.filesystem.open_binary_file_for_writing(out_filename),
355 before_a, output_data, after_a) 363 before_0, output_data, after_0)
356 364
357 @staticmethod 365 @staticmethod
358 def load_jsonp(fd): 366 def load_jsonp(fd):
359 """Load a JSONP file and return the JSON data parsed. 367 """Load a JSONP file and return the JSON data parsed.
360 368
361 JSONP files have a JSON data structure wrapped in a function call or 369 JSONP files have a JSON data structure wrapped in a function call or
362 other non-JSON data. 370 other non-JSON data.
363 """ 371 """
364 in_data = fd.read() 372 in_data = fd.read()
365 373
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after
458 # rel_file is the path of f relative to the base directory 466 # rel_file is the path of f relative to the base directory
459 rel_file = self.filesystem.join(dir_path, f)[len(base_dir) + 1:] 467 rel_file = self.filesystem.join(dir_path, f)[len(base_dir) + 1:]
460 files.setdefault(rel_file, []).append(base_dir) 468 files.setdefault(rel_file, []).append(base_dir)
461 469
462 # Go through each file and try to merge it. 470 # Go through each file and try to merge it.
463 # partial_file_path is the file relative to the directories. 471 # partial_file_path is the file relative to the directories.
464 for partial_file_path, in_dirs in sorted(files.iteritems()): 472 for partial_file_path, in_dirs in sorted(files.iteritems()):
465 out_path = self.filesystem.join(output_dir, partial_file_path) 473 out_path = self.filesystem.join(output_dir, partial_file_path)
466 if self.filesystem.exists(out_path): 474 if self.filesystem.exists(out_path):
467 raise MergeFailure( 475 raise MergeFailure(
468 'File %s already exist in output.', out_path, None, None) 476 'File %s already exist in output.', out_path, None)
469 477
470 dirname = self.filesystem.dirname(out_path) 478 dirname = self.filesystem.dirname(out_path)
471 if not self.filesystem.exists(dirname): 479 if not self.filesystem.exists(dirname):
472 self.filesystem.maybe_make_directory(dirname) 480 self.filesystem.maybe_make_directory(dirname)
473 481
474 to_merge = [self.filesystem.join(d, partial_file_path) for d in in_d irs] 482 to_merge = [self.filesystem.join(d, partial_file_path) for d in in_d irs]
475 483
476 _log.debug("Creating merged %s from %s", out_path, to_merge) 484 _log.debug("Creating merged %s from %s", out_path, to_merge)
477 485
478 for match_func, merge_func in reversed(self.helpers): 486 for match_func, merge_func in reversed(self.helpers):
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
529 ':num_passes$', 537 ':num_passes$',
530 ':num_regressions$', 538 ':num_regressions$',
531 ':skipped$', 539 ':skipped$',
532 ':skips$', 540 ':skips$',
533 # All keys inside the num_failures_by_type entry. 541 # All keys inside the num_failures_by_type entry.
534 ':num_failures_by_type:', 542 ':num_failures_by_type:',
535 ] 543 ]
536 for match_name in addable: 544 for match_name in addable:
537 self.add_helper( 545 self.add_helper(
538 NameMatch(match_name), 546 NameMatch(match_name),
539 lambda a, b, name=None: a + b) 547 lambda o, name=None: sum(o))
540 548
541 # If any shard is interrupted, mark the whole thing as interrupted. 549 # If any shard is interrupted, mark the whole thing as interrupted.
542 self.add_helper( 550 self.add_helper(
543 NameMatch(':interrupted$'), 551 NameMatch(':interrupted$'),
544 lambda a, b, name=None: a or b) 552 lambda o, name=None: bool(sum(o)))
545 553
546 # Layout test directory value is randomly created on each shard, so 554 # Layout test directory value is randomly created on each shard, so
547 # clear it. 555 # clear it.
548 self.add_helper( 556 self.add_helper(
549 NameMatch(':layout_tests_dir$'), 557 NameMatch(':layout_tests_dir$'),
550 lambda a, b, name=None: None) 558 lambda o, name=None: None)
551 559
552 # seconds_since_epoch is the start time, so we just take the earliest. 560 # seconds_since_epoch is the start time, so we just take the earliest.
553 self.add_helper( 561 self.add_helper(
554 NameMatch(':seconds_since_epoch$'), 562 NameMatch(':seconds_since_epoch$'),
555 lambda a, b, name=None: min(a, b)) 563 lambda o, name=None: min(*o))
556 564
557 def fallback_matcher(self, obj_a, obj_b, name=None): 565 def fallback_matcher(self, objs, name=None):
558 if self.allow_unknown_if_matching: 566 if self.allow_unknown_if_matching:
559 result = self.merge_equal(obj_a, obj_b, name) 567 result = self.merge_equal(objs, name)
560 _log.warning('Unknown value %s, accepting anyway as it matches.', na me) 568 _log.warning('Unknown value %s, accepting anyway as it matches.', na me)
561 return result 569 return result
562 return JSONMerger.fallback_matcher(self, obj_a, obj_b, name) 570 return JSONMerger.fallback_matcher(self, objs, name)
563 571
564 572
565 class LayoutTestDirMerger(DirMerger): 573 class LayoutTestDirMerger(DirMerger):
566 """Merge layout test result directory.""" 574 """Merge layout test result directory."""
567 575
568 def __init__(self, filesystem=None, 576 def __init__(self, filesystem=None,
569 results_json_value_overrides=None, 577 results_json_value_overrides=None,
570 results_json_allow_unknown_if_matching=False): 578 results_json_allow_unknown_if_matching=False):
571 DirMerger.__init__(self, filesystem) 579 DirMerger.__init__(self, filesystem)
572 580
573 # JSON merger for non-"result style" JSON files. 581 # JSON merger for non-"result style" JSON files.
574 basic_json_data_merger = JSONMerger() 582 basic_json_data_merger = JSONMerger()
575 basic_json_data_merger.fallback_matcher = basic_json_data_merger.merge_e qual 583 basic_json_data_merger.fallback_matcher = basic_json_data_merger.merge_e qual
576 self.add_helper( 584 self.add_helper(
577 FilenameMatch('\\.json'), 585 FilenameMatch('\\.json$'),
578 MergeFilesJSONP(self.filesystem, basic_json_data_merger)) 586 MergeFilesJSONP(self.filesystem, basic_json_data_merger))
579 587
580 # access_log and error_log are httpd log files which are sortable. 588 # access_log and error_log are httpd log files which are sortable.
581 self.add_helper( 589 self.add_helper(
582 FilenameMatch('access_log\\.txt'), 590 FilenameMatch('access_log\\.txt$'),
583 MergeFilesLinesSorted(self.filesystem)) 591 MergeFilesLinesSorted(self.filesystem))
584 self.add_helper( 592 self.add_helper(
585 FilenameMatch('error_log\\.txt'), 593 FilenameMatch('error_log\\.txt$'),
586 MergeFilesLinesSorted(self.filesystem)) 594 MergeFilesLinesSorted(self.filesystem))
587 595
588 # pywebsocket files aren't particularly useful, so just save them. 596 # pywebsocket files aren't particularly useful, so just save them.
589 self.add_helper( 597 self.add_helper(
590 FilenameMatch('pywebsocket\\.ws\\.log-.*-err.txt'), 598 FilenameMatch('pywebsocket\\.ws\\.log-.*-err\\.txt$'),
qyearsley 2017/04/19 22:40:20 Could also use a "raw string" for strings that con
mithro 2017/04/20 02:02:45 Yeah, raw string can be a little bit dangerous to
591 MergeFilesKeepFiles(self.filesystem)) 599 MergeFilesKeepFiles(self.filesystem))
592 600
593 # These JSON files have "result style" JSON in them. 601 # These JSON files have "result style" JSON in them.
594 results_json_file_merger = MergeFilesJSONP( 602 results_json_file_merger = MergeFilesJSONP(
595 self.filesystem, 603 self.filesystem,
596 JSONTestResultsMerger( 604 JSONTestResultsMerger(
597 allow_unknown_if_matching=results_json_allow_unknown_if_matching ), 605 allow_unknown_if_matching=results_json_allow_unknown_if_matching ),
598 json_data_value_overrides=results_json_value_overrides or {}) 606 json_data_value_overrides=results_json_value_overrides or {})
599 607
600 self.add_helper( 608 self.add_helper(
601 FilenameMatch('failing_results.json'), 609 FilenameMatch('failing_results\\.json$'),
602 results_json_file_merger) 610 results_json_file_merger)
603 self.add_helper( 611 self.add_helper(
604 FilenameMatch('full_results.json'), 612 FilenameMatch('full_results\\.json$'),
605 results_json_file_merger) 613 results_json_file_merger)
606 self.add_helper( 614 self.add_helper(
607 FilenameMatch('output.json'), 615 FilenameMatch('output\\.json$'),
608 results_json_file_merger) 616 results_json_file_merger)
OLDNEW
« no previous file with comments | « no previous file | third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/merge_results_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698