Skip to content
This repository has been archived by the owner on Dec 15, 2018. It is now read-only.

Commit

Permalink
various selective testing UI improvement
Browse files Browse the repository at this point in the history
Summary:
This diff contains the following:
 - show selective testing policy for a build in the build info page
 - separate out recreate button into the regular recreate and recrate with selective testing
 - display result source in the targets UI

Only autobazel projects should be affected.

Test Plan:
ran locally, unit tests

{F528893}

{F529914}

Reviewers: anupc

Reviewed By: anupc

Subscribers: changesbot, wwu, kylec, anupc

Tags: #changes_ui

Differential Revision: https://tails.corp.dropbox.com/D232658
  • Loading branch information
Naphat Sanguansin committed Sep 29, 2016
1 parent 842343b commit e9ef7e7
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 16 deletions.
3 changes: 2 additions & 1 deletion changes/api/build_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from changes.api.serializer.models.testcase import TestCaseWithOriginCrumbler
from changes.config import db
from changes.constants import Result, Status
from changes.lib import build_type
from changes.lib import build_lib, build_type
from changes.models.build import Build, BuildPriority
from changes.models.buildseen import BuildSeen
from changes.models.event import Event, ALL_EVENT_TYPES
Expand Down Expand Up @@ -149,6 +149,7 @@ def get(self, build_id):
'tests': self.serialize(test_failures, extended_serializers),
},
'parents': self.serialize(get_parents_last_builds(build)),
'containsAutogeneratedPlan': build_lib.contains_autogenerated_plan(build),
})

return self.respond(context)
Expand Down
25 changes: 23 additions & 2 deletions changes/api/build_retry.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import uuid

from flask.ext.restful import reqparse
from flask_restful.types import boolean
from sqlalchemy.orm import joinedload

from changes.api.base import APIView
from changes.api.build_index import create_build
from changes.constants import Cause
from changes.constants import Cause, SelectiveTestingPolicy
from changes.models.build import Build
from changes.utils.selective_testing import get_selective_testing_policy


class BuildRetryAPIView(APIView):
parser = reqparse.RequestParser()

"""Optional flag, default to False."""
parser.add_argument('selective_testing', type=boolean, default=False)

def post(self, build_id):
args = self.parser.parse_args()

build = Build.query.options(
joinedload('project', innerjoin=True),
joinedload('author'),
Expand All @@ -18,6 +28,17 @@ def post(self, build_id):
if build is None:
return '', 404

selective_testing_policy = SelectiveTestingPolicy.disabled
if args.selective_testing:
if not build.source.patch:
# TODO(naphat) expose message returned here
selective_testing_policy, _ = get_selective_testing_policy(build.project, build.source.revision_sha, None)
else:
# NOTE: for diff builds, it makes sense to just do selective testing,
# since it will never become a parent build and will never be used to
# calculate revision results.
selective_testing_policy = SelectiveTestingPolicy.enabled

collection_id = uuid.uuid4()
new_build = create_build(
project=build.project,
Expand All @@ -28,7 +49,7 @@ def post(self, build_id):
author=build.author,
source=build.source,
cause=Cause.retry,
selective_testing_policy=build.selective_testing_policy,
selective_testing_policy=selective_testing_policy,
)

return '', 302, {'Location': '/api/0/builds/{0}/'.format(new_build.id.hex)}
2 changes: 2 additions & 0 deletions changes/api/serializer/models/bazeltarget.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from changes.api.serializer import Crumbler, register
from changes.constants import ResultSource
from changes.models.bazeltarget import BazelTarget


Expand All @@ -12,6 +13,7 @@ def crumble(self, instance, attrs):
'duration': instance.duration or 0,
'status': instance.status,
'result': instance.result,
'resultSource': instance.result_source or ResultSource.from_self,
'dateCreated': instance.date_created,
}
if instance.step_id:
Expand Down
4 changes: 4 additions & 0 deletions changes/api/serializer/models/build.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from changes.api.serializer import Crumbler, register
from changes.constants import SelectiveTestingPolicy
from changes.models.build import Build
from changes.models.failurereason import FailureReason
from changes.models.itemstat import ItemStat
Expand Down Expand Up @@ -62,6 +63,8 @@ def crumble(self, item, attrs):
if target is None and item.source and item.source.revision_sha:
target = item.source.revision_sha[:12]

selective_testing_policy = item.selective_testing_policy if item.selective_testing_policy else SelectiveTestingPolicy.disabled

