| Index: media/tools/layout_tests/test_expectations.py
|
| diff --git a/media/tools/layout_tests/test_expectations.py b/media/tools/layout_tests/test_expectations.py
|
| index 81041a9a5d3a927beddef5a3b07662ed91e1b940..bb8ff781a80d55d7c4d8dc39374b29020dc875da 100755
|
| --- a/media/tools/layout_tests/test_expectations.py
|
| +++ b/media/tools/layout_tests/test_expectations.py
|
| @@ -3,439 +3,154 @@
|
| # Use of this source code is governed by a BSD-style license that can be
|
| # found in the LICENSE file.
|
|
|
| -"""A module for TestExpectionsManager.
|
| +"""A Module to analyze test expectations for Webkit layout tests."""
|
|
|
| -TestExpectaionManager manages data for Webkit layout tests.
|
| -"""
|
| -
|
| -import csv
|
| import re
|
| -import time
|
| -import urllib
|
| -import pysvn
|
| -
|
| -from csv_utils import CsvUtils
|
| -from layout_test_test_case import LayoutTestCaseManager
|
| -from test_case_patterns import TestCasePatterns
|
| -
|
| -
|
| -class TestExpectationsManager(object):
|
| - """This class manages test expectations data for Webkit layout tests.
|
| -
|
| - The detail of the test expectation file can be found in
|
| - http://trac.webkit.org/wiki/TestExpectations.
|
| -
|
| - This class does the following:
|
| - (1) get test expectation file from WebKit subversion source
|
| - repository using pysvn.
|
| - (2) parse the file and generate CSV entries.
|
| - (3) create hyperlinks where appropriate (e.g., test case name).
|
| +import urllib2
|
| +
|
| +# Default Webkit SVN location for chromium test expectation file.
|
| +# TODO(imasaki): support multiple test expectations files.
|
| +DEFAULT_TEST_EXPECTATION_LOCATION = (
|
| + 'http://svn.webkit.org/repository/webkit/trunk/'
|
| + 'LayoutTests/platform/chromium/test_expectations.txt')
|
| +
|
| +# The following is from test expectation syntax. The detail can be found in
|
| +# http://www.chromium.org/developers/testing/
|
| +# webkit-layout-tests#TOC-Test-Expectations
|
| +# <decision> ::== [SKIP] [WONTFIX] [SLOW]
|
| +DECISION_NAMES = ['SKIP', 'WONTFIX', 'SLOW']
|
| +# <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC]
|
| +PLATFORM_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC']
|
| +# <config> ::== RELEASE | DEBUG
|
| +CONFIG_NAMES = ['RELEASE', 'DEBUG']
|
| +# <EXPECTATION_NAMES> ::== \
|
| +# [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT]
|
| +EXPECTATION_NAMES = ['FAIL', 'PASS', 'CRASH',
|
| + 'TIMEOUT', 'IMAGE', 'TEXT',
|
| + 'IMAGE+TEXT']
|
| +ALL_TE_KEYWORDS = (DECISION_NAMES + PLATFORM_NAMES + CONFIG_NAMES +
|
| + EXPECTATION_NAMES)
|
| +
|
| +
|
| +class TestExpectations(object):
|
| + """A class to model the content of test expectation file for analysis.
|
| +
|
| + The raw test expectation file can be found in
|
| + |DEFAULT_TEST_EXPECTATION_LOCATION|.
|
| + It is necessary to parse this file and store meaningful information for
|
| + the analysis (joining with existing layout tests using a test name).
|
| + Instance variable |all_test_expectation_info| is used.
|
| + A test name such as 'media/video-source-type.html' is used for the key
|
| + to store information. However, a test name can appear multiple times in
|
| + the test expectation file. So, the map should keep all the occurrence
|
| + information. For example, the current test expectation file has the following
|
| + two entries:
|
| + BUGWK58587 LINUX DEBUG GPU : media/video-zoom.html = IMAGE
|
| + BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE
|
| + In this case, all_test_expectation_info['media/video-zoom.html'] will have
|
| + a list with two elements, each of which is the map of the test expectation
|
| + information.
|
| + """
|
| +
|
| + def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION):
|
| + """Read the test expectation file from the specified URL and parse it.
|
| +
|
| + All parsed information is stored into instance variable
|
| + |all_test_expectation_info|, which is a dictionary mapping a string test
|
| + name to a list of dictionaries containing test expectation entry
|
| + information. An example of such dictionary:
|
| + {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....},
|
| + {'MAC': True, 'GPU': True ....}]
|
| + which is produced from the lines:
|
| + BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE
|
| + BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE
|
| +
|
| + Args:
|
| + url: A URL string for the test expectation file.
|
| +
|
| + Raises:
|
| + NameError when the test expectation file cannot be retrieved from |url|.
|
| """
|
| -
|
| - DEFAULT_TEST_EXPECTATION_DIR = (
|
| - 'http://svn.webkit.org/repository/webkit/trunk/'
|
| - 'LayoutTests/platform/chromium/')
|
| -
|
| - DEFAULT_TEST_EXPECTATION_LOCATION = (
|
| - 'http://svn.webkit.org/repository/webkit/trunk/'
|
| - 'LayoutTests/platform/chromium/test_expectations.txt')
|
| -
|
| - CHROME_BUG_URL = 'http://code.google.com/p/chromium/issues/detail?id='
|
| -
|
| - WEBKIT_BUG_URL = 'https://bugs.webkit.org/show_bug.cgi?id='
|
| -
|
| - FLAKINESS_DASHBOARD_LINK = (
|
| - 'http://test-results.appspot.com/dashboards/'
|
| - 'flakiness_dashboard.html#tests=')
|
| -
|
| - TEST_CASE_PATTERNS = TestCasePatterns()
|
| -
|
| - OTHER_FIELD_NAMES = ['TestCase', 'Media', 'Flaky', 'WebKitD', 'Bug',
|
| - 'Test']
|
| -
|
| - # The following is from test expectation syntax.
|
| - # BUG[0-9]+ [SKIP] [WONTFIX] [SLOW]
|
| - DECISION_COLUMN_NAMES = ['SKIP', 'SLOW']
|
| - # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC]
|
| - PLATFORM_COLUMN_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC']
|
| - #<config> ::== RELEASE | DEBUG
|
| - CONFIG_COLUMN_NAMES = ['RELEASE', 'DEBUG']
|
| - # <EXPECTATION_COLUMN_NAMES> ::== \
|
| - # [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT]
|
| - EXPECTATION_COLUMN_NAMES = ['FAIL', 'PASS', 'CRASH',
|
| - 'TIMEOUT', 'IMAGE', 'TEXT',
|
| - 'IMAGE+TEXT']
|
| -
|
| - # These are coming from metadata in comments in the test expectation file.
|
| - COMMENT_COLUMN_NAMES = ['UNIMPLEMENTED', 'KNOWNISSUE', 'TESTISSUE',
|
| - 'WONTFIX']
|
| -
|
| - RAW_COMMENT_COLUMN_NAME = 'COMMENTS'
|
| -
|
| - def __init__(self):
|
| - """Initialize the test cases."""
|
| - self.testcases = []
|
| -
|
| - def get_column_indexes(self, column_names):
|
| - """Get column indexes for given column names.
|
| -
|
| - Args:
|
| - column_names: a list of column names.
|
| -
|
| - Returns:
|
| - a list of indexes for the given column names.
|
| - """
|
| - all_field_names = self.get_all_column_names(True, True)
|
| - return [all_field_names.index(
|
| - field_name) for field_name in column_names]
|
| -
|
| - def get_all_column_names(self, include_other_fields, include_comments):
|
| - """Get all column names that are used in CSV file.
|
| -
|
| - Args:
|
| - include_other_fields: a boolean that indicates the result should
|
| - include other column names (defined in OTHER_COLUMNS_NAMES).
|
| - include_comments: a boolean that indicates the result should
|
| - include comment-related names.
|
| -
|
| - Returns:
|
| - a list that contains column names.
|
| - """
|
| - return_list = (
|
| - self.DECISION_COLUMN_NAMES + self.PLATFORM_COLUMN_NAMES +
|
| - self.CONFIG_COLUMN_NAMES + self.EXPECTATION_COLUMN_NAMES)
|
| - if include_other_fields:
|
| - return_list = self.OTHER_FIELD_NAMES + return_list
|
| - if include_comments:
|
| - return_list.extend(self.COMMENT_COLUMN_NAMES)
|
| - return_list.append(self.RAW_COMMENT_COLUMN_NAME)
|
| - return return_list
|
| -
|
| - def get_test_case_element(self, test_case, column_names):
|
| - """Get test case elements.
|
| -
|
| - A test case is a collection of test case elements.
|
| -
|
| - Args:
|
| - test_case: test case data that contains all test case elements.
|
| - column_names: column names for specifying test case elements.
|
| -
|
| - Returns:
|
| - A list of test case elements for the given column names.
|
| - """
|
| - field_indexes = self.get_column_indexes(column_names)
|
| - test_case = self.get_test_case_by_name(test_case)
|
| - if test_case is None:
|
| - return []
|
| - return [test_case[fid] for fid in field_indexes]
|
| -
|
| - def get_test_case_by_name(self, target):
|
| - """Get test case object by test case name.
|
| -
|
| - Args:
|
| - target: test case name.
|
| -
|
| - Returns:
|
| - A test case with the test case name or None if the test case
|
| - cannot be found.
|
| - """
|
| - for testcase in self.testcases:
|
| - # Test case name is stored in the first column.
|
| - if testcase[0] == target:
|
| - return testcase
|
| - return None
|
| -
|
| - def get_all_test_case_names(self):
|
| - """Get all test case names.
|
| -
|
| - Returns:
|
| - A list of test case names.
|
| - """
|
| - return [testcase[0] for testcase in self.testcases]
|
| -
|
| - def generate_link_for_bug(self, bug):
|
| - """Generate link for a bug.
|
| -
|
| - Parse the bug description accordingly. The bug description can be like
|
| - the following: BUGWK1234, BUGCR1234, BUGFOO.
|
| -
|
| - Args:
|
| - bug: A string bug description
|
| -
|
| - Returns:
|
| - A string that represents a bug link. Returns an empty
|
| - string if match is not found.
|
| - """
|
| - pattern_for_webkit_bug = 'BUGWK(\d+)'
|
| - match = re.search(pattern_for_webkit_bug, bug)
|
| - if match is not None:
|
| - return self.WEBKIT_BUG_URL + match.group(1)
|
| - pattern_for_chrome_bug = 'BUGCR(\d+)'
|
| - match = re.search(pattern_for_chrome_bug, bug)
|
| - if match is not None:
|
| - return self.CHROME_BUG_URL + match.group(1)
|
| - pattern_for_other_bug = 'BUG(\S+)'
|
| - match = re.search(pattern_for_other_bug, bug)
|
| - if match is not None:
|
| - return 'mailto:%s@chromium.org' % match.group(1).lower()
|
| - return ''
|
| -
|
| - def generate_link_for_dashboard(self, testcase_name, webkit):
|
| - """Generate link for flakiness dashboard.
|
| -
|
| - Args:
|
| - testcase_name: a string test case name.
|
| - webkit: a boolean indicating whether to use the webkit dashboard.
|
| -
|
| - Returns:
|
| - A string link to the flakiness dashboard.
|
| - """
|
| - url = self.FLAKINESS_DASHBOARD_LINK + urllib.quote(testcase_name)
|
| - if webkit:
|
| - url += '&master=webkit.org'
|
| - return url
|
| -
|
| - def parse_line(self, line, previous_comment_info_list, test_case_patterns,
|
| - media_test_cases_only, writer):
|
| - """Parse each line in test_expectations.txt.
|
| -
|
| - The format of each line is as follows:
|
| - BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] [<platform>] [<config>]
|
| - : <url> = <EXPCTATION>
|
| - The example is:
|
| - BUG123 BUG345 MAC : media/hoge.html
|
| - WONTFIX SKIP BUG19635 : media/restore-from-page-cache.html
|
| - = TIMEOUT
|
| -
|
| - Args:
|
| - line: a text for each line in the test expectation file.
|
| - previous_comment_info_list: a list of comments in previous lines.
|
| - test_case_patterns: a string for test case pattern.
|
| - media_test_cases_only: a boolean for media test case only.
|
| - writer: writer for intermediate results.
|
| - """
|
| - bugs = re.findall(r'BUG\w+', line)
|
| - testcases = re.search(r':\s+(\S+.[html|xml|svg|js])', line)
|
| - if testcases is not None:
|
| - testcase = testcases.group(1)
|
| - data = []
|
| - data.append(testcase)
|
| -
|
| - if LayoutTestCaseManager.check_test_case_matches_pattern(
|
| - testcase, test_case_patterns):
|
| - data.append('Y')
|
| - media_test_case = True
|
| - else:
|
| - data.append('N')
|
| - media_test_case = False
|
| -
|
| - dlink = self.generate_link_for_dashboard(testcase, False)
|
| - # Generate dashboard link.
|
| - data.append(
|
| - CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', ''))
|
| - dlink = self.generate_link_for_dashboard(testcase, True)
|
| - # Generate dashboard link.
|
| - data.append(
|
| - CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', ''))
|
| - for bug in bugs:
|
| - blink = self.generate_link_for_bug(bug)
|
| - data.append(
|
| - CsvUtils.generate_hyperlink_in_csv(blink, bug, ''))
|
| - # Fill the gap for test case with less bug/test.
|
| - for i in range(2 - len(bugs)):
|
| - data.append('')
|
| -
|
| - # Fill all data with 'N' (default).
|
| - for field_name in self.get_all_column_names(False, False):
|
| - if field_name in line:
|
| - data.append('Y')
|
| - else:
|
| - data.append('N')
|
| - # Comments will be accumulated in previous comments.
|
| - # This is for multiple-line comment.
|
| - for previous_comment_info in previous_comment_info_list:
|
| - data.append(previous_comment_info)
|
| - # Write only media related test case if specified.
|
| - if (media_test_case and media_test_cases_only
|
| - or (not media_test_cases_only)):
|
| - writer.writerow(data)
|
| - self.testcases.append(data)
|
| -
|
| - def process_comments(self, comments):
|
| - """Process comments in the test expectations file.
|
| -
|
| - Comments may contain special keywords such as UNIMPLEMENTED,
|
| - KNOWNISSUE, TESTISSUE (in TestExpectationsManager.COMMENT_COLUMN_NAMES)
|
| - and may be multiple lines.
|
| -
|
| - Args:
|
| - comments: A raw comment from the test expectation file.
|
| - It is above test case name.
|
| -
|
| - Returns:
|
| - A list of 'Y' or 'N' whether the comments contain
|
| - each column name in TestExpectationsManager.COMMENT_COLUMN_NAMES
|
| - in the comments.
|
| - """
|
| - return_list = []
|
| - for ccn in TestExpectationsManager.COMMENT_COLUMN_NAMES:
|
| - if ccn in comments:
|
| - return_list.append('Y')
|
| - else:
|
| - return_list.append('N')
|
| - return_list.append(comments)
|
| - return return_list
|
| -
|
| - def get_and_save_content(self, location, output):
|
| - """Simply get test expectation from the specified location and save it.
|
| -
|
| - Args:
|
| - location: SVN location of the test expectation file.
|
| - output: an output file path including file name.
|
| - """
|
| - client = pysvn.Client()
|
| - file_object = open(output, 'w')
|
| - file_object.write(client.cat(location))
|
| - file_object.close()
|
| -
|
| - def get_and_save_content_media_only(self, location, output):
|
| - """Simply get test expectation from the specified location.
|
| -
|
| - It also saves it (media only).
|
| -
|
| - Args:
|
| - location: SVN location of the test expectation file.
|
| - output: an output file path including file name.
|
| - """
|
| - file_object = file(location, 'r')
|
| - text_list = list(file_object)
|
| - output_text = ''
|
| - for txt in text_list:
|
| - if txt.startswith('//'):
|
| - output_text += txt
|
| - else:
|
| - for pattern in (
|
| - self.TEST_CASE_PATTERNS.get_test_case_pattern(
|
| - 'media')):
|
| - if re.search(pattern, txt) is not None:
|
| - output_text += txt
|
| - break
|
| - file_object.close()
|
| - # Save output.
|
| - file_object = open(output, 'w')
|
| - file_object.write(output_text)
|
| - file_object.close()
|
| -
|
| - def get_and_parse_content(self, location, output, media_test_case_only):
|
| - """Get test_expectations.txt and parse the content.
|
| -
|
| - The comments are parsed as well since it contains keywords.
|
| -
|
| - Args:
|
| - location: SVN location of the test expectation file.
|
| - output: an output file path including file name.
|
| - media_test_case_only: A boolean indicating whether media
|
| - test cases should only be processed.
|
| - """
|
| - if location.startswith('http'):
|
| - # Get from SVN.
|
| - client = pysvn.Client()
|
| - # Check out the current version of the pysvn project.
|
| - txt = client.cat(location)
|
| - else:
|
| - if location.endswith('.csv'):
|
| - # No parsing in the case of CSV file.
|
| - # Direct reading.
|
| - file_object = file(location, 'r')
|
| - self.testcases = list(csv.reader(file_object))
|
| - file_object.close()
|
| - return
|
| - file_object = open(location, 'r')
|
| - txt = file_object.read()
|
| - file_object.close()
|
| -
|
| - file_object = file(output, 'wb')
|
| - writer = csv.writer(file_object)
|
| - writer.writerow(self.get_all_column_names(True, True))
|
| - lines = txt.split('\n')
|
| - process = False
|
| - previous_comment = ''
|
| - previous_comment_list = []
|
| - for line in lines:
|
| - if line.isspace() or str is '':
|
| - continue
|
| - line.strip()
|
| - if line.startswith('//'):
|
| - # There are comments.
|
| - line = line.replace('//', '')
|
| - if process is True:
|
| - previous_comment = line
|
| - else:
|
| - previous_comment = previous_comment + '\n' + line
|
| - previous_comment_list = self.process_comments(previous_comment)
|
| - process = False
|
| - else:
|
| - self.parse_line(
|
| - line, previous_comment_list,
|
| - self.TEST_CASE_PATTERNS.get_test_case_pattern('media'),
|
| - media_test_case_only, writer)
|
| - process = True
|
| - file_object.close()
|
| -
|
| - def get_te_diff_between_times(self, te_location, start, end, patterns,
|
| - change, checkchange):
|
| - """Get test expectation diff output for a given time period.
|
| -
|
| - Args:
|
| - te_location: SVN location of the test expectation file.
|
| - start: a date object for the start of the time period.
|
| - end: a date object for the end of the time period.
|
| - patterns: test case name patterns of the test cases that
|
| - we are interested in. This could be test case name
|
| - as it is in the case of exact matching.
|
| - change: the number of change in occurrence of test case in
|
| - test expectation.
|
| - checkchange: a boolean to indicate if we want to check the
|
| - change in occurrence as specified in |change| argument.
|
| -
|
| - Returns:
|
| - a list of tuples (old_revision, new_revision,
|
| - diff_line, author, date, commit_message) that matches
|
| - the condition.
|
| - """
|
| - client = pysvn.Client()
|
| - client.checkout(te_location, 'tmp', recurse=False)
|
| - logs = client.log('tmp/test_expectations.txt',
|
| - revision_start=pysvn.Revision(
|
| - pysvn.opt_revision_kind.date, start),
|
| - revision_end=pysvn.Revision(
|
| - pysvn.opt_revision_kind.date, end))
|
| - result_list = []
|
| - for i in xrange(len(logs) - 1):
|
| - # PySVN.log returns logs in reverse chronological order.
|
| - new_rev = logs[i].revision.number
|
| - old_rev = logs[i + 1].revision.number
|
| - # Getting information about new revision.
|
| - author = logs[i].author
|
| - date = logs[i].date
|
| - message = logs[i].message
|
| - text = client.diff('/tmp', 'tmp/test_expectations.txt',
|
| - revision1=pysvn.Revision(
|
| - pysvn.opt_revision_kind.number, old_rev),
|
| - revision2=pysvn.Revision(
|
| - pysvn.opt_revision_kind.number, new_rev))
|
| - lines = text.split('\n')
|
| - for line in lines:
|
| - for pattern in patterns:
|
| - matches = re.findall(pattern, line)
|
| - if matches:
|
| - if checkchange:
|
| - if ((line[0] == '+' and change > 0) or
|
| - (line[0] == '-' and change < 0)):
|
| - result_list.append((old_rev, new_rev, line,
|
| - author, date, message))
|
| - else:
|
| - if line[0] == '+' or line[0] == '-':
|
| - result_list.append((old_rev, new_rev, line,
|
| - author, date, message))
|
| - return result_list
|
| + self.all_test_expectation_info = {}
|
| + resp = urllib2.urlopen(url)
|
| + if resp.code != 200:
|
| + raise NameError('Test expectation file does not exist in %s' % source)
|
| + # Start parsing each line.
|
| + comments = ''
|
| + for line in resp.read().split('\n'):
|
| + if line.startswith('//'):
|
| + # Comments can be multiple lines.
|
| + comments += line.replace('//', '')
|
| + elif not line:
|
| + comments = ''
|
| + else:
|
| + test_expectation_info = self.ParseLine(line, comments)
|
| + testname = TestExpectations.ExtractTestOrDirectoryName(line)
|
| + if not testname in self.all_test_expectation_info:
|
| + self.all_test_expectation_info[testname] = []
|
| + # This is a list for multiple entries.
|
| + self.all_test_expectation_info[testname].append(test_expectation_info)
|
| +
|
| + @staticmethod
|
| + def ExtractTestOrDirectoryName(line):
|
| + """Extract either a test name or a directory name from each line.
|
| +
|
| + Please note the name in the test expectation entry can be test name or
|
| + directory: Such examples are:
|
| + BUGWK43668 SKIP : media/track/ = TIMEOUT
|
| +
|
| + Args:
|
| + line: a line in the test expectation file.
|
| +
|
| + Returns:
|
| + a test name or directory name string. Returns '' if no match.
|
| +
|
| + Raises:
|
| + ValueError when there is no test name match.
|
| + """
|
| + # First try to find test name ending with .html.
|
| + matches = re.search(r':\s+(\S+.html)', line)
|
| + # Next try to find directory name.
|
| + if matches:
|
| + return matches.group(1)
|
| + matches = re.search(r':\s+(\S+)', line)
|
| + if matches:
|
| + return matches.group(1)
|
| + else:
|
| + raise ValueError('test or dictionary name cannot be found in the line')
|
| +
|
| + @staticmethod
|
| + def ParseLine(line, comment_prefix):
|
| + """Parse each line in test expectation and update test expectation info.
|
| +
|
| + This function checks for each entry from |ALL_TE_KEYWORDS| in the current
|
| + line and stores it in the test expectation info map if found. Comment
|
| + and bug information is also stored in the map.
|
| +
|
| + Args:
|
| + line: a line in the test expectation file. For example,
|
| + "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE"
|
| + comment_prefix: comments from the test expectation file occurring just
|
| + before the current line being parsed.
|
| +
|
| + Returns:
|
| + a dictionary containing test expectation info, including comment and bug
|
| + info.
|
| + """
|
| + test_expectation_info = {}
|
| + # Store comments.
|
| + inline_comments = ''
|
| + if '//' in line:
|
| + inline_comments = line[line.rindex('//') + 2:]
|
| + # Remove the inline comments to avoid the case where keywords are in
|
| + # inline comments.
|
| + line = line[0:line.rindex('//')]
|
| + for name in ALL_TE_KEYWORDS:
|
| + if name in line:
|
| + test_expectation_info[name] = True
|
| + test_expectation_info['Comments'] = comment_prefix + inline_comments
|
| + # Store bug informations.
|
| + bugs = re.findall(r'BUG\w+', line)
|
| + if bugs:
|
| + test_expectation_info['Bugs'] = bugs
|
| + return test_expectation_info
|
|
|