blob: 60aa600f8fe387d415356f5e725e097d637043e2 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Classes for efficient data retrieval for dash utilities.
To see live data for these data structures best, run test_dash_view.py and
review its output. Output is produced by ShowDataModel() and ShowKeyVals().
Includes: class CrashDashView(object)
class AutotestDashView(object)
class SummaryRanges(object)
"""
import datetime
import itertools
import logging
import os
import re
import dash_util
import test_summary
settings = "autotest_lib.frontend.settings"
os.environ["DJANGO_SETTINGS_MODULE"] = settings
# For db access.
from autotest_lib.frontend.afe import readonly_connection
# String resources.
from dash_strings import AUTOTEST_USER
from dash_strings import JOB_RESULT_DIR
from dash_strings import KERNELTEST_TAG
from dash_strings import LAST_N_JOBS_LIMIT
from dash_strings import LOCAL_TMP_DIR
from dash_strings import UNKNOWN_TIME_STR
GTEST_SUFFIXES = ["audio", "browsertests", "enterprise", "pagecycler", "pyauto",
"pyauto_basic", "pyauto_perf", "sync", "video"]
SUFFIXES_TO_SHOW = ["bvt", "flaky", "hwqual", "regression",
KERNELTEST_TAG] + GTEST_SUFFIXES
SERVER_JOB = "SERVER_JOB"
LEGACY_PLATFORM_PREFIXES = ("netbook_", "desktop_")
class CrashDashView(object):
"""View used to show crash information in summary and details views.
An important reason we separate this class from AutotestDashView is that
individual test details in AutotestDashView are frequently aliased into
multiple categories. To dedup crash results we use this separate structure.
"""
def __init__(self, dash_base_dir):
"""Initialize grouping containers used to retrieve crashes.
Crashes are summarized per build for entire platforms, categories and
test names. Data structures need to support retrieval of both detailed
crash strings and counts of crashes discovered.
The _crashes data structure is indexed as follows:
+netbooks (netbook_HP_INDIANA, netbook_DELL_L13, ...)
|+boards (x86-generic-bin, arm-generic-rel, ...)
|+build
|+test_name
|-categories
|-crash strings
Args:
dash_base_dir: root of the cache directory for crash results.
"""
job_cache_dir = os.path.join(dash_base_dir, LOCAL_TMP_DIR, JOB_RESULT_DIR)
dash_util.MakeChmodDirs(job_cache_dir)
dash_util.PruneOldDirsFiles(job_cache_dir)
self._test_summaries = test_summary.TestSummaryInfo(job_cache_dir)
self._crashes = {}
def _LookupCrashDict(self, netbook, board, build, test_name=None):
"""Retrieve a leaf level (test_name) or one-up (build) of the crash tree.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@param test_name: test_name of Autotest test.
@return Leaf level tuple of the category list and a dictionary for crash
string details indexed on result instance idx. If no test_name is
supplied then the dict will contain crash results for all tests
executed in that build (and the category list will be None).
"""
netbook_dict = self._crashes.setdefault(netbook, {})
board_dict = netbook_dict.setdefault(board, {})
build_dict = board_dict.setdefault(build, {})
if test_name:
return build_dict.setdefault(test_name, (set(), {}))
return None, build_dict
def AddToCrashTree(self, netbook, board, build, test_name, result_idx,
job_tag):
"""Update the crash strings container from the results summary file.
This crash strings container is optimized to support the two views
that consume it: the waterfall summary view (platform x build) and the
details view (platform x build x test_name).
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full, x86-mario-full-chromeos,
...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@param test_name: test_name of Autotest test.
@param result_idx: unique identifier for a test result instance.
@param job_tag: path base for finding test result file under CAutotest
results.
"""
crash_strings = self._LookupCrashDict(netbook, board, build, test_name)[1]
if result_idx in crash_strings:
# The same test result can be attempted for entry because we alias
# some test results under multiple categories.
return
job_crashes = self._test_summaries.RetrieveTestSummary(job_tag, test_name)
if job_crashes and job_crashes.get('crashes'):
crash_strings[result_idx] = job_crashes['crashes']
def AddCrashCategory(self, netbook, board, build, test_name, category):
"""Keep a list of the categories assigned to this test_name.
Used to hyperlink from a crash summary to it's related details page.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full, x86-mario-full-chromeos,
...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@param test_name: test_name of Autotest test.
@param category: test category (test prefix or job suffix usually).
"""
categories = self._LookupCrashDict(netbook, board, build, test_name)[0]
categories.add(category)
def _CollapseCrashes(self, crash_strings):
"""Helper to change 'chrome sig 11' 'chrome sig 11' to '(2) chrome sig 11'
This is needed to tighten up the popups when large crash quantities
are encountered.
"""
counted = {}
for c in crash_strings:
counted[c] = counted.setdefault(c, 0) + 1
collapsed = []
for c, v in counted.iteritems():
collapsed.append('(%s) %s' % (v, c))
return sorted(collapsed)
def GetBuildCrashSummary(self, netbook, board, build, category=None):
"""Used to populate the waterfall summary page with a crash count.
The cells on the waterfall summary page reflect all crashes found from
all tests in all categories for a given platform/build combination. The
cells on a category summary (kernel) page reflect crashes found only in a
specific category for a given platform/build combination.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full, x86-mario-full-chromeos,
...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@param category: test category (test prefix or job suffix usually), None
for all.
@return Tuple used in watefall summary views of: crash_details list, a
count of crashes and the first category for a hyperlink. The result
tuple is ([], 0, None) if no crashes were discovered.
"""
crashes = []
n = 0
all_categories = set()
build_crashes = self._LookupCrashDict(netbook, board, build)[1]
if build_crashes:
for test_name in sorted(build_crashes):
categories, crash_dict = build_crashes[test_name]
if (not category) or (category and category in categories):
new_crashes = sorted(list(itertools.chain(*crash_dict.values())))
if new_crashes:
crashes.append((test_name, self._CollapseCrashes(new_crashes)))
n += len(new_crashes)
all_categories |= categories
if not crashes:
return crashes, n, None
return crashes, n, sorted(all_categories)[0]
def GetBuildTestCrashSummary(self, netbook, board, build, test_name):
"""Used to populate the test details pages with crash counts per test.
The cells on each category details page reflect crashes found only in a
specific test for a given platform/build combination.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full, x86-mario-full-chromeos,
...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@param test_name: name of a specific test.
@return Tuple used in details views: list of crash details and a count of
crashes.
"""
test_crashes = self._LookupCrashDict(netbook, board, build, test_name)[1]
if not test_crashes:
return [], 0
new_crashes = sorted(list(itertools.chain(*test_crashes.values())))
return self._CollapseCrashes(new_crashes), len(new_crashes)
def GetTestSummaries(self):
"""Test Summaries are used to probe the crash cache for crashes in a job.
Used by the test result summary emailer to include crashes and a link to
each job with a crash for research.
@return The TestSummaryInfo object that is shared.
"""
return self._test_summaries
class AutotestDashView(object):
"""View used by table_gen, plot_gen and dash_email."""
class __impl:
"""Nested class implements code wrapped by singleton."""
def __init__(self):
"""Setup common data structures for the models.
Uses dashboard cache files for some (crash/timing) data.
"""
self._dash_config = None
self._cursor = readonly_connection.connection().cursor()
self._common_where = (
"WHERE job_owner = %s"
" AND NOT ISNULL(test_finished_time)"
" AND NOT ISNULL(job_finished_time)"
" AND NOT test_name LIKE 'CLIENT_JOB%%'"
" AND NOT test_name LIKE 'boot.%%'"
" AND NOT test_name IN ('Autotest.install', 'cleanup_test', "
" 'lmbench', 'logfile.monitor', 'repair', "
" 'sleeptest', 'tsc')")
# Used in expression parsing - have slightly different captures.
# New test suite job regex.
# (x86-zgb)-release/((R19)-1913.0.0-a1-b1539) \
# /(bvt)/(network_DisableInterface)
self._jobname_testsuite_parse = re.compile(
'(.*?)-release/((R\d+)-.*?)/(.*?)/(.*)')
# (x86-alex-r18)-(R18-1660.71.0-a1-b75)_(bvt)
# (x86-generic-full)-(R20-2112.0.0-a1-b2686)_(browsertests)
self._jobname_parse = re.compile(
'([\w-]+-[fr][\w]+)-(.*[.-][\d]+\.[\d]+\.[\d]+)(?:-[ar][\w]+-b[\d]+)?'
'_([\w_]*)')
# (R18-1660.71.0)-a1-b75
# r18-1660.122.0_to_(R18-1660.122.0)-a1-b146
self._subjob_parse = re.compile(
'.*(R[\d]+-[\d]+\.[\d]+\.[\d]+)')
self._board_parse = re.compile(
'(x86|tegra2)-(.+)-(r[\d]+)')
# (R19-1913.0.0)
# (R19-1913.0.0)-a1-b1539
# (0.8.73.0)-r3ed8d12f-b719.
self._shortbuild1_parse = re.compile('(R[\d]+-[\d]+\.[\d]+\.[\d]+)')
self._shortbuild2_parse = re.compile('([\d]+\.[\d]+\.[\d]+\.[\d]+)')
self._release_parse = re.compile('r[\d]')
# Test creation info (author, path).
# Populated by QueryAutotests().
self._autotests = {}
self.TEST_TREE_DOC = """
The test_tree is a dictionary of:
+netbooks (netbook_HP_INDIANA, netbook_DELL_L13, ...)
|+boards (x86-generic-bin, arm-generic-rel, ...)
|+categories (platform, desktopui, bvt, regression, ...)
|+test_name (platform_BootPerfServer, ...)
|+builds (0.8.67.0-re7c459dc-b1135)
|+indices [test_idx]
This is our lookup index into tests.
Populate netbooks by QueryNetbooks() and the rest by QueryTests().
"""
self._test_tree = {}
self.UI_CATEGORIES_DOC = """
Many categories will not show on the dash but are available
for use by emailer so must remain in the data model.
This will be a subset of upper levels of the test_tree.
+netbooks (netbook_HP_INDIANA, netbook_DELL_L13, ...)
|+boards (x86-generic-bin, arm-generic-rel, ...)
|+categories (platform, desktopui, bvt, regression, ...)
Populated by QueryTests().
"""
self._ui_categories = {}
# Short subset of job_id's.
# Populated by QueryBuilds().
self._job_ids = set()
self.BUILDS_DOC = """
A little tree to track the builds for each of the boards.
+board
|-dictionary mapping short to long for lookups
Populated by QueryBuilds().
"""
self._builds = {}
self.BUILD_TREE_DOC = """
Need a tree of builds to show which builds were actually
run for each netbook, board.
+netbooks (netbook_HP_INDIANA, netbook_DELL_L13, ...)
|+boards (x86-generic-bin, arm-generic-rel, ...)
|+categories
|+build
|+aggregate build info
|-latest job_id "job_id"
|-earliest job started time "start"
|-last job finished time "finish"
|-number of 'GOOD' test names "ngood"
|-number of total test names "ntotal"
Used in netbook->board->category views.
Populate netbooks by QueryNetbooks() and the rest by QueryTests().
"""
self._build_tree = {}
self.TESTS_DOC = """
The test list is a dictionary of:
+test_idx
|+-test_name.
-tag.
-hostname.
-status.
-start (job_started_time)
-finish (job_finished_time)
-attr
-experimental (optional boolean)
The actual test data. Used to fill popups and test status.
Populated by QueryTests().
"""
self._tests = {}
self.CRASHES_DOC = """
The crashes object is a container of crashes that may be
filtered by build, category and test_name.
"""
self._crashes = None
self.PERF_KEYVALS_DOC = """
For performance counters.
+netbooks
|+boards
|+test_name
|+key
|+build
|+(value_list, test_idx_list, iteration_list)
Used in plotting.
Populated by QueryKeyVals().
"""
self._perf_keyvals = {}
# Constant for date comparisons
self._null_datetime = datetime.datetime(2010, 1, 1)
self._null_timedelta = datetime.timedelta(0)
# Performance optimization
self._formatted_time_cache = {}
self._last_updated = datetime.datetime.ctime(datetime.datetime.now())
def CrashSetup(self, dash_base_dir):
"""Set up the crash view.
@param dash_base_dir: root of the cache directory for crash results.
"""
self._crashes = CrashDashView(dash_base_dir)
def GetCrashes(self):
"""Accessor for crash data and functions."""
return self._crashes
def SetDashConfig(self, dash_config):
"""Some preprocessing of dash_config.
@param dash_config: dictionary of dash config entries.
"""
self._dash_config = dash_config
if 'customboardfilter' in dash_config:
self._board_parse = re.compile(
'(%s)-(.+)-(r[\d]+)' % dash_config['customboardfilter'])
def GetAutotestInfo(self, name):
"""Return author and path of an autotest test.
@param name: Autotest test_name.
@return 2-Tuple of (author_name, test_path) used to locate test code.
"""
name = name.split(".")[0]
author = ""
test_path = ""
server_test_name = name + "Server"
if name in self._autotests:
author, test_path = self._autotests[name]
elif server_test_name in self._autotests:
author, test_path = self._autotests[server_test_name]
if test_path:
# convert server/tests/netpipe/control.srv --> server/tests/netpipe
test_path = os.path.dirname(test_path)
return author, test_path
@property
def netbooks(self):
"""Return a list of known netbooks - some may have not run tests.
@return Unsorted List of all known netbooks (with netbook_ prefix). Some
of these may have no tests run against them.
"""
return self._test_tree.keys()
def GetNetbooksWithBoardType(self, board):
"""Return list of netbooks with tests run under board.
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@return Sorted List of netbooks (with netbook_ prefix) that have
completed tests associated with the given board.
"""
netbooks = self._test_tree.keys()
netbooks.sort()
return [n for n in netbooks if board in self._test_tree[n]]
def GetNetbooksWithBoardTypeCategory(self, board, category):
"""Return list of netbooks with tests under board and category.
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@return Sorted List of netbooks (with netbook_ prefix) that have
completed tests of given category with the given board.
"""
netbooks = self._build_tree.keys()
netbooks.sort()
return [n for n in netbooks if (
board in self._build_tree[n] and
category in self._build_tree[n][board])]
def GetBoardTypes(self):
"""Return list of boards found.
@return Unsorted List of all known boards: x86-generic-full,
x86-mario-full-chromeos, ...
"""
return self._builds.keys()
def GetNetbookBoardTypes(self, netbook):
"""Return list of boards used in the given netbook.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@return Unsorted List of boards which have completed tests on the
given netbook (with netbook_ prefix).
"""
if netbook in self._build_tree:
return self._build_tree[netbook].keys()
return []
def GetAllBuilds(self):
"""Return list of all known builds that we used.
@return Unsorted Set of unique builds across all boards.
"""
results = set()
for build_dict in self._builds.itervalues():
for b in build_dict.itervalues():
results.add(b)
return results
def GetBoardtypeBuilds(
self, board, limit=LAST_N_JOBS_LIMIT, asc=False):
"""Return list of builds with tests run in the given board.
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param limit: common to truncate the build list for display.
@param asc: if False, sort descending (tables) else ascending (plots).
@return Sorted List of builds from with attempted jobs. These builds may
NOT have associated test results if no tests completed on a netbook.
"""
results = sorted(
self._builds[board].values(),
cmp=dash_util.BuildNumberCmp,
reverse=asc)
build_count = min(len(results), limit)
if asc:
return results[len(results)-build_count:]
else:
return results[:build_count]
def GetBuilds(self, netbook, board, category):
"""Return list of builds with tests run in the given netbook.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@return Sorted List of builds with jobs attempted on the given netbook,
board combination with tests attempted in the given category.
Again, tests may not have been completed thus there may be no
corresponding test results.
"""
results = []
if not netbook in self._build_tree:
return results
if (board in self._build_tree[netbook] and
category in self._build_tree[netbook][board]):
for b in self._build_tree[netbook][board][category].iterkeys():
if not b in self._builds[board]:
logging.warn(
"***DATA WARNING: %s not in build list for %s, %s, %s!",
b, netbook, board, category)
else:
results.append(self._builds[board][b])
results.sort(dash_util.BuildNumberCmp)
return results
def GetUICategories(self, netbook, board):
"""Return categories for DASH UI of tests run in netbook - board.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@return Unsorted List of the UI categories (bvt, desktopui, ...) of tests
with completed results run against the given netbook and board.
"""
return list(self._ui_categories[netbook][board])
def GetCategories(self, netbook, board, regex=None):
"""Return categories of tests run in netbook - board.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param regex: optional match filter for categories.
@return Unsorted List of the categories (bvt, desktopui, ...) of tests
with completed results run against the given netbook and board.
"""
if netbook in self._test_tree and board in self._test_tree[netbook]:
if not regex:
return self._test_tree[netbook][board].keys()
return [c for c in self._test_tree[netbook][board].keys()
if re.match(regex, c)]
else:
return []
def GetTestNames(self, netbook, board, category):
"""Return unique test names run in netbook - board - category.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@return Unsorted or empty List of test names for building a table listing
all tests in the given category with completed results on the given
netbook and board.
"""
if category not in self._test_tree[netbook][board]:
return []
return self._test_tree[netbook][board][category].keys()
def GetTestNamesInBuild(self, netbook, board, category, build, regex=None):
"""Return the unique test names like GetTestNames() but for 1 build.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a full build string: 0.8.73.0.
@param regex: optional match filter for test name.
@return Sorted or empty List of the test names in the given category and
given build with completed test results against the given netbook
and board.
"""
results = []
try:
for t, b in self._test_tree[netbook][board][category].iteritems():
if build in b:
if regex and not re.match(regex, t):
continue
results.append(t)
results.sort()
except KeyError:
logging.debug("***KeyError: %s, %s, %s.", netbook, board, category)
return results
def GetCategorySummary(self, netbook, board, category, build):
"""Return ngood and ntotal for the given job.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a full build string: 0.8.73.0.
@return 4-Tuple:
-Boolean: True if the job was attempted
-Boolean: True if all server jobs GOOD
-Integer: number of tests completed GOOD
-Integer: number of tests completed
"""
ngood = 0
ntotal = 0
xngood = 0
xntotal = 0
job_attempted = False
job_good = False
if build in self._build_tree[netbook][board][category]:
job = self._build_tree[netbook][board][category][build]
ngood = job["ngood"]
ntotal = job["ntotal"]
xngood = job["xngood"]
xntotal = job["xntotal"]
job_good = job["server_good"]
job_attempted = True
return job_attempted, job_good, ngood, ntotal, xngood, xntotal
def TestDetailIterator(self, netbook, board, category, build):
"""Common iterator for looking through test details.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@return Iterative test details using the Python generator (yield)
mechanism.
"""
tests = self.GetTestNamesInBuild(netbook, board, category, build)
if not tests:
return
for test in tests:
test_details = self.GetTestDetails(netbook, board, category, test,
build)
if not test_details:
continue
for t in test_details:
yield t
def GetCategoryKernel(self, netbook, board, category, build):
"""Return string name of the kernel version like: 2.6.38.3+.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@return String name of the kernel tested. If multiple kernels tested emit
the one most used with a marker string.
"""
kernel_votes = dash_util.SimpleCounter()
for t in self.TestDetailIterator(netbook, board, category, build):
kernel_votes.Push(t['attr'].get('sysinfo-uname', None))
return kernel_votes.MaxKey()
def GetCategoryFailedTests(self, netbook, board, category, build):
"""Return list of failed tests for easy popup display.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a full build string: 0.8.73.0-r3ed8d12f-b719.
@return Tuple including a List of unique test names of failed tests,
and a List of unique test names of experimental failed tests.
"""
failed_tests = set()
xfailed_tests = set()
for t in self.TestDetailIterator(netbook, board, category, build):
if t['status'] != 'GOOD':
if t.get('experimental'):
xfailed_tests.add(t['test_name'])
else:
failed_tests.add(t['test_name'])
return (', '.join(sorted(failed_tests)),
', '.join(sorted(xfailed_tests)))
def GetJobTimes(self, netbook, board, category, build):
"""Return job_start_time, job_end_time and elapsed for the given job.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a build on this netbook and board.
@return 3-Tuple of datetime.datetime,datetime.datetime,datetime.timedelta
for started_datetime, finished_datetime, elapsed_datetime. All are
calculated across multiple jobs by looking at completed test results
and choosing the earliest start time and the latest finish time.
"""
job_started = self._null_datetime
job_finished = self._null_datetime
job_elapsed = self._null_timedelta
if build in self._build_tree[netbook][board][category]:
job = self._build_tree[netbook][board][category][build]
job_started = job["start"]
job_finished = job["finish"]
job_elapsed = job_finished - job_started
return job_started, job_finished, job_elapsed
def GetJobTimesNone(self, netbook, board, category, build):
"""Translate null_datetime from GetJobTimes() to None.
@param netbook: same as for GetJobTimes() above.
@param board: same as for GetJobTimes() above.
@param category: same as for GetJobTimes() above.
@param build: same as for GetJobTimes() above.
@return Same as for GetJobTimes() above, except with null datetimes
translated to None.
"""
job_started, job_finished, job_elapsed = self.GetJobTimes(
netbook, board, category, build)
if job_started == self._null_datetime:
job_started = None
if job_finished == self._null_datetime:
job_finished = None
if job_elapsed == self._null_datetime:
job_elapsed = None
return job_started, job_finished, job_elapsed
def GetFormattedJobTimes(self, netbook, board, category, build):
"""Return job_start_time, job_end_time and elapsed in datetime format.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param build: a build on this netbook and board.
@return 3-Tuple of stringified started_datetime, finished_datetime, and
elapsed_datetime. Returns a common string when invalid or no datetime
was found.
"""
time_key = (netbook, board, category, build)
if time_key in self._formatted_time_cache:
return self._formatted_time_cache[time_key]
job_started_str = UNKNOWN_TIME_STR
job_finished_str = UNKNOWN_TIME_STR
job_elapsed_str = UNKNOWN_TIME_STR
job_started, job_finished, job_elapsed = self.GetJobTimes(*time_key)
if job_started != self._null_datetime:
job_started_str = datetime.datetime.ctime(job_started)
if job_finished != self._null_datetime:
job_finished_str = datetime.datetime.ctime(job_finished)
if job_elapsed != self._null_timedelta:
job_elapsed_str = str(job_elapsed)
result = (job_started_str, job_finished_str, job_elapsed_str)
self._formatted_time_cache[time_key] = result
return result
def GetFormattedLastUpdated(self):
"""Return a string used to date-time stamp our reports."""
return self._last_updated
def GetTestDetails(self, netbook, board, category, test_name, build):
"""Return tests details for a given test_name x build cell.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param category: a test group: bvt, regression, desktopui, graphics, ...
@param test_name: test_name of Autotest test.
@param build: a build on this netbook and board.
@return Sorted or empty List of multiple test dictionaries for test
instances in the given category that completed on the given netbook
and board in the given build. The test dictionaries include common
fields 'test_name', 'tag', 'hostname', 'status', experimental
(optional) and an embedded dictionary of varying attributes under
'attr'.
"""
test_details = []
if build in self._test_tree[netbook][board][category][test_name]:
test_index_list = (list(
self._test_tree[netbook][board][category][test_name][build][0]))
test_index_list.sort(reverse=True)
for i in test_index_list:
test_details.append(self._tests[i])
return test_details
def GetTestFromIdx(self, idx):
"""Returns all details about 1 specific instance of 1 test result.
@param idx: unique index of the test result.
@return A Dictionary with attributes for a test result instance including
tag.
"""
return self._tests[str(idx)]
def GetPlatformKeyValTests(self, netbook, board):
"""Return list of tests that have keyvals for a given netbook and board.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@return None or a sorted list of the test names with keyvals.
"""
if (not netbook in self._perf_keyvals or
not board in self._perf_keyvals[netbook]):
return []
return sorted(self._perf_keyvals[netbook][board].keys())
def GetTestKeys(self, netbook, board, test_name):
"""Return list of test keys with values for a given netbook and board.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param test_name: test_name of Autotest test.
@return None or a sorted list of the test keys with keyvals.
"""
if (not netbook in self._perf_keyvals or
not board in self._perf_keyvals[netbook] or
not test_name in self._perf_keyvals[netbook][board]):
return None
return sorted(self._perf_keyvals[netbook][board][test_name].keys())
def GetTestKeyVals(self, netbook, board, test_name):
"""Return keyvals for one test over our queried jobs/builds.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param test_name: test_name of Autotest test.
@return None or a dictionary of the performance key-values recorded during
a completed test with the given netbook, board, test_name. The
keyvals are from the overall set of jobs/builds that were discovered
when querying the last n jobs/builds. The dictionary has keys of
each performance key recorded and build dictionary. The build
dictionary has keys of each build with the named performance key
recorded and a value of the value list. The value list is a 2-Tuple
of Lists. One is a list of the perf values and the other is a list
of corresponding test_idx that may be used to look up job/test
details from the point in a graphed plot.
"""
if (not netbook in self._perf_keyvals or
not board in self._perf_keyvals[netbook] or
not test_name in self._perf_keyvals[netbook][board]):
return None
return self._perf_keyvals[netbook][board][test_name]
def GetTestPerfVals(self, netbook, board, test_name, key):
"""Return values for one test/key over our queried jobs/builds.
@param netbook: one of our netbooks with the netbook_ prefix:
netbook_DELL_L13, netbook_ANDRETTI, ...
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@param test_name: test_name of Autotest test.
@param key: autotest perf key.
@return None or a dictionary of the performance values recorded during
a completed test with the given netbook, board, test_name, key. The
vals are from the overall set of jobs/builds that were discovered
when querying the last n jobs/builds. The dictionary has keys of
each build with the named performance key recorded and a value of
the value list. The value list is a 2-Tuple of Lists. One is a
list of the perf values and the other is a list of corresponding
test_idx that may be used to look up job/test details from the
point in a graphed plot.
"""
keyvals = self.GetTestKeyVals(netbook, board, test_name)
if keyvals and key in keyvals:
return keyvals[key]
return None
def ParseBoard(self, board):
"""Return simple board without release identifier: e.g. x86-mario.
Examples:
stumpy-r16
tegra2-kaen-r16
tegra2-seaboard
tegra2-seaboard-rc
x86-alex-r16
x86-generic-full
x86-mario-r15
@param board: one of our boards: x86-generic-full,
x86-mario-full-chromeos, ...
@return (simple_board, release_if_found).
"""
m = re.match(self._board_parse, board)
if m and m.lastindex == 3:
parsed_board = '%s-%s' % (m.group(1), m.group(2))
release = m.group(3)
else:
split_board = board.split('-')
found = False
parsed_board = []
for i in xrange(len(split_board)):
if re.match(self._release_parse, split_board[i]):
found = True
break
parsed_board.append(split_board[i])
parsed_board = '-'.join(parsed_board)
if found:
release = split_board[i]
else:
release = None
return parsed_board, release
def ParseTestName(self, test_name):
"""Return category of test_name or a general category.
A test_name defines a category if it has a prefix.
@param test_name: test_name from autotest db.
@return Single token test category.
"""
if test_name.find(".") > 0:
test_name = test_name.split(".")[0]
if test_name.find("_") > 0:
category = test_name.split("_")[0]
else:
category = "autotest"
return category
def ParseJobName(self, job_name):
"""Return board - build# and job_suffix from the job_name.
@param job_name: complex string created by test_scheduler from a build
image.
@return Tuple of: board, a build#, a job group, and a bool True if
experimental.
"""
# (x86-zgb)-release/((R19)-1913.0.0-a1-b1539) \
# /(bvt)/(network_DisableInterface)
match = self._jobname_testsuite_parse.match(job_name)
if match:
board, build, milestone, suite, suffix = match.groups()
# Put board in the format expected, e.g. x86-mario-r19.
board = '%s-%s' % (board, milestone.lower())
# Remove possible sequence artifacts, e.g.'-a1-b1539'
build = self.ParseSimpleBuild(build)
return (board, build, self.TranslateSuffix(suite),
suffix.startswith('experimental_'))
# Old suite style parsing.
m = re.match(self._jobname_parse, job_name)
if not m or not len(m.groups()) == 3:
return None, None, None, None
board, subjob, suffix = m.group(1, 2, 3)
# Subjob handles multi-build au test job names.
n = re.match(self._subjob_parse, subjob)
# Old full_build is: build#-token-build_sequence#.
# Trim token-buildsequence since it's not used anymore.
if not n or not len(n.groups()) == 1:
full_build = None
else:
full_build = n.group(1)
# Translate new -release naming into old x86-mario-r19 style.
# x86-alex-release-R19-1979.0.0-a1-b1798_security =>
# x86-alex-r19-R19-1979.0.0-a1-b1798_security.
# Also change x86-generic-full-R19... to x86-generic-full-r19.
for terminator, replacement in [('-release', '-r'), ('-full', '-full-r')]:
if board.endswith(terminator):
board = board.replace(terminator,
replacement + full_build.split('-')[0][1:])
return board, full_build, self.TranslateSuffix(suffix), False
def ParseSimpleBuild(self, build):
"""Strip out the 0.x.y.z portion of the build.
Strip the core build string (1913.0.0 or 0.8.73.0) from a full build
string:
-(R19-1913.0.0)
-(R19-1913.0.0)-a1-b1539
-(0.8.73.0)-r3ed8d12f-b719.
@param build: long/full build string.
@return The simple numeric build number.
"""
for p in [self._shortbuild1_parse, self._shortbuild2_parse]:
m = re.match(p, build)
if m:
break
if m:
parsed_build = m.group(1)
else:
parsed_build = build
return parsed_build
def LoadFromDB(self, job_limit=None):
"""Initial queries from the db for test tables.
@param job_limit: Limit query to last n jobs.
"""
diag = dash_util.DebugTiming()
if not self._autotests:
self.QueryAutotests()
if not self._test_tree:
self.QueryNetbooks()
if not self._builds:
self.QueryBuilds(job_limit)
if not self._tests:
self.QueryTests()
del diag
def LoadPerfFromDB(self, job_limit=None):
"""Initial queries from db for perf checking.
@param job_limit: Limit query to last n jobs.
"""
diag = dash_util.DebugTiming()
self.LoadFromDB(job_limit)
if not self._perf_keyvals:
self.QueryKeyVals()
del diag
def QueryDjangoSession(self):
"""Get current row count from django_session table."""
query = [
"SELECT COUNT(*)",
"FROM django_session"]
self._cursor.execute(" ".join(query))
return self._cursor.fetchone()[0]
def QueryAutotests(self):
"""Get test attributes like author and path."""
query = [
"SELECT name, path, author",
"FROM afe_autotests",
"ORDER BY name"]
self._cursor.execute(" ".join(query))
for (name, path, author) in self._cursor.fetchall():
self._autotests[name] = [author, path]
def ScrubNetbook(self, netbook):
"""Remove deprecated platform prefixes.
If present, older prefixes are removed
and the string is lower-cased for one
common platform convention.
@param netbook: platform from Autotest (e.g. ALEX).
@return String with a 'scrubbed' netbook value.
"""
for prefix in LEGACY_PLATFORM_PREFIXES:
if netbook.startswith(prefix):
netbook = netbook[len(prefix):]
return netbook.lower()
def QueryNetbooks(self):
"""Get the netbooks know the to database."""
query = [
"SELECT name",
"FROM afe_labels",
"WHERE platform AND NOT invalid",
"UNION",
"SELECT distinct machine_group as name",
"FROM tko_machines",
"ORDER BY name"]
self._cursor.execute(" ".join(query))
for (netbook,) in self._cursor.fetchall():
netbook = self.ScrubNetbook(netbook)
self._test_tree[netbook] = {}
self._ui_categories[netbook] = {}
self._build_tree[netbook] = {}
def TranslateSuffix(self, suffix):
"""Allow processing of suffixes for aligning test suites.
@param suffix: The suffix to process.
@return The translated suffix.
"""
if not suffix:
return suffix
if suffix.startswith('kernel_'):
return KERNELTEST_TAG
elif suffix.startswith('enroll_'):
return 'enterprise'
elif suffix.endswith('_bvt'):
return 'bvt'
return suffix
def QueryBuilds(self, job_limit=None):
"""Get the boards and builds (jobs) to use.
@param job_limit: Limit query to last n jobs.
"""
query = [
"SELECT j.id, j.name, complete",
"FROM afe_jobs AS j",
"INNER JOIN afe_host_queue_entries AS q ON j.id = q.job_id",
"WHERE owner = %s",
" AND NOT name LIKE '%%-try'"
" AND NOT name LIKE '%%-test_suites/%%'"
"ORDER BY created_on DESC",
"LIMIT %s"]
if not job_limit:
job_limit = LAST_N_JOBS_LIMIT
params = [AUTOTEST_USER, job_limit]
self._cursor.execute(" ".join(query), params)
incomplete_jobnames = set()
jobname_to_jobid = {}
for job_id, name, complete in self._cursor.fetchall():
board, full_build, suffix, _ = self.ParseJobName(name)
if not board or not full_build or not suffix:
logging.debug("Ignoring invalid: %s (%s, %s, %s).", name, board,
full_build, suffix)
continue
if (self._dash_config and
'blacklistboards' in self._dash_config and
board in self._dash_config['blacklistboards']):
continue
str_job_id = str(job_id)
self._job_ids.add(str_job_id)
build_list_dict = self._builds.setdefault(board, {})
build_list_dict.setdefault(full_build, full_build)
# Track job_id's to later prune incomplete jobs.
# Use a name common to all the jobs.
tracking_name = "%s-%s" % (board, full_build)
suffixes = jobname_to_jobid.setdefault(tracking_name, {})
ids = suffixes.setdefault(suffix, [])
ids.append(str_job_id)
if not complete:
incomplete_jobnames.add(name)
# Now go prune out incomplete jobs.
for name in incomplete_jobnames:
logging.debug("Ignoring incomplete: %s.", name)
board, full_build, suffix, _ = self.ParseJobName(name)
tracking_name = "%s-%s" % (board, full_build)
if suffix in jobname_to_jobid[tracking_name]:
for str_job_id in jobname_to_jobid[tracking_name][suffix]:
if str_job_id in self._job_ids:
self._job_ids.remove(str_job_id)
del jobname_to_jobid[tracking_name][suffix]
if not jobname_to_jobid[tracking_name]:
if full_build in self._builds[board]:
del self._builds[board][full_build]
def QueryTests(self):
"""Get and stash the test data and attributes."""
if not self._job_ids:
return
query = [
"SELECT test_idx, test_name, job_name, job_tag, afe_job_id,",
" platform, hostname, status, job_started_time,"
" job_finished_time, reason",
"FROM tko_test_view_2",
self._common_where,
" AND afe_job_id IN (%s)" % ",".join(self._job_ids),
"ORDER BY job_idx DESC"]
params = [AUTOTEST_USER]
self._cursor.execute(" ".join(query), params)
results = self._cursor.fetchall()
for (idx, test_name, job_name, job_tag, job_id, netbook,
hostname, status, start_time, finish_time, reason) in results:
netbook = self.ScrubNetbook(netbook)
if not netbook in self.netbooks:
continue
board, full_build, suffix, experimental = self.ParseJobName(job_name)
if not board or not full_build or not suffix:
continue
category = self.ParseTestName(test_name)
ui_categories = self._ui_categories[netbook].setdefault(board, set())
if suffix in SUFFIXES_TO_SHOW:
ui_categories.add(suffix)
if suffix in GTEST_SUFFIXES:
category = suffix
category_dict = self._test_tree[netbook].setdefault(board, {})
if not test_name == SERVER_JOB:
attribute_dict = {}
attribute_dict["test_name"] = test_name
attribute_dict["hostname"] = hostname
attribute_dict["tag"] = job_tag
attribute_dict["status"] = status
attribute_dict["experimental"] = experimental
attribute_dict["attr"] = {}
if not status == 'GOOD':
attribute_dict["attr"]["reason"] = reason[:min(len(reason), 120)]
self._tests[str(idx)] = attribute_dict
ui_categories.add(category)
categories_to_load = [category, suffix]
# Add crash string summary details.
self._crashes.AddToCrashTree(netbook, board, full_build, test_name,
idx, job_tag)
else:
categories_to_load = [suffix]
for c in categories_to_load:
self._crashes.AddCrashCategory(
netbook, board, full_build, test_name, c)
# Add earliest job started time and latest job_finished_time.
build_board_dict = self._build_tree[netbook].setdefault(
board, {})
build_category_dict = build_board_dict.setdefault(c, {})
build_info = build_category_dict.setdefault(full_build, {
"start": datetime.datetime.now(),
"finish": datetime.datetime(2010, 1, 1),
"ngood": 0, # number of good test results excluding experimental
"ntotal": 0, # number of tests run excluding experimental
"xngood": 0, # number of good experimental test results
"xntotal": 0, # number of experimental tests run
"server_good": True})
if start_time < build_info["start"]:
build_info["start"] = start_time
if finish_time > build_info["finish"]:
build_info["finish"] = finish_time
if test_name == SERVER_JOB:
if not status == "GOOD":
build_info["server_good"] = False
continue
test_dict = category_dict.setdefault(c, {})
build_dict = test_dict.setdefault(test_name, {})
test_index_list = build_dict.setdefault(full_build, [set(), None])
test_index_list[0].add(str(idx))
if not test_index_list[1]:
test_index_list[1] = status
if not experimental:
build_info["ntotal"] += 1
if status == "GOOD":
build_info["ngood"] += 1
else:
build_info["xntotal"] += 1
if status == "GOOD":
build_info["xngood"] += 1
elif not status == "GOOD" and test_index_list[1] == "GOOD":
test_index_list[1] = status
if not experimental:
build_info["ngood"] -= 1
else:
build_info["xngood"] -= 1
query = [
"SELECT test_idx, attribute, value",
"FROM tko_test_attributes",
"WHERE test_idx in ('",
"','".join(self._tests.keys()),
"')",
"ORDER BY test_idx, attribute"]
self._cursor.execute(" ".join(query))
for i, a, v in self._cursor.fetchall():
self._tests[str(i)]["attr"][a] = v
def QueryKeyVals(self):
"""Get the performance keyvals."""
if not self._job_ids:
return
query = [
"SELECT platform, job_name, hostname, test_idx, test_name, ",
" iteration_key, iteration, iteration_value",
"FROM tko_perf_view_2 as p",
"INNER JOIN tko_jobs as j USING (job_idx)",
self._common_where,
" AND afe_job_id IN (%s)" % ",".join(self._job_ids),
"AND NOT ISNULL(iteration_value)",
"ORDER BY platform, job_name, test_name, iteration_key, ",
"test_idx, iteration"]
params = [AUTOTEST_USER]
self._cursor.execute(" ".join(query), params)
results = self._cursor.fetchall()
for (netbook, job_name, hostname, test_idx, test_name,
iteration_key, iteration, iteration_value) in results:
if iteration_value < 0:
continue
board, full_build, _, _ = self.ParseJobName(job_name)
if not board or not full_build:
continue
netbook = self.ScrubNetbook(netbook)
board_dict = self._perf_keyvals.setdefault(netbook, {})
test_dict = board_dict.setdefault(board, {})
key_dict = test_dict.setdefault(test_name, {})
build_dict = key_dict.setdefault(iteration_key, {})
value_list = build_dict.setdefault(full_build, ([], [], [], []))
value_list[0].append(iteration_value)
# Save test_idx to retrieve job details from data point.
value_list[1].append(test_idx)
# Save iteration for plotting.
value_list[2].append(iteration)
# Save hostname for plotting.
value_list[3].append(hostname)
def ShowDataModel(self):
"""Dump the data model for inspection."""
dash_util.ShowStructure("AUTOTESTS", self._autotests)
dash_util.ShowStructure("NETBOOKS", self.netbooks)
dash_util.ShowStructure("BOARDS", self.GetBoardTypes())
dash_util.ShowStructure("JOB IDS", self._job_ids)
dash_util.ShowStructure(
"BUILDS", self._builds, self.BUILDS_DOC)
dash_util.ShowStructure(
"BUILD TREE", self._build_tree, self.BUILD_TREE_DOC)
dash_util.ShowStructure(
"UI CATEGORIES", self._ui_categories, self.UI_CATEGORIES_DOC)
dash_util.ShowStructure(
"TEST TREE", self._test_tree, self.TEST_TREE_DOC)
dash_util.ShowStructure(
"TESTS WITH ATTRIBUTES", self._tests, self.TESTS_DOC)
dash_util.ShowStructure(
"CRASHES WITH TESTS AND CATEGORIES", self._crashes, self.CRASHES_DOC)
def ShowKeyVals(self):
"""Dump the perf keyvals for inspection."""
dash_util.ShowStructure(
"PERF KEYVALS", self._perf_keyvals, self.PERF_KEYVALS_DOC)
# Instance reference for singleton behavior.
__instance = None
__refs = 0
def __init__(self):
if AutotestDashView.__instance is None:
AutotestDashView.__instance = AutotestDashView.__impl()
self.__dict__["_AutotestDashView__instance"] = AutotestDashView.__instance
AutotestDashView.__refs += 1
def __del__(self):
AutotestDashView.__refs -= 1
if not AutotestDashView.__instance is None and AutotestDashView.__refs == 0:
del AutotestDashView.__instance
AutotestDashView.__instance = None
def __getattr__(self, attr):
return getattr(AutotestDashView.__instance, attr)
def __setattr__(self, attr, value):
return setattr(AutotestDashView.__instance, attr, value)
class SummaryRanges(object):
"""Each summary page needs list of each: boards, netbooks, builds."""
def __init__(self, dash_view, category, summary_limit):
self._summary_ranges = {}
self._summary_kernels = {}
boards = dash_view.GetBoardTypes() # Some may not have tests.
for board in boards:
netbooks = dash_view.GetNetbooksWithBoardTypeCategory(
board, category)
netbooks.sort()
# If all jobs were filtered by the summary, do not show that netbook
# in the summary (it will show in the details view).
build_numbers = dash_view.GetBoardtypeBuilds(board, summary_limit)
build_number_set = set(build_numbers)
netbooks_copy = netbooks[:]
for netbook in netbooks_copy:
netbook_set = set(dash_view.GetBuilds(
netbook, board, category))
if (build_number_set - netbook_set) == build_number_set:
netbooks.remove(netbook)
if netbooks:
self._summary_ranges[board] = (netbooks, build_numbers)
# Populate kernels
self._summary_kernels[board] = {}
for n in netbooks:
self._summary_kernels[board][n] = {}
for b in build_numbers:
self._summary_kernels[board][n][b] = dash_view.GetCategoryKernel(
n, board, category, b)
def GetBoards(self):
"""Gets all boards."""
boards = self._summary_ranges.keys()
boards.sort()
return boards
def GetNetbooks(self, board):
"""Gets all netbooks associated with a board.
@param board: The associated board.
@return A list of netbooks associated with the specified board.
"""
return self._summary_ranges[board][0]
def GetBuildNumbers(self, board):
"""Gets all build numbers associated with a board.
@param board: The associated board.
@return A list of build numbers associated with the specified board.
"""
return self._summary_ranges[board][1]
def GetKernel(self, board, netbook, build):
"""Gets the kernel assocaited with a board/netbook/build combination.
@param board: The associated board.
@param netbook: The associated netbook.
@param build: The associated build.
@return The kernel associated with the specified board/netbook/build
combination.
"""
try:
return self._summary_kernels[board][netbook][build]
except KeyError:
return None