return {
'id': item.id.hex,
'collection_id': item.collection_id,
Expand All @@ -70,6 +73,7 @@ def crumble(self, item, attrs):
'target': target,
'result': item.result,
'status': item.status,
'selectiveTestingPolicy': selective_testing_policy,
'project': item.project,
'cause': item.cause,
'author': item.author,
Expand Down
8 changes: 8 additions & 0 deletions changes/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class SelectiveTestingPolicy(Enum):
disabled = 0
enabled = 1

def __str__(self):
return SELECTIVE_TESTING_POLICY_LABELS[self]


class Provider(Enum):
unknown = 0
Expand Down Expand Up @@ -156,6 +159,11 @@ def __str__(self):
SelectiveTestingPolicy.enabled,
)

SELECTIVE_TESTING_POLICY_LABELS = {
SelectiveTestingPolicy.enabled: 'Enabled',
SelectiveTestingPolicy.disabled: 'Disabled',
}

CAUSE_LABELS = {
Cause.unknown: 'Unknown',
Cause.manual: 'Manual',
Expand Down
22 changes: 22 additions & 0 deletions changes/lib/build_lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sqlalchemy import literal
from changes.config import db
from changes.models.build import Build # NOQA
from changes.models.jobplan import JobPlan
from changes.models.option import ItemOption


def contains_autogenerated_plan(build):
"""Returns true if any of the jobs in this build was created with an
autogenerated jobplan.
"""
# type: (Build)->bool
contains = db.session.query(literal(True)).filter(
ItemOption.query.join(
JobPlan, JobPlan.plan_id == ItemOption.item_id,
).filter(
ItemOption.name == 'bazel.autogenerate',
ItemOption.value == '1',
JobPlan.build_id == build.id,
).exists()
).scalar()
return bool(contains)
26 changes: 26 additions & 0 deletions tests/changes/api/serializer/models/test_bazeltarget.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_simple(self):
assert result['result']['id'] == 'failed'
assert result['status']['id'] == 'finished'
assert result['duration'] == 134
assert result['resultSource']['id'] == 'from_self'

def test_no_step(self):
project = self.create_project()
Expand All @@ -50,3 +51,28 @@ def test_no_step(self):
assert result['result']['id'] == 'failed'
assert result['status']['id'] == 'finished'
assert result['duration'] == 134

def test_no_result_source(self):
project = self.create_project()
build = self.create_build(project=project)
job = self.create_job(build=build)
phase = self.create_jobphase(job)
step = self.create_jobstep(phase)
target = self.create_target(job, step,
name='target_foo',
duration=134,
result=Result.failed,
status=Status.finished,
date_created=datetime(2013, 9, 19, 22, 15, 22),
)
target.result_source = None
result = serialize(target)
assert result['id'] == str(target.id.hex)
assert result['job']['id'] == str(job.id.hex)
assert result['step']['id'] == str(step.id.hex)
assert result['name'] == 'target_foo'
assert result['dateCreated'] == '2013-09-19T22:15:22'
assert result['result']['id'] == 'failed'
assert result['status']['id'] == 'finished'
assert result['duration'] == 134
assert result['resultSource']['id'] == 'from_self'
1 change: 1 addition & 0 deletions tests/changes/api/serializer/models/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_simple():
result = serialize(build)
assert result['name'] == 'Hello world'
assert result['id'] == '33846695b2774b29a71795a009e8168a'
assert result['selectiveTestingPolicy']['id'] == 'disabled'
assert result['source']['id'] == build.source.id.hex
assert result['target'] == 'D1234'
assert result['message'] == 'Foo bar'
Expand Down
1 change: 1 addition & 0 deletions tests/changes/api/test_build_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def test_simple(self):
'job_id': job1.id.hex,
'step_id': step.id.hex
}
assert not data['containsAutogeneratedPlan']

def test_last_parent_revision_build(self):
project = self.create_project()
Expand Down
54 changes: 53 additions & 1 deletion tests/changes/api/test_build_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_simple(self, get_vcs):
assert new_build.label == build.label
assert new_build.message == build.message
assert new_build.target == build.target
assert new_build.selective_testing_policy == build.selective_testing_policy
assert new_build.selective_testing_policy is SelectiveTestingPolicy.disabled

