blob: 42396b349b58439c3e055219e67f5a9c402a8c25 [file]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This module is for logic about build test steps."""
import logging
from common.constants import SUPPORTED_ISOLATED_SCRIPT_TESTS
from common.findit_http_client import FinditHttpClient
from libs.test_results.webkit_layout_test_results import WebkitLayoutTestResults
from model.isolated_target import IsolatedTarget
from services import constants
from services import swarming
from waterfall import build_util
from waterfall import buildbot
from waterfall import waterfall_config
_HTTP_CLIENT = FinditHttpClient()
# TODO(crbug/804617): Modify this function to use new LUCI API when ready.
def _GetCandidateBounds(master_name, builder_name, upper_bound, lower_bound,
requested_commit_position):
""" Bisects the build number range and search for the earliest build whose
commit position >= requested_commit_position.
This function is still based on the assumption that build numbers are
consecutive. But because it asserts all results from GetBuildInfo, so
the worst case is that it hits assertion error on missing builds and aborts
analysis, and it should not happen often since the gaps between build
numbers should be not too many.
"""
while upper_bound - lower_bound > 1:
candidate_build_number = (upper_bound - lower_bound) / 2 + lower_bound
_, candidate_build = build_util.GetBuildInfo(master_name, builder_name,
candidate_build_number)
assert candidate_build
if candidate_build.commit_position == requested_commit_position:
# Exact match.
lower_bound = candidate_build_number - 1
upper_bound = candidate_build_number
elif candidate_build.commit_position > requested_commit_position:
# Go left.
upper_bound = candidate_build_number
else:
# Go right.
lower_bound = candidate_build_number
return upper_bound, lower_bound
def _GetLowerBoundBuildNumber(
lower_bound_build_number,
upper_bound_build_number,
# The default window is the number of builds
# Findit will look back for an analysis.
default_build_number_window_size=500):
"""Determines the lowest bound build number relative to an upper bound.
Args:
lower_bound_build_number (int): An optional int to return directly.
upper_bound_build)number (int): A non-optional int to use as a reference
point.
default_build_number_window_size (int): A fallback window to use to
determine the lower bound.
"""
if lower_bound_build_number is not None:
return lower_bound_build_number
if upper_bound_build_number > default_build_number_window_size:
return upper_bound_build_number - default_build_number_window_size
# For new builders, there may not be that many builds yet. This is a temporary
# workaround and wil be replaced when the isolate index service is ready and
# this function will no longer be necessary.
return upper_bound_build_number / 2
def _GetLowerBoundBuild(master_name, builder_name, lower_bound_build_number,
upper_bound_build_number, step_name):
"""Gets a valid lower build near build_number."""
# Search 10 below then 10 above for a valid build.
return (GetValidBuild(
master_name, builder_name, lower_bound_build_number, step_name, False,
min(10, lower_bound_build_number)) or GetValidBuild(
master_name, builder_name, lower_bound_build_number, step_name, True,
min(10, upper_bound_build_number - lower_bound_build_number)))
def GetValidBuild(master_name, builder_name, requested_build_number, step_name,
search_ascending, maximum_search_distance):
"""Gets a valid bound at or near the requested build number.
A build is considered valid if it exists, has a commit position, and has a
swarming task available.
Args:
master_name (str): The name of the master to check.
builder_name (str): The name of the builder to check.
requested_build_number (int): The build number to get a valid build at or
near.
step_name (str): The name of the step.
search_ascending (bool): Whether to return a build at least as high as the
requested build number.
maximum_search_distance (int): The maximum number of builds to check.
Returns:
(BuildInfo): A valid BuildInfo at or near requested_build_number, or None if
not found.
"""
candidate_build_number = requested_build_number
increment = 0
direction = 1 if search_ascending else -1
while increment <= maximum_search_distance:
candidate_build_number = requested_build_number + increment * direction
_, candidate_build = build_util.GetBuildInfo(master_name, builder_name,
candidate_build_number)
if (candidate_build and candidate_build.commit_position is not None and
(candidate_build.result != buildbot.EXCEPTION or
swarming.CanFindSwarmingTaskFromBuildForAStep(
_HTTP_CLIENT, master_name, builder_name, candidate_build_number,
step_name))):
return candidate_build
increment += 1
logging.warning('Failed to find valid build for %s/%s/%s within %s builds',
master_name, builder_name, requested_build_number,
maximum_search_distance)
return None
def GetBoundingIsolatedTargets(master_name, builder_name, target_name,
commit_position):
"""Determines the IsolatedTarget instances surrounding a commit position.
Args:
master_name (str): The name of the master to search by.
builder_name (str): The name of the builder to search by.
target_name (str): The name of the target to search by, e.g.
'browser_tests'.
commit_position (int): The desired commit position to find neighboring
IsolatedTargets.
Returns:
(IsolatedTarget, IsolatedTarget): The lower and upper bound IsolatedTargets.
"""
upper_bound_targets = (
IsolatedTarget.FindIsolateAtOrAfterCommitPositionByMaster(
master_name, builder_name, constants.GITILES_HOST,
constants.GITILES_PROJECT, constants.GITILES_REF, target_name,
commit_position))
lower_bound_targets = (
IsolatedTarget.FindIsolateBeforeCommitPositionByMaster(
master_name, builder_name, constants.GITILES_HOST,
constants.GITILES_PROJECT, constants.GITILES_REF, target_name,
commit_position))
assert upper_bound_targets, ((
'Unable to detect isolated targets at for {}/{} with minimum commit '
'position {}').format(master_name, builder_name, commit_position))
assert lower_bound_targets, ((
'Unable to detect isolated targets at for {}/{} below commit position'
' {}').format(master_name, builder_name, commit_position))
return lower_bound_targets[0], upper_bound_targets[0]
# TODO(crbug/804617): Modify this function to use new LUCI API when ready.
def GetValidBoundingBuildsForStep(
master_name, builder_name, step_name, lower_bound_build_number,
upper_bound_build_number, requested_commit_position):
"""Finds the two builds immediately before and after a commit position.
TODO (lijeffrey): use case in regression_range_analysis_pipeline.py is not
verified to be supported yet because that feature is not fully supported.
The builds should also have useful artifacts for the step. Meaning:
- The build completed without exception, or
- The build completed the step without exception but exceptioned out later.
Args:
master_name (str): The name of the master.
builder_name (str): The name of the builder.
step_name (str): The name of the step.
lower_bound_build_number (int): The earliest build number to search.
upper_bound_build_number (int): The latest build number to search.
requested_commit_position (int): The specified commit_position to find the
bounding build numbers.
Returns:
(BuildInfo, Buildinfo): The two nearest builds that bound the requested
commit position, with the first being earlier of the two. For example,
if build_1 has commit position 100, build_2 has commit position 110,
and 105 is requested, returns (build_1, build_2). Returns None for
either or both of the builds if they cannot be determined. If the
requested commit is before the lower bound, returns (None, BuildInfo).
If the requested commit is after the upper bound, returns
(BuildInfo, None). The calling code should check for the returned builds
and decide what to do accordingly.
If a given commit position is included in the blame list of either
boundary build, that boundary build is returned as both the lower and
upper bound build.
"""
logging.debug(
'GetBoundingBuildsForStep being called for %s/%s/%s with build '
'number bounds (%d, %d) at commit position %d', master_name, builder_name,
step_name, lower_bound_build_number or -1, upper_bound_build_number or -1,
requested_commit_position)
assert upper_bound_build_number is not None, 'upper_bound can\'t be None'
_, latest_build_info = build_util.GetBuildInfo(master_name, builder_name,
upper_bound_build_number)
logging.debug('latest_build_info: %r', latest_build_info)
assert latest_build_info, 'Couldn\'t find build info for %s/%s/%s' % (
master_name, builder_name, upper_bound_build_number)
assert latest_build_info.commit_position is not None
lower_bound_build_number = _GetLowerBoundBuildNumber(
lower_bound_build_number, upper_bound_build_number)
logging.info('Found lower_bound_build_number to be %d.',
lower_bound_build_number)
earliest_build_info = _GetLowerBoundBuild(master_name, builder_name,
lower_bound_build_number,
upper_bound_build_number, step_name)
logging.debug('earliest_build_info: %r', earliest_build_info)
assert earliest_build_info, 'Couldn\'t find build info for %s/%s/%s' % (
master_name, builder_name, lower_bound_build_number)
assert earliest_build_info.commit_position is not None
assert (latest_build_info.commit_position >=
earliest_build_info.commit_position)
if requested_commit_position <= earliest_build_info.commit_position:
if not swarming.CanFindSwarmingTaskFromBuildForAStep(
_HTTP_CLIENT, master_name, builder_name, lower_bound_build_number,
step_name):
# TODO(crbug.com/831828): Support newly added test steps for this case.
# Cannot find valid artifact in earliest_build for the step.
return None, None
if requested_commit_position == earliest_build_info.commit_position:
return earliest_build_info, earliest_build_info
else:
return None, earliest_build_info
if requested_commit_position >= latest_build_info.commit_position:
if not swarming.CanFindSwarmingTaskFromBuildForAStep(
_HTTP_CLIENT, master_name, builder_name, upper_bound_build_number,
step_name):
# Cannot find valid artifact in latest_build for the step.
return None, None
if latest_build_info.commit_position == requested_commit_position:
return latest_build_info, latest_build_info
else:
return latest_build_info, None
# Gets candidata builds.
upper_bound, lower_bound = _GetCandidateBounds(
master_name, builder_name, upper_bound_build_number,
lower_bound_build_number, requested_commit_position)
# Get valid builds at or near the candidate build bounds.
lower_bound_build = GetValidBuild(master_name, builder_name, lower_bound,
step_name, False,
lower_bound - lower_bound_build_number)
upper_bound_build = GetValidBuild(master_name, builder_name, upper_bound,
step_name, True,
upper_bound_build_number - upper_bound)
return lower_bound_build, upper_bound_build
def IsStepSupportedByFindit(test_result_object, step_name, master_name):
"""Checks if a test step is currently supported by Findit.
Currently Findit supports all gtest test steps;
for isolated-script-tests, Findit only supports webkit_layout_tests.
* If there isn't a parser for the test_result of the step, it's not supported;
* If the step is an isolated-script-test step but not webkit_layout_tests,
it's not supported.
* If the step is set to unsupported in config, it's not supported.
"""
if not test_result_object:
return False
if not waterfall_config.StepIsSupportedForMaster(step_name, master_name):
return False
# TODO(crbug/836317): remove the special check for step_name when Findit
# supports all isolated_script_tests.
if (isinstance(test_result_object, WebkitLayoutTestResults) and
step_name not in SUPPORTED_ISOLATED_SCRIPT_TESTS):
return False
return True