Bug 1270629 - Replace Job Info artifacts with a "jobdetail" endpoint

This is simpler and removes one more type of artifact from the per-project
databases.
This commit is contained in:
William Lachance 2016-05-05 16:40:59 -04:00
Родитель d0ea27feb8
Коммит af9c208792
12 изменённых файлов: 162 добавлений и 109 удалений

Просмотреть файл

@ -1,6 +1,7 @@
import json
import pytest
from django.forms import model_to_dict
from mock import MagicMock
from tests.test_utils import post_collection
@ -9,6 +10,7 @@ from treeherder.log_parser.parsers import StepParser
from treeherder.model import error_summary
from treeherder.model.derived import (ArtifactsModel,
JobsModel)
from treeherder.model.models import JobDetail
@pytest.fixture
@ -361,7 +363,8 @@ def test_post_job_artifacts_by_add_artifact(
}
})
ji_blob = json.dumps({"job_details": [{"foo": "fah"}]})
ji_blob = json.dumps({"job_details": [{"title": "mytitle",
"value": "myvalue"}]})
bapi_blob = json.dumps({"buildername": "merd"})
pb_blob = json.dumps({"build_url": "feh", "chunk": 1, "config_file": "mah"})
@ -374,8 +377,17 @@ def test_post_job_artifacts_by_add_artifact(
post_collection(test_project, tjc)
check_artifacts(test_project, job_guid, 'parsed', 5,
{'Bug suggestions', 'text_log_summary', 'Job Info',
assert JobDetail.objects.count() == 1
assert model_to_dict(JobDetail.objects.get(job__guid=job_guid)) == {
'id': 1,
'job': 1,
'title': 'mytitle',
'value': 'myvalue',
'url': None
}
check_artifacts(test_project, job_guid, 'parsed', 4,
{'Bug suggestions', 'text_log_summary',
'privatebuild', 'buildapi'}, mock_error_summary)
# ensure the parsing didn't happen

Просмотреть файл

@ -4,6 +4,7 @@ import pytest
from treeherder.etl.job_loader import JobLoader
from treeherder.model.derived.artifacts import ArtifactsModel
from treeherder.model.models import JobDetail
@pytest.fixture
@ -55,7 +56,9 @@ def test_ingest_pulse_jobs(pulse_jobs, test_project, jm, result_set_stored,
assert len(logs) == 1
with ArtifactsModel(test_project) as am:
artifacts = am.get_job_artifact_list(0, 10)
assert len(artifacts) == 4
assert len(artifacts) == 3
assert JobDetail.objects.count() == 2
def test_transition_pending_running_complete(first_job, jm, mock_log_parser):

Просмотреть файл

@ -7,6 +7,8 @@ import simplejson as json
from django.conf import settings
from django.utils.six import BytesIO
from treeherder.model.models import JobDetail
from ..sampledata import SampleData
@ -60,8 +62,8 @@ def mock_mozlog_get_log_handler(monkeypatch):
def test_parse_log(jm, jobs_with_local_log, sample_resultset,
mock_post_json, mock_fetch_json):
"""
check that at least 3 job_artifacts get inserted when running
a parse_log task for a successful job
check that 2 job_artifacts get inserted when running a parse_log task for
a successful job and that JobDetail objects get created
"""
jm.store_result_set_data(sample_resultset)
@ -84,11 +86,12 @@ def test_parse_log(jm, jobs_with_local_log, sample_resultset,
placeholders=[job_id]
)
# we must have at least 3 artifacts:
# we should have 2 artifacts:
# 1 for the log viewer
# 1 for the job artifact panel
# 1 for the bug suggestions
assert len(job_artifacts) >= 3
assert len(job_artifacts) == 2
# this log generates 4 job detail objects at present
print JobDetail.objects.count() == 4
def test_bug_suggestions_artifact(jm, jobs_with_local_log,
@ -118,11 +121,10 @@ def test_bug_suggestions_artifact(jm, jobs_with_local_log,
placeholders=[job_id]
)
# we must have at least 3 artifacts:
# we must have 2 artifacts:
# 1 for the log viewer
# 1 for the job artifact panel
# 1 for the bug suggestions
assert len(job_artifacts) >= 3
assert len(job_artifacts) == 2
structured_log_artifact = [artifact for artifact in job_artifacts
if artifact["name"] == "text_log_summary"][0]

Просмотреть файл

@ -14,6 +14,7 @@ from treeherder.model.models import (BuildPlatform,
FailureClassification,
FailureLine,
Job,
JobDetail,
JobDuration,
JobGroup,
JobType,
@ -368,6 +369,7 @@ def test_cycle_all_data(jm, sample_data,
assert len(jobs_after) == 0
assert FailureLine.objects.count() == 0
assert Job.objects.count() == 0
assert JobDetail.objects.count() == 0
def test_cycle_one_job(jm, sample_data,
@ -383,10 +385,17 @@ def test_cycle_one_job(jm, sample_data,
job_not_deleted = jm.get_job(2)[0]
failure_lines_remaining = create_failure_lines(test_repository,
job_not_deleted["job_guid"],
[(test_line, {}),
(test_line, {"subtest": "subtest2"})])
extra_objects = {
'failure_lines': (FailureLine,
create_failure_lines(test_repository,
job_not_deleted["job_guid"],
[(test_line, {}),
(test_line, {"subtest": "subtest2"})])),
'job_details': (JobDetail, [JobDetail.objects.create(
job=Job.objects.get(guid=job_not_deleted["job_guid"]),
title='test',
value='testvalue')])
}
time_now = time.time()
cycle_date_ts = int(time_now - 7 * 24 * 3600)
@ -424,8 +433,9 @@ def test_cycle_one_job(jm, sample_data,
assert len(jobs_after) == len(jobs_before) - len(jobs_to_be_deleted)
assert len(jobs_after) == Job.objects.count()
assert (set(item.id for item in FailureLine.objects.all()) ==
set(item.id for item in failure_lines_remaining))
for (object_type, objects) in extra_objects.values():
assert (set(item.id for item in object_type.objects.all()) ==
set(item.id for item in objects))
def test_cycle_all_data_in_chunks(jm, sample_data,
@ -467,6 +477,7 @@ def test_cycle_all_data_in_chunks(jm, sample_data,
assert len(jobs_after) == 0
assert Job.objects.count() == 0
assert FailureLine.objects.count() == 0
assert JobDetail.objects.count() == 0
def test_cycle_task_set_meta(jm):

Просмотреть файл

@ -8,6 +8,7 @@ from django.forms import model_to_dict
from treeherder.etl.perf import load_perf_artifacts
from treeherder.model import utils
from treeherder.model.models import (Job,
JobDetail,
ReferenceDataSignatures)
from .base import TreeherderModelBase
@ -91,6 +92,19 @@ class ArtifactsModel(TreeherderModelBase):
placeholders=artifact_placeholders,
executemany=True)
def store_job_details(self, job, job_info_artifact):
"""
Store the contents of the job info artifact
in job details
"""
job_details = json.loads(job_info_artifact['blob'])['job_details']
for job_detail in job_details:
JobDetail.objects.get_or_create(
job=job,
title=job_detail.get('title'),
value=job_detail['value'],
url=job_detail.get('url'))
def store_performance_artifact(
self, job_ids, performance_artifact_placeholders):
"""
@ -169,6 +183,8 @@ class ArtifactsModel(TreeherderModelBase):
artifact, performance_artifact_list,
performance_artifact_job_id_list,
job.project_specific_id)
elif artifact_name == 'Job Info':
self.store_job_details(job, artifact)
else:
self._adapt_job_artifact_collection(
artifact, job_artifact_list,

Просмотреть файл

@ -22,6 +22,7 @@ from treeherder.model.models import (BuildPlatform,
FailureClassification,
FailureLine,
Job,
JobDetail,
JobDuration,
JobGroup,
JobType,
@ -751,9 +752,11 @@ into chunks of chunk_size size. Returns the number of result sets deleted"""
# remove data from specified jobs tables that is older than max_timestamp
self._execute_table_deletes(jobs_targets, 'jobs', sleep_time)
# Remove FailueLine + intermediate job entries for these jobs
# Remove ORM entries for these jobs
orm_delete(FailureLine, FailureLine.objects.filter(job_guid__in=job_guid_list),
chunk_size, sleep_time)
orm_delete(JobDetail, JobDetail.objects.filter(job__guid__in=job_guid_list),
chunk_size, sleep_time)
orm_delete(Job, Job.objects.filter(guid__in=job_guid_list),
chunk_size, sleep_time)
@ -1272,7 +1275,6 @@ into chunks of chunk_size size. Returns the number of result sets deleted"""
push_timestamps = {}
for index, job in enumerate(job_placeholders):
# Replace reference data with their associated ids
self._set_data_ids(
index,

Просмотреть файл

@ -93,6 +93,7 @@
<script src="js/models/resultset.js"></script>
<script src="js/models/resultsets_store.js"></script>
<script src="js/models/job_artifact.js"></script>
<script src="js/models/job_detail.js"></script>
<script src="js/models/repository.js"></script>
<script src="js/models/bug_job_map.js"></script>
<script src="js/models/classification.js"></script>

Просмотреть файл

@ -2,13 +2,13 @@
logViewerApp.controller('LogviewerCtrl', [
'$anchorScroll', '$http', '$location', '$q', '$rootScope', '$scope',
'$timeout', 'ThJobArtifactModel', 'ThLog', 'ThLogSliceModel', 'ThJobModel', 'thNotify',
'dateFilter', 'ThResultSetModel', 'thDateFormat', 'thReftestStatus',
'$timeout', 'ThJobArtifactModel', 'ThJobDetailModel', 'ThLog', 'ThLogSliceModel',
'ThJobModel', 'thNotify', 'dateFilter', 'ThResultSetModel', 'thDateFormat',
'thReftestStatus',
function Logviewer(
$anchorScroll, $http, $location, $q, $rootScope, $scope,
$timeout, ThJobArtifactModel, ThLog, ThLogSliceModel, ThJobModel, thNotify,
dateFilter, ThResultSetModel, thDateFormat, thReftestStatus) {
var $log = new ThLog('LogviewerCtrl');
// changes the size of chunks pulled from server
@ -250,6 +250,9 @@ logViewerApp.controller('LogviewerCtrl', [
$scope.logProperties.push({label: "Revision", value: revision});
});
ThJobDetailModel.getJobDetails(job.job_guid).then(function(jobDetails) {
$scope.job_details = jobDetails;
});
}, function (error) {
$scope.loading = false;
$scope.jobExists = false;
@ -257,42 +260,37 @@ logViewerApp.controller('LogviewerCtrl', [
});
// Make the log and job artifacts available
ThJobArtifactModel.get_list({job_id: $scope.job_id, name__in: 'text_log_summary,Job Info'})
ThJobArtifactModel.get_list({job_id: $scope.job_id, name: 'text_log_summary'})
.then(function(artifactList) {
artifactList.forEach(function(artifact, $event) {
if (artifact.name === 'text_log_summary') {
$scope.artifact = artifact.blob;
$scope.step_data = artifact.blob.step_data;
if (artifactList.length === 0)
return;
$scope.artifact = artifactList[0].blob;
$scope.step_data = $scope.artifact.step_data;
// If the log contains no errors load the head otherwise
// load the first failure step line in the artifact. We
// also need to test for the 0th element for outlier jobs.
if ($scope.step_data.steps[0]) {
// If the log contains no errors load the head otherwise
// load the first failure step line in the artifact. We
// also need to test for the 0th element for outlier jobs.
if ($scope.step_data.steps[0]) {
if ($scope.step_data.all_errors.length === 0) {
angular.element(document).ready(function () {
if (isNaN($scope.selectedBegin)) {
for (var i = 0; i < $scope.step_data.steps.length; i++) {
var step = $scope.step_data.steps[i];
if (step.result !== "success") {
$scope.selectedBegin = step.started_linenumber;
$scope.selectedEnd = step.finished_linenumber;
break;
}
}
if ($scope.step_data.all_errors.length === 0) {
angular.element(document).ready(function () {
if (isNaN($scope.selectedBegin)) {
for (var i = 0; i < $scope.step_data.steps.length; i++) {
var step = $scope.step_data.steps[i];
if (step.result !== "success") {
$scope.selectedBegin = step.started_linenumber;
$scope.selectedEnd = step.finished_linenumber;
break;
}
moveScrollToLineNumber($scope.selectedBegin, $event);
});
} else {
$scope.setLineNumber($scope.step_data.all_errors[0].linenumber);
moveScrollToLineNumber($scope.selectedBegin, $event);
}
}
}
} else if (artifact.name === 'Job Info') {
$scope.job_details = artifact.blob.job_details;
moveScrollToLineNumber($scope.selectedBegin, $event);
});
} else {
$scope.setLineNumber($scope.step_data.all_errors[0].linenumber);
moveScrollToLineNumber($scope.selectedBegin, $event);
}
});
}
});
};

Просмотреть файл

@ -0,0 +1,18 @@
'use strict';
treeherder.factory('ThJobDetailModel', [
'$http', 'thUrl', function($http, thUrl) {
return {
getJobDetails: function(jobGuid, config) {
config = config || {};
var timeout = config.timeout || null;
return $http.get(thUrl.getRootUrl("/jobdetail/"), {
params: { job__guid: jobGuid },
timeout: timeout
}).then(function(response) {
return response.data.results;
});
}
};
}]);

Просмотреть файл

@ -96,13 +96,16 @@
ng-cloak class="break-word">{{property.value}}</td>
</tr>
<tr ng-repeat="line in job_details | orderBy:'title'">
<th ng-cloak>{{line.title}}:</th>
<td ng-switch on="line.content_type">
<a ng-cloak ng-switch-when="link" title="{{line.value}}"
href="{{line.url}}" target="_blank">{{line.value}}</a>
<span ng-switch-when="raw_html" ng-bind-html="line.value"></span>
<span ng-switch-default>{{line.value}}</span>
<td/>
<th ng-cloak>
<span ng-if="line.title">
{{line.title}}:
</span>
</th>
<td ng-cloak>
<a ng-if="line.url" title="{{line.value}}"
href="{{line.url}}" target="_blank">{{line.value}}</a>
<span ng-if="!line.url" ng-bind-html="line.value"></span>
</td>
</tr>
</table>
</div>
@ -147,6 +150,7 @@
<!-- Model services -->
<script src="js/models/job_artifact.js"></script>
<script src="js/models/job_detail.js"></script>
<script src="js/models/job.js"></script>
<script src="js/models/runnable_job.js"></script>
<script src="js/models/resultset.js"></script>

Просмотреть файл

@ -5,15 +5,15 @@ treeherder.controller('PluginCtrl', [
'thClassificationTypes', 'ThJobModel', 'thEvents', 'dateFilter', 'thDateFormat',
'numberFilter', 'ThBugJobMapModel', 'thResultStatus', 'thJobFilters',
'ThResultSetModel', 'ThLog', '$q', 'thPinboard', 'ThJobArtifactModel',
'thBuildApi', 'thNotify', 'ThJobLogUrlModel', 'ThModelErrors', 'thTabs',
'$timeout', 'thReftestStatus', 'ThResultSetStore',
'ThJobDetailModel', 'thBuildApi', 'thNotify', 'ThJobLogUrlModel', 'ThModelErrors',
'thTabs', '$timeout', 'thReftestStatus', 'ThResultSetStore',
'PhSeries', 'thServiceDomain', 'ThFailureLinesModel',
function PluginCtrl(
$scope, $rootScope, $location, $http, thUrl, ThJobClassificationModel,
thClassificationTypes, ThJobModel, thEvents, dateFilter, thDateFormat,
numberFilter, ThBugJobMapModel, thResultStatus, thJobFilters,
ThResultSetModel, ThLog, $q, thPinboard, ThJobArtifactModel,
thBuildApi, thNotify, ThJobLogUrlModel, ThModelErrors, thTabs,
ThJobDetailModel, thBuildApi, thNotify, ThJobLogUrlModel, ThModelErrors, thTabs,
$timeout, thReftestStatus, ThResultSetStore, PhSeries,
thServiceDomain, ThFailureLinesModel) {
@ -105,9 +105,9 @@ treeherder.controller('PluginCtrl', [
var selectJobPromise = null;
var selectJobRetryPromise = null;
var selectJob = function(job_id) {
var selectJob = function(job) {
// set the scope variables needed for the job detail panel
if (job_id) {
if (job.id) {
$scope.job_detail_loading = true;
if(selectJobPromise !== null){
$log.debug("timing out previous job request");
@ -121,34 +121,33 @@ treeherder.controller('PluginCtrl', [
$scope.job = {};
$scope.artifacts = {};
$scope.job_details = [];
var jobDetailPromise = ThJobModel.get(
$scope.repoName, job_id,
var jobPromise = ThJobModel.get(
$scope.repoName, job.id,
{timeout: selectJobPromise});
var buildapiArtifactPromise = ThJobArtifactModel.get_list(
{name: "buildapi", "type": "json", job_id: job_id},
{name: "buildapi", "type": "json", job_id: job.id},
{timeout: selectJobPromise});
var jobInfoArtifactPromise = ThJobArtifactModel.get_list({
name: "Job Info", "type": "json", job_id: job_id},
{timeout: selectJobPromise});
var jobDetailPromise = ThJobDetailModel.getJobDetails(
job.job_guid, {timeout: selectJobPromise});
var jobLogUrlPromise = ThJobLogUrlModel.get_list(
job_id,
job.id,
{timeout: selectJobPromise});
var phSeriesPromise = PhSeries.getSeriesData(
$scope.repoName, { job_id: job_id });
$scope.repoName, { job_id: job.id });
return $q.all([
jobDetailPromise,
jobPromise,
buildapiArtifactPromise,
jobInfoArtifactPromise,
jobDetailPromise,
jobLogUrlPromise,
phSeriesPromise
]).then(function(results){
//the first result comes from the job detail promise
//the first result comes from the job promise
$scope.job = results[0];
if ($scope.job.state === 'running') {
$scope.eta = $scope.job.running_time_remaining();
@ -162,7 +161,6 @@ treeherder.controller('PluginCtrl', [
// the second result come from the buildapi artifact promise
var buildapi_artifact = results[1];
// if this is a buildbot job use the buildername for searching
if (buildapi_artifact.length > 0 &&
_.has(buildapi_artifact[0], 'blob')){
@ -175,23 +173,14 @@ treeherder.controller('PluginCtrl', [
$scope.jobSearchSignature = $scope.job.signature;
$scope.jobSearchStrHref = getJobSearchStrHref($scope.jobSearchStr);
$scope.jobSearchSignatureHref = getJobSearchStrHref($scope.job.signature);
// the third result comes from the job info artifact promise
var jobInfoArtifact = results[2];
if (jobInfoArtifact.length > 0) {
// The job artifacts may have many "Job Info" blobs so
// we merge them here to make displaying them in the UI
// easier.
$scope.job_details = jobInfoArtifact.reduce(function(result, artifact) {
if (artifact.blob && Array.isArray(artifact.blob.job_details)) {
result = result.concat(artifact.blob.job_details);
}
if ($scope.artifacts.buildapi) {
$scope.artifacts.buildapi.blob.title = "Buildername";
$scope.artifacts.buildapi.blob.value = $scope.artifacts.buildapi.blob.buildername;
result = result.concat($scope.artifacts.buildapi.blob);
}
return result;
}, []);
// the third result comes from the job detail promise
$scope.job_details = results[2];
// incorporate the buildername into the job details if it is present
if ($scope.artifacts.buildapi) {
$scope.artifacts.buildapi.blob.title = "Buildername";
$scope.artifacts.buildapi.blob.value = $scope.artifacts.buildapi.blob.buildername;
$scope.job_details = $scope.job_details.concat($scope.artifacts.buildapi.blob);
$scope.buildernameIndex = _.findIndex($scope.job_details, {title: "Buildername"});
}
@ -234,7 +223,7 @@ treeherder.controller('PluginCtrl', [
)){
selectJobRetryPromise = $timeout(function(){
// refetch the job data details
selectJobAndRender(job_id);
selectJobAndRender(job);
}, 5000);
}
});
@ -447,15 +436,15 @@ treeherder.controller('PluginCtrl', [
}
};
var selectJobAndRender = function(job_id) {
$scope.jobLoadedPromise = selectJob(job_id);
var selectJobAndRender = function(job) {
$scope.jobLoadedPromise = selectJob(job);
$scope.jobLoadedPromise.then(function(){
thTabs.showTab(thTabs.selectedTab, job_id);
thTabs.showTab(thTabs.selectedTab, job.id);
});
};
$rootScope.$on(thEvents.jobClick, function(event, job) {
selectJobAndRender(job.id);
selectJobAndRender(job);
$rootScope.selectedJob = job;
});

Просмотреть файл

@ -1,17 +1,14 @@
<div class="job-tabs-content">
<ul class="list-unstyled">
<li ng-repeat="line in job_details | orderBy:'title'" class="small">
<label ng-if="line.content_type != 'TalosResult'">{{line.title}}:</label>
<span ng-if="line.content_type != 'TalosResult'" ng-switch on="line.content_type">
<a ng-switch-when="link" title="{{line.value}}"
href="{{line.url}}" target="_blank">{{line.value}}</a>
<span ng-if="line.value.endsWith('raw.log')">
- <a title="{{line.value}}" href="http://mozilla.github.io/wptview/#/?urls={{line.url | encodeURIComponent}},{{job_details[buildernameIndex].value | encodeURIComponent}}">open in test results viewer</a>
</span>
<span ng-switch-when="raw_html" ng-bind-html="line.value"></span>
<span title="{{line.value}}" ng-switch-when="object">{{line.value}}</span>
<span title="{{line.value}}" ng-switch-default>{{line.value}}</span>
<label>{{line.title ? line.title : 'Untitled data'}}:</label>
<!-- URL provided -->
<a ng-if="line.url" title="{{line.value}}" href="{{line.url}}" target="_blank">{{line.value}}</a>
<span ng-if="line.url && line.value.endsWith('raw.log')">
- <a title="{{line.value}}" href="http://mozilla.github.io/wptview/#/?urls={{line.url | encodeURIComponent}},{{job_details[buildernameIndex].value | encodeURIComponent}}">open in test results viewer</a>
</span>
<!-- no URL (just informational) -->
<span ng-if="!line.url" ng-bind-html="line.value"></span>
</li>
</ul>
</div>