jobs = list(Job.query.filter(
Job.build_id == new_build.id,
Expand All @@ -79,3 +79,55 @@ def test_simple(self, get_vcs):

new_job = jobs[0]
assert new_job.id != job.id

@patch('changes.api.build_retry.get_selective_testing_policy')
@patch('changes.models.repository.Repository.get_vcs')
def test_with_selective_testing_commit_builds(self, get_vcs, get_selective_testing_policy):
get_vcs.return_value = self.get_fake_vcs()
get_selective_testing_policy.return_value = SelectiveTestingPolicy.enabled, None
project = self.create_project()
build = self.create_build(project=project, selective_testing_policy=SelectiveTestingPolicy.disabled)
job = self.create_job(build=build)
self.create_plan(project)

path = '/api/0/builds/{0}/retry/'.format(build.id.hex)
resp = self.client.post(path, data={'selective_testing': 'true'}, follow_redirects=True)

get_selective_testing_policy.assert_called_once_with(project, build.source.revision_sha, None)

assert resp.status_code == 200

data = self.unserialize(resp)

assert data['id']

new_build = Build.query.get(data['id'])

assert new_build.id != build.id
assert new_build.cause == Cause.retry
assert new_build.selective_testing_policy is SelectiveTestingPolicy.enabled

@patch('changes.models.repository.Repository.get_vcs')
def test_with_selective_testing_diff_builds(self, get_vcs):
get_vcs.return_value = self.get_fake_vcs()
project = self.create_project()
patch = self.create_patch()
source = self.create_source(project, patch=patch)
build = self.create_build(project=project, selective_testing_policy=SelectiveTestingPolicy.disabled, source=source)
job = self.create_job(build=build)
self.create_plan(project)

path = '/api/0/builds/{0}/retry/'.format(build.id.hex)
resp = self.client.post(path, data={'selective_testing': 'true'}, follow_redirects=True)

assert resp.status_code == 200

data = self.unserialize(resp)

assert data['id']

new_build = Build.query.get(data['id'])

assert new_build.id != build.id
assert new_build.cause == Cause.retry
assert new_build.selective_testing_policy is SelectiveTestingPolicy.enabled
43 changes: 43 additions & 0 deletions tests/changes/lib/test_build_lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import absolute_import

from changes.lib import build_lib
from changes.models.plan import PlanStatus
from changes.testutils import TestCase


class AutogeneratedJobplansTestCase(TestCase):
def test_false(self):
build = self.create_build(self.create_project())
assert not build_lib.contains_autogenerated_plan(build)

def test_false_value(self):
project = self.create_project()
plan = self.create_plan(project)
self.create_option(item_id=plan.id, name='bazel.autogenerate', value='0')
build = self.create_build(project)
job = self.create_job(build)
jobplan = self.create_job_plan(job, plan)
assert not build_lib.contains_autogenerated_plan(build)

def test_true(self):
project = self.create_project()
plan = self.create_plan(project)
self.create_option(item_id=plan.id, name='bazel.autogenerate', value='1')
build = self.create_build(project)
job = self.create_job(build)
jobplan = self.create_job_plan(job, plan)
assert build_lib.contains_autogenerated_plan(build)

def test_true_multiple(self):
project = self.create_project()
plan = self.create_plan(project, status=PlanStatus.active)
self.create_option(item_id=plan.id, name='bazel.autogenerate', value='0')
build = self.create_build(project)
job = self.create_job(build)
jobplan = self.create_job_plan(job, plan)
plan = self.create_plan(project, status=PlanStatus.active)
self.create_option(item_id=plan.id, name='bazel.autogenerate', value='1')
build = self.create_build(project)
job = self.create_job(build)
jobplan = self.create_job_plan(job, plan)
assert build_lib.contains_autogenerated_plan(build)
12 changes: 8 additions & 4 deletions webapp/css/display.less
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ tr.gridHeader {
padding: 0px 5px;
margin: 5px 0;
cursor: pointer;

&:hover {
text-decoration: underline;
}
Expand Down Expand Up @@ -344,7 +344,7 @@ tr.gridHeader {
font-size: 8px;
top: -4px;
}

&.singleDigit .dotText {
font-size: 11px;
top: -3px;
Expand Down Expand Up @@ -553,6 +553,10 @@ a.button.sizedButton{
min-width: 115px;
}

a.button.sizedButton.wider{
min-width: 175px;
}

a.button.iconButton {
padding: 4px 6px;
}
Expand Down Expand Up @@ -697,7 +701,7 @@ div.chartBar.blackBg {
white-space: pre-wrap;
}

@keyframes blink {
@keyframes blink {
0% { opacity: 1.0; }
50% { opacity: 0.0; }
100% { opacity: 1.0; }
Expand All @@ -712,4 +716,4 @@ div.chartBar.blackBg {
.blink {
animation: blink 1.2s step-start 0s infinite;
-webkit-animation: blink 1.2s step-start 0s infinite;
}
}

0 comments on commit e9ef7e7

Please sign in to comment.