зеркало из https://github.com/mozilla/treeherder.git
Bug 1321787 - Autoclassify through the TextLogError rather than through the FailureLine (#2179)
This commit is contained in:
Родитель
76fc09fd1e
Коммит
3c03bb9fba
|
@ -1,23 +1,19 @@
|
||||||
from datetime import (datetime,
|
|
||||||
timedelta)
|
|
||||||
|
|
||||||
from treeherder.autoclassify.detectors import (ManualDetector,
|
from treeherder.autoclassify.detectors import (ManualDetector,
|
||||||
TestFailureDetector)
|
TestFailureDetector)
|
||||||
from treeherder.autoclassify.matchers import (CrashSignatureMatcher,
|
from treeherder.autoclassify.matchers import (CrashSignatureMatcher,
|
||||||
ElasticSearchTestMatcher,
|
ElasticSearchTestMatcher,
|
||||||
PreciseTestMatcher,
|
PreciseTestMatcher)
|
||||||
time_window)
|
|
||||||
from treeherder.autoclassify.tasks import autoclassify
|
from treeherder.autoclassify.tasks import autoclassify
|
||||||
from treeherder.model.models import (BugJobMap,
|
from treeherder.model.models import (BugJobMap,
|
||||||
ClassifiedFailure,
|
ClassifiedFailure,
|
||||||
FailureMatch,
|
FailureMatch,
|
||||||
JobNote,
|
JobNote,
|
||||||
TextLogError,
|
TextLogError,
|
||||||
|
TextLogErrorMatch,
|
||||||
TextLogErrorMetadata)
|
TextLogErrorMetadata)
|
||||||
|
|
||||||
from .utils import (crash_line,
|
from .utils import (crash_line,
|
||||||
create_failure_lines,
|
create_lines,
|
||||||
create_text_log_errors,
|
|
||||||
log_line,
|
log_line,
|
||||||
register_detectors,
|
register_detectors,
|
||||||
register_matchers,
|
register_matchers,
|
||||||
|
@ -37,16 +33,6 @@ def do_autoclassify(job, test_failure_lines, matchers, status="testfailed"):
|
||||||
item.refresh_from_db()
|
item.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
def create_lines(test_job, lines):
|
|
||||||
error_lines = create_text_log_errors(test_job, lines)
|
|
||||||
failure_lines = create_failure_lines(test_job, lines)
|
|
||||||
|
|
||||||
for error_line, failure_line in zip(error_lines, failure_lines):
|
|
||||||
TextLogErrorMetadata.objects.create(text_log_error=error_line,
|
|
||||||
failure_line=failure_line)
|
|
||||||
return error_lines, failure_lines
|
|
||||||
|
|
||||||
|
|
||||||
def test_classify_test_failure(text_log_errors_failure_lines,
|
def test_classify_test_failure(text_log_errors_failure_lines,
|
||||||
classified_failures,
|
classified_failures,
|
||||||
test_job_2):
|
test_job_2):
|
||||||
|
@ -120,7 +106,7 @@ def test_autoclassify_update_job_classification(failure_lines, classified_failur
|
||||||
|
|
||||||
|
|
||||||
def test_autoclassify_no_update_job_classification(test_job, test_job_2,
|
def test_autoclassify_no_update_job_classification(test_job, test_job_2,
|
||||||
failure_lines,
|
text_log_errors_failure_lines,
|
||||||
classified_failures):
|
classified_failures):
|
||||||
|
|
||||||
lines = [(test_line, {})]
|
lines = [(test_line, {})]
|
||||||
|
@ -134,13 +120,18 @@ def test_autoclassify_no_update_job_classification(test_job, test_job_2,
|
||||||
assert JobNote.objects.filter(job=test_job_2).count() == 0
|
assert JobNote.objects.filter(job=test_job_2).count() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_autoclassified_after_manual_classification(test_user, test_job_2,
|
def test_autoclassified_after_manual_classification(test_user,
|
||||||
failure_lines, failure_classifications):
|
test_job_2,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
failure_classifications):
|
||||||
register_detectors(ManualDetector, TestFailureDetector)
|
register_detectors(ManualDetector, TestFailureDetector)
|
||||||
|
|
||||||
lines = [(test_line, {})]
|
lines = [(test_line, {})]
|
||||||
test_error_lines, test_failure_lines = create_lines(test_job_2, lines)
|
test_error_lines, test_failure_lines = create_lines(test_job_2, lines)
|
||||||
|
|
||||||
|
BugJobMap.objects.create(job=test_job_2,
|
||||||
|
bug_id=1234,
|
||||||
|
user=test_user)
|
||||||
JobNote.objects.create(job=test_job_2,
|
JobNote.objects.create(job=test_job_2,
|
||||||
failure_classification_id=4,
|
failure_classification_id=4,
|
||||||
user=test_user,
|
user=test_user,
|
||||||
|
@ -186,9 +177,9 @@ def test_autoclassified_no_update_after_manual_classification_2(test_user, test_
|
||||||
register_detectors(ManualDetector, TestFailureDetector)
|
register_detectors(ManualDetector, TestFailureDetector)
|
||||||
|
|
||||||
# Too many failure lines
|
# Too many failure lines
|
||||||
test_failure_lines = create_failure_lines(test_job_2,
|
test_error_lines, test_failure_lines = create_lines(test_job_2,
|
||||||
[(log_line, {}),
|
[(log_line, {}),
|
||||||
(test_line, {"subtest": "subtest2"})])
|
(test_line, {"subtest": "subtest2"})])
|
||||||
|
|
||||||
JobNote.objects.create(job=test_job_2,
|
JobNote.objects.create(job=test_job_2,
|
||||||
failure_classification_id=4,
|
failure_classification_id=4,
|
||||||
|
@ -214,9 +205,9 @@ def test_classify_skip_ignore(test_job_2,
|
||||||
failure_lines[1].best_classification = None
|
failure_lines[1].best_classification = None
|
||||||
failure_lines[1].save()
|
failure_lines[1].save()
|
||||||
|
|
||||||
test_failure_lines = create_failure_lines(test_job_2,
|
test_error_lines, test_failure_lines = create_lines(test_job_2,
|
||||||
[(test_line, {}),
|
[(test_line, {}),
|
||||||
(test_line, {"subtest": "subtest2"})])
|
(test_line, {"subtest": "subtest2"})])
|
||||||
|
|
||||||
do_autoclassify(test_job_2, test_failure_lines, [PreciseTestMatcher])
|
do_autoclassify(test_job_2, test_failure_lines, [PreciseTestMatcher])
|
||||||
|
|
||||||
|
@ -231,14 +222,14 @@ def test_classify_skip_ignore(test_job_2,
|
||||||
|
|
||||||
|
|
||||||
def test_classify_es(test_job_2, failure_lines, classified_failures):
|
def test_classify_es(test_job_2, failure_lines, classified_failures):
|
||||||
test_failure_lines = create_failure_lines(test_job_2,
|
test_error_lines, test_failure_lines = create_lines(test_job_2,
|
||||||
[(test_line, {}),
|
[(test_line, {}),
|
||||||
(test_line, {"message": "message2"}),
|
(test_line, {"message": "message2"}),
|
||||||
(test_line, {"message": "message 1.2"}),
|
(test_line, {"message": "message 1.2"}),
|
||||||
(test_line, {"message": "message 0x1F"}),
|
(test_line, {"message": "message 0x1F"}),
|
||||||
(test_line, {"subtest": "subtest3"}),
|
(test_line, {"subtest": "subtest3"}),
|
||||||
(test_line, {"status": "TIMEOUT"}),
|
(test_line, {"status": "TIMEOUT"}),
|
||||||
(test_line, {"expected": "ERROR"})])
|
(test_line, {"expected": "ERROR"})])
|
||||||
|
|
||||||
do_autoclassify(test_job_2, test_failure_lines, [ElasticSearchTestMatcher])
|
do_autoclassify(test_job_2, test_failure_lines, [ElasticSearchTestMatcher])
|
||||||
|
|
||||||
|
@ -253,9 +244,9 @@ def test_classify_es(test_job_2, failure_lines, classified_failures):
|
||||||
|
|
||||||
|
|
||||||
def test_classify_multiple(test_job_2, failure_lines, classified_failures):
|
def test_classify_multiple(test_job_2, failure_lines, classified_failures):
|
||||||
test_failure_lines = create_failure_lines(test_job_2,
|
test_error_lines, test_failure_lines = create_lines(test_job_2,
|
||||||
[(test_line, {}),
|
[(test_line, {}),
|
||||||
(test_line, {"message": "message 1.2"})])
|
(test_line, {"message": "message 1.2"})])
|
||||||
|
|
||||||
expected_classified_precise = [test_failure_lines[0]]
|
expected_classified_precise = [test_failure_lines[0]]
|
||||||
expected_classified_fuzzy = [test_failure_lines[1]]
|
expected_classified_fuzzy = [test_failure_lines[1]]
|
||||||
|
@ -273,20 +264,24 @@ def test_classify_multiple(test_job_2, failure_lines, classified_failures):
|
||||||
|
|
||||||
|
|
||||||
def test_classify_crash(test_repository, test_job, test_job_2, test_matcher):
|
def test_classify_crash(test_repository, test_job, test_job_2, test_matcher):
|
||||||
failure_lines_ref = create_failure_lines(test_job,
|
error_lines_ref, failure_lines_ref = create_lines(test_job,
|
||||||
[(crash_line, {})])
|
[(crash_line, {})])
|
||||||
|
|
||||||
failure_lines = create_failure_lines(test_job_2,
|
error_lines, failure_lines = create_lines(test_job_2,
|
||||||
[(crash_line, {}),
|
[(crash_line, {}),
|
||||||
(crash_line, {"test": "test1"}),
|
(crash_line, {"test": "test1"}),
|
||||||
(crash_line, {"signature": "signature1"}),
|
(crash_line, {"signature": "signature1"}),
|
||||||
(crash_line, {"signature": None})])
|
(crash_line, {"signature": None})])
|
||||||
|
|
||||||
classified_failure = ClassifiedFailure.objects.create()
|
classified_failure = ClassifiedFailure.objects.create()
|
||||||
FailureMatch.objects.create(failure_line=failure_lines_ref[0],
|
FailureMatch.objects.create(failure_line=failure_lines_ref[0],
|
||||||
classified_failure=classified_failure,
|
classified_failure=classified_failure,
|
||||||
matcher=test_matcher.db_object,
|
matcher=test_matcher.db_object,
|
||||||
score=1.0)
|
score=1.0)
|
||||||
|
TextLogErrorMatch.objects.create(text_log_error=error_lines_ref[0],
|
||||||
|
classified_failure=classified_failure,
|
||||||
|
matcher=test_matcher.db_object,
|
||||||
|
score=1.0)
|
||||||
do_autoclassify(test_job_2, failure_lines, [CrashSignatureMatcher])
|
do_autoclassify(test_job_2, failure_lines, [CrashSignatureMatcher])
|
||||||
|
|
||||||
expected_classified = failure_lines[0:2]
|
expected_classified = failure_lines[0:2]
|
||||||
|
@ -297,22 +292,3 @@ def test_classify_crash(test_repository, test_job, test_job_2, test_matcher):
|
||||||
|
|
||||||
for item in expected_unclassified:
|
for item in expected_unclassified:
|
||||||
assert item.classified_failures.count() == 0
|
assert item.classified_failures.count() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_classify_test_failure_window(failure_lines, classified_failures):
|
|
||||||
failure_lines[0].created = datetime.now() - timedelta(days=2)
|
|
||||||
failure_lines[0].save()
|
|
||||||
|
|
||||||
failure_matches = FailureMatch.objects.all()
|
|
||||||
failure_matches[1].score = 0.5
|
|
||||||
failure_matches[1].save()
|
|
||||||
|
|
||||||
best_match = time_window(FailureMatch.objects.all(), timedelta(days=1), 0,
|
|
||||||
lambda x: x.score)
|
|
||||||
|
|
||||||
assert best_match == failure_matches[1]
|
|
||||||
|
|
||||||
best_match = time_window(FailureMatch.objects.all(), timedelta(days=1), None,
|
|
||||||
lambda x: x.score)
|
|
||||||
|
|
||||||
assert best_match == failure_matches[1]
|
|
||||||
|
|
|
@ -3,15 +3,31 @@ import datetime
|
||||||
from mozlog.formatters.tbplformatter import TbplFormatter
|
from mozlog.formatters.tbplformatter import TbplFormatter
|
||||||
|
|
||||||
from treeherder.model.models import (FailureLine,
|
from treeherder.model.models import (FailureLine,
|
||||||
|
Job,
|
||||||
MatcherManager,
|
MatcherManager,
|
||||||
TextLogError,
|
TextLogError,
|
||||||
|
TextLogErrorMetadata,
|
||||||
TextLogStep)
|
TextLogStep)
|
||||||
from treeherder.model.search import refresh_all
|
from treeherder.model.search import refresh_all
|
||||||
|
|
||||||
test_line = {"action": "test_result", "test": "test1", "subtest": "subtest1",
|
test_line = {"action": "test_result", "test": "test1", "subtest": "subtest1",
|
||||||
"status": "FAIL", "expected": "PASS", "message": "message1"}
|
"status": "FAIL", "expected": "PASS", "message": "message1"}
|
||||||
log_line = {"action": "log", "level": "ERROR", "message": "message1"}
|
log_line = {"action": "log", "level": "ERROR", "message": "message1"}
|
||||||
crash_line = {"action": "crash", "signature": "signature"}
|
crash_line = {"action": "crash", "signature": "signature", "test": "test1"}
|
||||||
|
|
||||||
|
|
||||||
|
def create_lines(test_job, lines):
|
||||||
|
error_lines = create_text_log_errors(test_job, lines)
|
||||||
|
failure_lines = create_failure_lines(test_job, lines)
|
||||||
|
|
||||||
|
for error_line, failure_line in zip(error_lines, failure_lines):
|
||||||
|
TextLogErrorMetadata.objects.create(text_log_error=error_line,
|
||||||
|
failure_line=failure_line)
|
||||||
|
|
||||||
|
test_job.autoclassify_status = Job.CROSSREFERENCED
|
||||||
|
test_job.save()
|
||||||
|
|
||||||
|
return error_lines, failure_lines
|
||||||
|
|
||||||
|
|
||||||
def create_failure_lines(job, failure_line_list,
|
def create_failure_lines(job, failure_line_list,
|
||||||
|
@ -46,6 +62,8 @@ def get_data(base_data, updates):
|
||||||
elif data["action"] == "log":
|
elif data["action"] == "log":
|
||||||
if data["level"] not in ("ERROR", "CRITICAL"):
|
if data["level"] not in ("ERROR", "CRITICAL"):
|
||||||
return
|
return
|
||||||
|
elif data["action"] == "crash":
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -52,13 +52,12 @@ def test_update_autoclassification_bug(test_job, test_job_2,
|
||||||
# Job 1 has two failure lines so nothing should be updated
|
# Job 1 has two failure lines so nothing should be updated
|
||||||
assert test_job.update_autoclassification_bug(1234) is None
|
assert test_job.update_autoclassification_bug(1234) is None
|
||||||
|
|
||||||
failure_lines = create_failure_lines(test_job_2,
|
lines = [(test_line, {})]
|
||||||
[(test_line, {})])
|
create_failure_lines(test_job_2, lines)
|
||||||
failure_lines[0].best_classification = classified_failures[0]
|
error_lines = create_text_log_errors(test_job_2, lines)
|
||||||
failure_lines[0].save()
|
|
||||||
classified_failures[0].bug_number = None
|
error_lines[0].mark_best_classification(classified_failures[0])
|
||||||
lines = [(item, {}) for item in FailureLine.objects.filter(job_guid=test_job_2.guid).values()]
|
assert classified_failures[0].bug_number is None
|
||||||
create_text_log_errors(test_job_2, lines)
|
|
||||||
|
|
||||||
assert test_job_2.update_autoclassification_bug(1234) == classified_failures[0]
|
assert test_job_2.update_autoclassification_bug(1234) == classified_failures[0]
|
||||||
classified_failures[0].refresh_from_db()
|
classified_failures[0].refresh_from_db()
|
||||||
|
|
|
@ -433,7 +433,9 @@ def test_text_log_steps_and_errors(webapp, test_job):
|
||||||
'search_terms': ['failure 1'],
|
'search_terms': ['failure 1'],
|
||||||
'bugs': {'open_recent': [], 'all_others': []}
|
'bugs': {'open_recent': [], 'all_others': []}
|
||||||
},
|
},
|
||||||
'metadata': None
|
'metadata': None,
|
||||||
|
'matches': [],
|
||||||
|
'classified_failures': []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': 2,
|
'id': 2,
|
||||||
|
@ -444,7 +446,9 @@ def test_text_log_steps_and_errors(webapp, test_job):
|
||||||
'search_terms': ['failure 2'],
|
'search_terms': ['failure 2'],
|
||||||
'bugs': {'open_recent': [], 'all_others': []}
|
'bugs': {'open_recent': [], 'all_others': []}
|
||||||
},
|
},
|
||||||
'metadata': None
|
'metadata': None,
|
||||||
|
'matches': [],
|
||||||
|
'classified_failures': []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'finished': '1970-01-01T00:03:20',
|
'finished': '1970-01-01T00:03:20',
|
||||||
|
@ -494,7 +498,9 @@ def test_text_log_errors(webapp, test_job):
|
||||||
'search_terms': ['failure 1'],
|
'search_terms': ['failure 1'],
|
||||||
'bugs': {'open_recent': [], 'all_others': []}
|
'bugs': {'open_recent': [], 'all_others': []}
|
||||||
},
|
},
|
||||||
'metadata': None
|
'metadata': None,
|
||||||
|
'matches': [],
|
||||||
|
'classified_failures': []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': 2,
|
'id': 2,
|
||||||
|
@ -505,7 +511,9 @@ def test_text_log_errors(webapp, test_job):
|
||||||
'search_terms': ['failure 2'],
|
'search_terms': ['failure 2'],
|
||||||
'bugs': {'open_recent': [], 'all_others': []}
|
'bugs': {'open_recent': [], 'all_others': []}
|
||||||
},
|
},
|
||||||
'metadata': None
|
'metadata': None,
|
||||||
|
'matches': [],
|
||||||
|
'classified_failures': []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,628 @@
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from tests.autoclassify.utils import (create_failure_lines,
|
||||||
|
create_text_log_errors,
|
||||||
|
test_line)
|
||||||
|
from treeherder.autoclassify.detectors import ManualDetector
|
||||||
|
from treeherder.model.models import (BugJobMap,
|
||||||
|
ClassifiedFailure,
|
||||||
|
FailureLine,
|
||||||
|
Job,
|
||||||
|
JobNote,
|
||||||
|
Matcher,
|
||||||
|
MatcherManager,
|
||||||
|
TextLogError,
|
||||||
|
TextLogErrorMetadata)
|
||||||
|
from treeherder.model.search import TestFailureLine
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_error(text_log_errors_failure_lines):
|
||||||
|
"""
|
||||||
|
test getting a single failure line
|
||||||
|
"""
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
resp = client.get(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": text_log_errors[0].id}))
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
assert isinstance(data, object)
|
||||||
|
exp_error_keys = ["id", "line", "line_number", "matches",
|
||||||
|
"classified_failures", "bug_suggestions", "metadata"]
|
||||||
|
|
||||||
|
assert set(data.keys()) == set(exp_error_keys)
|
||||||
|
|
||||||
|
exp_meta_keys = ["text_log_error", "failure_line", "best_classification",
|
||||||
|
"best_is_verified"]
|
||||||
|
assert set(data["metadata"].keys()) == set(exp_meta_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_verify(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"best_classification": classified_failures[0].id}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
|
||||||
|
es_line = TestFailureLine.get(failure_line.id, routing=failure_line.test)
|
||||||
|
assert es_line.best_classification == classified_failures[0].id
|
||||||
|
assert es_line.best_is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_replace(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"best_classification": classified_failures[1].id}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[1]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert len(failure_line.classified_failures.all()) == 2
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[1]
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
|
||||||
|
expected_matcher = Matcher.objects.get(name="ManualDetector")
|
||||||
|
assert failure_line.matches.get(classified_failure_id=classified_failures[1].id).matcher == expected_matcher
|
||||||
|
assert error_line.matches.get(classified_failure_id=classified_failures[1].id).matcher == expected_matcher
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_mark_job(test_job,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
classified_failures[1].bug_number = 1234
|
||||||
|
classified_failures[1].save()
|
||||||
|
|
||||||
|
for text_log_error, failure_line in zip(text_log_errors, failure_lines):
|
||||||
|
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert text_log_error.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"best_classification": classified_failures[1].id}
|
||||||
|
|
||||||
|
resp = client.put(reverse("text-log-error-detail", kwargs={"pk": text_log_error.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
text_log_error.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[1]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert text_log_error.metadata.best_classification == classified_failures[1]
|
||||||
|
assert text_log_error.metadata.best_is_verified
|
||||||
|
|
||||||
|
assert test_job.is_fully_verified()
|
||||||
|
|
||||||
|
# should only be one, will assert if that isn't the case
|
||||||
|
note = JobNote.objects.get(job=test_job)
|
||||||
|
assert note.failure_classification.id == 4
|
||||||
|
assert note.user == test_user
|
||||||
|
job_bugs = BugJobMap.objects.filter(job=test_job)
|
||||||
|
assert job_bugs.count() == 1
|
||||||
|
assert job_bugs[0].bug_id == 1234
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_mark_job_with_human_note(test_job,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures, test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
JobNote.objects.create(job=test_job,
|
||||||
|
failure_classification_id=4,
|
||||||
|
user=test_user,
|
||||||
|
text="note")
|
||||||
|
|
||||||
|
for error_line in text_log_errors:
|
||||||
|
|
||||||
|
body = {"best_classification": classified_failures[1].id}
|
||||||
|
|
||||||
|
resp = client.put(reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
assert test_job.is_fully_verified()
|
||||||
|
|
||||||
|
# should only be one, will assert if that isn't the case
|
||||||
|
note = JobNote.objects.get(job=test_job)
|
||||||
|
assert note.failure_classification.id == 4
|
||||||
|
assert note.user == test_user
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_line_mark_job_with_auto_note(test_job,
|
||||||
|
mock_autoclassify_jobs_true,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
JobNote.objects.create(job=test_job,
|
||||||
|
failure_classification_id=7,
|
||||||
|
text="note")
|
||||||
|
|
||||||
|
for text_log_error in text_log_errors:
|
||||||
|
body = {"best_classification": classified_failures[1].id}
|
||||||
|
|
||||||
|
resp = client.put(reverse("text-log-error-detail", kwargs={"pk": text_log_error.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
assert test_job.is_fully_verified()
|
||||||
|
|
||||||
|
notes = JobNote.objects.filter(job=test_job).order_by('-created')
|
||||||
|
assert notes.count() == 2
|
||||||
|
|
||||||
|
assert notes[0].failure_classification.id == 4
|
||||||
|
assert notes[0].user == test_user
|
||||||
|
assert notes[0].text == ''
|
||||||
|
|
||||||
|
assert notes[1].failure_classification.id == 7
|
||||||
|
assert not notes[1].user
|
||||||
|
assert notes[1].text == "note"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_errors(mock_autoclassify_jobs_true,
|
||||||
|
test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
eleven_jobs_stored,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
jobs = (Job.objects.get(id=1), Job.objects.get(id=2))
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
lines = [(test_line, {}),
|
||||||
|
(test_line, {"subtest": "subtest2"})]
|
||||||
|
new_failure_lines = create_failure_lines(jobs[1], lines)
|
||||||
|
new_text_log_errors = create_text_log_errors(jobs[1], lines)
|
||||||
|
|
||||||
|
for text_log_error, failure_line in zip(new_text_log_errors,
|
||||||
|
new_failure_lines):
|
||||||
|
TextLogErrorMetadata.objects.create(text_log_error=text_log_error,
|
||||||
|
failure_line=failure_line)
|
||||||
|
|
||||||
|
failure_lines = FailureLine.objects.filter(
|
||||||
|
job_guid__in=[job.guid for job in jobs]).all()
|
||||||
|
text_log_errors = TextLogError.objects.filter(
|
||||||
|
step__job__in=jobs).all()
|
||||||
|
|
||||||
|
for text_log_error, failure_line in zip(text_log_errors, failure_lines):
|
||||||
|
assert text_log_error.metadata.best_is_verified is False
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
|
||||||
|
body = [{"id": failure_line.id,
|
||||||
|
"best_classification": classified_failures[1].id}
|
||||||
|
for failure_line in failure_lines]
|
||||||
|
resp = client.put(reverse("text-log-error-list"), body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
for text_log_error, failure_line in zip(text_log_errors, failure_lines):
|
||||||
|
text_log_error.metadata.refresh_from_db()
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
assert failure_line.best_classification == classified_failures[1]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert text_log_error.metadata.best_classification == classified_failures[1]
|
||||||
|
assert text_log_error.metadata.best_is_verified
|
||||||
|
|
||||||
|
for job in jobs:
|
||||||
|
assert job.is_fully_verified()
|
||||||
|
|
||||||
|
# will assert if we don't have exactly one job, which is what we want
|
||||||
|
note = JobNote.objects.get(job=job)
|
||||||
|
assert note.failure_classification.id == 4
|
||||||
|
assert note.user == test_user
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_ignore(test_job, text_log_errors_failure_lines,
|
||||||
|
classified_failures, test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_error = text_log_errors[0]
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
assert text_log_error.metadata.best_classification == classified_failures[0]
|
||||||
|
assert text_log_error.metadata.best_is_verified is False
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"project": test_job.repository.name,
|
||||||
|
"best_classification": None}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": text_log_error.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
text_log_error.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification is None
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert text_log_error.metadata.best_classification is None
|
||||||
|
assert text_log_error.metadata.best_is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_all_ignore_mark_job(test_job,
|
||||||
|
mock_autoclassify_jobs_true,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
job_failure_lines = [line for line in failure_lines if
|
||||||
|
line.job_guid == test_job.guid]
|
||||||
|
job_text_log_errors = [error for error in text_log_errors if
|
||||||
|
error.step.job == test_job]
|
||||||
|
|
||||||
|
for error_line, failure_line in zip(job_text_log_errors, job_failure_lines):
|
||||||
|
error_line.best_is_verified = False
|
||||||
|
error_line.best_classification = None
|
||||||
|
failure_line.best_is_verified = False
|
||||||
|
failure_line.best_classification = None
|
||||||
|
|
||||||
|
assert JobNote.objects.count() == 0
|
||||||
|
|
||||||
|
for error_line, failure_line in zip(job_text_log_errors, job_failure_lines):
|
||||||
|
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"best_classification": None}
|
||||||
|
|
||||||
|
resp = client.put(reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
|
||||||
|
assert error_line.metadata.best_classification is None
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
assert failure_line.best_classification is None
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
|
||||||
|
assert test_job.is_fully_verified()
|
||||||
|
|
||||||
|
assert JobNote.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_partial_ignore_mark_job(test_job,
|
||||||
|
mock_autoclassify_jobs_true,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
for i, (error_line, failure_line) in enumerate(zip(text_log_errors, failure_lines)):
|
||||||
|
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
|
||||||
|
body = {"best_classification": None if i == 0 else classified_failures[0].id}
|
||||||
|
|
||||||
|
resp = client.put(reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
assert error_line.metadata.best_classification is None
|
||||||
|
assert failure_line.best_classification is None
|
||||||
|
else:
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
|
||||||
|
assert test_job.is_fully_verified()
|
||||||
|
|
||||||
|
# will assert if we don't have exactly one note for this job, which is
|
||||||
|
# what we want
|
||||||
|
note = JobNote.objects.get(job=test_job)
|
||||||
|
assert note.failure_classification.id == 4
|
||||||
|
assert note.user == test_user
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_verify_bug(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
classified_failures[0].bug_number = 1234
|
||||||
|
classified_failures[0].save()
|
||||||
|
|
||||||
|
body = {"bug_number": classified_failures[0].bug_number}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
|
||||||
|
es_line = TestFailureLine.get(failure_line.id, routing=failure_line.test)
|
||||||
|
assert es_line.best_classification == classified_failures[0].id
|
||||||
|
assert es_line.best_is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_verify_new_bug(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
assert 78910 not in [item.bug_number for item in classified_failures]
|
||||||
|
body = {"bug_number": 78910}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification not in classified_failures
|
||||||
|
assert failure_line.best_classification.bug_number == 78910
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification not in classified_failures
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification.bug_number == 78910
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_verify_ignore_now(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
assert 78910 not in [item.bug_number for item in classified_failures]
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification is None
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification is None
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_change_bug(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
assert 78910 not in [item.bug_number for item in classified_failures]
|
||||||
|
body = {"best_classification": classified_failures[0].id,
|
||||||
|
"bug_number": 78910}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
classified_failures[0].refresh_from_db()
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_classification.bug_number == 78910
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification.bug_number == 78910
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error_bug_change_cf(test_repository,
|
||||||
|
text_log_errors_failure_lines,
|
||||||
|
classified_failures,
|
||||||
|
test_user):
|
||||||
|
|
||||||
|
MatcherManager.register_detector(ManualDetector)
|
||||||
|
|
||||||
|
text_log_errors, failure_lines = text_log_errors_failure_lines
|
||||||
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
failure_line = failure_lines[0]
|
||||||
|
error_line = text_log_errors[0]
|
||||||
|
assert failure_line.best_classification == classified_failures[0]
|
||||||
|
assert failure_line.best_is_verified is False
|
||||||
|
assert error_line.metadata.failure_line == failure_line
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[0]
|
||||||
|
assert error_line.metadata.best_is_verified is False
|
||||||
|
|
||||||
|
assert 78910 not in [item.bug_number for item in classified_failures]
|
||||||
|
classified_failures[1].bug_number = 78910
|
||||||
|
classified_failures[1].save()
|
||||||
|
|
||||||
|
body = {"best_classification": classified_failures[0].id,
|
||||||
|
"bug_number": 78910}
|
||||||
|
|
||||||
|
resp = client.put(
|
||||||
|
reverse("text-log-error-detail", kwargs={"pk": error_line.id}),
|
||||||
|
body, format="json")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
classified_failures[1].refresh_from_db()
|
||||||
|
failure_line.refresh_from_db()
|
||||||
|
error_line.metadata.refresh_from_db()
|
||||||
|
|
||||||
|
assert failure_line.best_classification == classified_failures[1]
|
||||||
|
assert failure_line.best_classification.bug_number == 78910
|
||||||
|
assert failure_line.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification == classified_failures[1]
|
||||||
|
assert error_line.metadata.best_is_verified
|
||||||
|
assert error_line.metadata.best_classification.bug_number == 78910
|
||||||
|
assert ClassifiedFailure.objects.count() == len(classified_failures) - 1
|
|
@ -4,6 +4,8 @@ from rest_framework.test import APIClient
|
||||||
from treeherder.model.models import (BugJobMap,
|
from treeherder.model.models import (BugJobMap,
|
||||||
FailureLine,
|
FailureLine,
|
||||||
JobNote,
|
JobNote,
|
||||||
|
TextLogError,
|
||||||
|
TextLogErrorMetadata,
|
||||||
TextLogSummary)
|
TextLogSummary)
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,6 +138,7 @@ def test_put_verify_job(webapp, test_repository, test_job, text_summary_lines, t
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(user=test_user)
|
client.force_authenticate(user=test_user)
|
||||||
|
|
||||||
|
TextLogErrorMetadata.objects.filter(text_log_error__step__job=test_job).update(best_is_verified=True)
|
||||||
FailureLine.objects.filter(job_guid=test_job.guid).update(best_is_verified=True)
|
FailureLine.objects.filter(job_guid=test_job.guid).update(best_is_verified=True)
|
||||||
|
|
||||||
text_summary_lines = TextLogSummary.objects.filter(job_guid=test_job.guid).get().lines.all()
|
text_summary_lines = TextLogSummary.objects.filter(job_guid=test_job.guid).get().lines.all()
|
||||||
|
|
|
@ -3,10 +3,12 @@ from collections import defaultdict
|
||||||
|
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from treeherder.model.models import (FailureLine,
|
from treeherder.model.models import (ClassifiedFailure,
|
||||||
FailureMatch,
|
FailureMatch,
|
||||||
|
Job,
|
||||||
JobNote,
|
JobNote,
|
||||||
Matcher,
|
Matcher,
|
||||||
|
TextLogError,
|
||||||
TextLogErrorMatch)
|
TextLogErrorMatch)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -21,66 +23,85 @@ def match_errors(job):
|
||||||
# Only try to autoclassify where we have a failure status; sometimes there can be
|
# Only try to autoclassify where we have a failure status; sometimes there can be
|
||||||
# error lines even in jobs marked as passing.
|
# error lines even in jobs marked as passing.
|
||||||
|
|
||||||
|
if job.autoclassify_status < Job.CROSSREFERENCED:
|
||||||
|
logger.error("Tried to autoclassify job %i without crossreferenced error lines" % job.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if job.autoclassify_status == Job.AUTOCLASSIFIED:
|
||||||
|
logger.error("Tried to autoclassify job %i which was already autoclassified" % job.id)
|
||||||
|
return
|
||||||
|
|
||||||
if job.result not in ["testfailed", "busted", "exception"]:
|
if job.result not in ["testfailed", "busted", "exception"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
unmatched_failures = set(FailureLine.objects.unmatched_for_job(job))
|
unmatched_errors = set(TextLogError.objects.unmatched_for_job(job))
|
||||||
|
|
||||||
if not unmatched_failures:
|
if not unmatched_errors:
|
||||||
|
logger.info("Skipping autoclassify of job %i because it has no unmatched errors" % job.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
matches, all_matched = find_matches(unmatched_failures)
|
try:
|
||||||
update_db(job, matches, all_matched)
|
matches, all_matched = find_matches(unmatched_errors)
|
||||||
|
update_db(job, matches, all_matched)
|
||||||
|
except:
|
||||||
|
logger.error("Autoclassification of job %s failed" % job.id)
|
||||||
|
job.autoclassify_status = Job.FAILED
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
logger.debug("Autoclassification of job %s suceeded" % job.id)
|
||||||
|
job.autoclassify_status = Job.AUTOCLASSIFIED
|
||||||
|
finally:
|
||||||
|
job.save(update_fields=['autoclassify_status'])
|
||||||
|
|
||||||
|
|
||||||
def find_matches(unmatched_failures):
|
def find_matches(unmatched_errors):
|
||||||
all_matches = set()
|
all_matches = set()
|
||||||
|
|
||||||
for matcher in Matcher.objects.registered_matchers():
|
for matcher in Matcher.objects.registered_matchers():
|
||||||
matches = matcher(unmatched_failures)
|
matches = matcher(unmatched_errors)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
logger.info("Matched failure %i with intermittent %i" %
|
logger.info("Matched error %i with intermittent %i" %
|
||||||
(match.failure_line.id, match.classified_failure.id))
|
(match.text_log_error.id, match.classified_failure_id))
|
||||||
all_matches.add((matcher.db_object, match))
|
all_matches.add((matcher.db_object, match))
|
||||||
if match.score >= AUTOCLASSIFY_GOOD_ENOUGH_RATIO:
|
if match.score >= AUTOCLASSIFY_GOOD_ENOUGH_RATIO:
|
||||||
unmatched_failures.remove(match.failure_line)
|
unmatched_errors.remove(match.text_log_error)
|
||||||
|
|
||||||
if not unmatched_failures:
|
if not unmatched_errors:
|
||||||
break
|
break
|
||||||
|
|
||||||
return all_matches, len(unmatched_failures) == 0
|
return all_matches, len(unmatched_errors) == 0
|
||||||
|
|
||||||
|
|
||||||
def update_db(job, matches, all_matched):
|
def update_db(job, matches, all_matched):
|
||||||
matches_by_failure_line = defaultdict(set)
|
matches_by_error = defaultdict(set)
|
||||||
for item in matches:
|
classified_failures = {item.id: item for item in
|
||||||
matches_by_failure_line[item[1].failure_line].add(item)
|
ClassifiedFailure.objects.filter(
|
||||||
|
id__in=[match.classified_failure_id for _, match in matches])}
|
||||||
|
for matcher, match in matches:
|
||||||
|
classified_failure = classified_failures[match.classified_failure_id]
|
||||||
|
matches_by_error[match.text_log_error].add((matcher, match, classified_failure))
|
||||||
|
|
||||||
for failure_line, matches in matches_by_failure_line.iteritems():
|
for text_log_error, matches in matches_by_error.iteritems():
|
||||||
for matcher, match in matches:
|
for (matcher, match, classified_failure) in matches:
|
||||||
try:
|
try:
|
||||||
FailureMatch.objects.create(
|
TextLogErrorMatch.objects.create(
|
||||||
score=match.score,
|
score=match.score,
|
||||||
matcher=matcher,
|
matcher=matcher,
|
||||||
classified_failure=match.classified_failure,
|
classified_failure=classified_failure,
|
||||||
failure_line=failure_line)
|
text_log_error=match.text_log_error)
|
||||||
if failure_line.error:
|
if match.text_log_error.metadata and match.text_log_error.metadata.failure_line:
|
||||||
TextLogErrorMatch.objects.create(
|
FailureMatch.objects.create(
|
||||||
score=match.score,
|
score=match.score,
|
||||||
matcher=matcher,
|
matcher=matcher,
|
||||||
classified_failure=match.classified_failure,
|
classified_failure=classified_failure,
|
||||||
text_log_error=failure_line.error)
|
failure_line=match.text_log_error.metadata.failure_line)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Tried to create duplicate match for failure line %i with matcher %i and classified_failure %i" %
|
"Tried to create duplicate match for TextLogError %i with matcher %i and classified_failure %i" %
|
||||||
(failure_line.id, matcher.id, match.classified_failure.id))
|
(text_log_error.id, matcher.id, classified_failure.id))
|
||||||
best_match = failure_line.best_automatic_match(AUTOCLASSIFY_CUTOFF_RATIO)
|
best_match = text_log_error.best_automatic_match(AUTOCLASSIFY_CUTOFF_RATIO)
|
||||||
if best_match:
|
if best_match:
|
||||||
failure_line.best_classification = best_match.classified_failure
|
text_log_error.mark_best_classification(classified_failure)
|
||||||
failure_line.save(update_fields=['best_classification'])
|
|
||||||
if failure_line.error:
|
|
||||||
failure_line.error.metadata.best_classification = best_match.classified_failure
|
|
||||||
failure_line.error.metadata.save(update_fields=['best_classification'])
|
|
||||||
|
|
||||||
if all_matched:
|
if all_matched:
|
||||||
if job.is_fully_autoclassified():
|
if job.is_fully_autoclassified():
|
||||||
|
|
|
@ -25,11 +25,14 @@ class Detector(object):
|
||||||
|
|
||||||
|
|
||||||
class TestFailureDetector(Detector):
|
class TestFailureDetector(Detector):
|
||||||
def __call__(self, failure_lines):
|
def __call__(self, text_log_errors):
|
||||||
rv = []
|
rv = []
|
||||||
for i, failure in enumerate(failure_lines):
|
with_failure_lines = [(i, item) for (i, item) in enumerate(text_log_errors)
|
||||||
if (failure.action == "test_result" and failure.test and failure.status
|
if item.metadata and item.metadata.failure_line]
|
||||||
and failure.expected):
|
for i, text_log_error in with_failure_lines:
|
||||||
|
failure = text_log_error.metadata.failure_line
|
||||||
|
if (failure.action == "test_result" and failure.test and failure.status and
|
||||||
|
failure.expected):
|
||||||
rv.append(i)
|
rv.append(i)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
@ -37,7 +40,7 @@ class TestFailureDetector(Detector):
|
||||||
class ManualDetector(Detector):
|
class ManualDetector(Detector):
|
||||||
"""Small hack; this ensures that there's a matcher object indicating that a match
|
"""Small hack; this ensures that there's a matcher object indicating that a match
|
||||||
was by manual association, but which never automatically matches any lines"""
|
was by manual association, but which never automatically matches any lines"""
|
||||||
def __call__(self, failure_lines):
|
def __call__(self, text_log_errors):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ import time
|
||||||
from abc import (ABCMeta,
|
from abc import (ABCMeta,
|
||||||
abstractmethod)
|
abstractmethod)
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import (datetime,
|
|
||||||
timedelta)
|
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -12,16 +10,15 @@ from django.db.models import Q
|
||||||
from elasticsearch_dsl.query import Match as ESMatch
|
from elasticsearch_dsl.query import Match as ESMatch
|
||||||
|
|
||||||
from treeherder.autoclassify.autoclassify import AUTOCLASSIFY_GOOD_ENOUGH_RATIO
|
from treeherder.autoclassify.autoclassify import AUTOCLASSIFY_GOOD_ENOUGH_RATIO
|
||||||
from treeherder.model.models import (ClassifiedFailure,
|
from treeherder.model.models import (MatcherManager,
|
||||||
FailureLine,
|
TextLogError,
|
||||||
FailureMatch,
|
TextLogErrorMatch)
|
||||||
MatcherManager)
|
|
||||||
from treeherder.model.search import (TestFailureLine,
|
from treeherder.model.search import (TestFailureLine,
|
||||||
es_connected)
|
es_connected)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Match = namedtuple('Match', ['failure_line', 'classified_failure', 'score'])
|
Match = namedtuple('Match', ['text_log_error', 'classified_failure_id', 'score'])
|
||||||
|
|
||||||
|
|
||||||
class Matcher(object):
|
class Matcher(object):
|
||||||
|
@ -37,83 +34,134 @@ class Matcher(object):
|
||||||
def __init__(self, db_object):
|
def __init__(self, db_object):
|
||||||
self.db_object = db_object
|
self.db_object = db_object
|
||||||
|
|
||||||
|
def __call__(self, text_log_errors):
|
||||||
|
rv = []
|
||||||
|
for text_log_error in text_log_errors:
|
||||||
|
match = self.match(text_log_error)
|
||||||
|
if match:
|
||||||
|
rv.append(match)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def match(self, text_log_error):
|
||||||
|
best_match = self.query_best(text_log_error)
|
||||||
|
if best_match:
|
||||||
|
classified_failure_id, score = best_match
|
||||||
|
logger.debug("Matched using %s" % self.__class__.__name__)
|
||||||
|
return Match(text_log_error,
|
||||||
|
classified_failure_id,
|
||||||
|
score)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __call__(self, failure_lines):
|
def query_best(self, text_log_error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
ignored_line = (Q(failure_line__best_classification=None) &
|
ignored_line = (Q(text_log_error___metadata__best_classification=None) &
|
||||||
Q(failure_line__best_is_verified=True))
|
Q(text_log_error___metadata__best_is_verified=True))
|
||||||
|
|
||||||
|
|
||||||
def time_window(queryset, interval, time_budget_ms, match_filter):
|
class id_window(object):
|
||||||
upper_cutoff = datetime.now()
|
def __init__(self, size, time_budget):
|
||||||
lower_cutoff = upper_cutoff - interval
|
self.size = size
|
||||||
matches = []
|
self.time_budget_ms = time_budget
|
||||||
|
|
||||||
time_budget = time_budget_ms / 1000. if time_budget_ms is not None else None
|
def __call__(self, f):
|
||||||
t0 = time.time()
|
outer = self
|
||||||
|
|
||||||
min_date = FailureLine.objects.order_by("id")[0].created
|
def inner(self, text_log_error):
|
||||||
|
queries = f(self, text_log_error)
|
||||||
|
if not queries:
|
||||||
|
return
|
||||||
|
|
||||||
count = 0
|
for item in queries:
|
||||||
while True:
|
if isinstance(item, tuple):
|
||||||
count += 1
|
query, score_multiplier = item
|
||||||
window_queryset = queryset.filter(
|
else:
|
||||||
failure_line__created__range=(lower_cutoff, upper_cutoff))
|
query = item
|
||||||
logger.debug("[time_window] Queryset: %s" % window_queryset.query)
|
score_multiplier = (1, 1)
|
||||||
match = window_queryset.first()
|
|
||||||
if match is not None:
|
result = outer.run(query, score_multiplier)
|
||||||
matches.append(match)
|
if result:
|
||||||
if match.score >= AUTOCLASSIFY_GOOD_ENOUGH_RATIO:
|
return result
|
||||||
|
inner.__name__ = f.__name__
|
||||||
|
inner.__doc__ = f.__doc__
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def run(self, query, score_multiplier):
|
||||||
|
matches = []
|
||||||
|
time_budget = self.time_budget_ms / 1000. if self.time_budget_ms is not None else None
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
upper_cutoff = (TextLogError.objects
|
||||||
|
.order_by('-id')
|
||||||
|
.values_list('id', flat=True)[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
while upper_cutoff > 0:
|
||||||
|
count += 1
|
||||||
|
lower_cutoff = max(upper_cutoff - self.size, 0)
|
||||||
|
window_queryset = query.filter(
|
||||||
|
text_log_error__id__range=(lower_cutoff, upper_cutoff))
|
||||||
|
logger.debug("[time_window] Queryset: %s" % window_queryset.query)
|
||||||
|
match = window_queryset.first()
|
||||||
|
if match is not None:
|
||||||
|
score = match.score * score_multiplier[0] / score_multiplier[1]
|
||||||
|
matches.append((match, score))
|
||||||
|
if score >= AUTOCLASSIFY_GOOD_ENOUGH_RATIO:
|
||||||
|
break
|
||||||
|
upper_cutoff -= self.size
|
||||||
|
|
||||||
|
if time_budget is not None and time.time() - t0 > time_budget:
|
||||||
|
# Putting the condition at the end of the loop ensures that we always
|
||||||
|
# run it once, which is useful for testing
|
||||||
break
|
break
|
||||||
upper_cutoff = lower_cutoff
|
|
||||||
lower_cutoff = upper_cutoff - interval
|
|
||||||
if upper_cutoff < min_date:
|
|
||||||
break
|
|
||||||
|
|
||||||
if time_budget_ms is not None and time.time() - t0 > time_budget:
|
logger.debug("[time_window] Used %i queries" % count)
|
||||||
# Putting the condition at the end of the loop ensures that we always
|
if matches:
|
||||||
# run it once, which is useful for testing
|
matches.sort(key=lambda x: (-x[1], -x[0].classified_failure_id))
|
||||||
break
|
best = matches[0]
|
||||||
|
return best[0].classified_failure_id, best[1]
|
||||||
|
|
||||||
logger.debug("[time_window] Used %i queries" % count)
|
return None
|
||||||
if matches:
|
|
||||||
matches.sort(key=match_filter)
|
|
||||||
return matches[0]
|
def with_failure_lines(f):
|
||||||
return None
|
def inner(self, text_log_errors):
|
||||||
|
with_failure_lines = [item for item in text_log_errors
|
||||||
|
if item.metadata and item.metadata.failure_line]
|
||||||
|
return f(self, with_failure_lines)
|
||||||
|
inner.__name__ = f.__name__
|
||||||
|
inner.__doc__ = f.__doc__
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
class PreciseTestMatcher(Matcher):
|
class PreciseTestMatcher(Matcher):
|
||||||
"""Matcher that looks for existing failures with identical tests and identical error
|
"""Matcher that looks for existing failures with identical tests and
|
||||||
message."""
|
identical error message."""
|
||||||
|
|
||||||
def __call__(self, failure_lines):
|
@with_failure_lines
|
||||||
rv = []
|
def __call__(self, text_log_errors):
|
||||||
for failure_line in failure_lines:
|
return super(PreciseTestMatcher, self).__call__(text_log_errors)
|
||||||
logger.debug("Looking for test match in failure %d" % failure_line.id)
|
|
||||||
|
|
||||||
if failure_line.action != "test_result" or failure_line.message is None:
|
@id_window(size=20000,
|
||||||
continue
|
time_budget=500)
|
||||||
|
def query_best(self, text_log_error):
|
||||||
|
failure_line = text_log_error.metadata.failure_line
|
||||||
|
logger.debug("Looking for test match in failure %d" % failure_line.id)
|
||||||
|
|
||||||
matching_failures = FailureMatch.objects.filter(
|
if failure_line.action != "test_result" or failure_line.message is None:
|
||||||
failure_line__action="test_result",
|
return
|
||||||
failure_line__test=failure_line.test,
|
|
||||||
failure_line__subtest=failure_line.subtest,
|
|
||||||
failure_line__status=failure_line.status,
|
|
||||||
failure_line__expected=failure_line.expected,
|
|
||||||
failure_line__message=failure_line.message).exclude(
|
|
||||||
ignored_line | Q(failure_line__job_guid=failure_line.job_guid)
|
|
||||||
).order_by("-score", "-classified_failure")
|
|
||||||
|
|
||||||
best_match = time_window(matching_failures, timedelta(days=7), 500,
|
return [(TextLogErrorMatch.objects
|
||||||
lambda x: (-x.score, -x.classified_failure_id))
|
.filter(text_log_error___metadata__failure_line__action="test_result",
|
||||||
if best_match:
|
text_log_error___metadata__failure_line__test=failure_line.test,
|
||||||
logger.debug("Matched using precise test matcher")
|
text_log_error___metadata__failure_line__subtest=failure_line.subtest,
|
||||||
rv.append(Match(failure_line,
|
text_log_error___metadata__failure_line__status=failure_line.status,
|
||||||
best_match.classified_failure,
|
text_log_error___metadata__failure_line__expected=failure_line.expected,
|
||||||
best_match.score))
|
text_log_error___metadata__failure_line__message=failure_line.message)
|
||||||
return rv
|
.exclude(ignored_line |
|
||||||
|
Q(text_log_error__step__job=text_log_error.step.job))
|
||||||
|
.order_by("-score", "-classified_failure"))]
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchTestMatcher(Matcher):
|
class ElasticSearchTestMatcher(Matcher):
|
||||||
|
@ -126,79 +174,68 @@ class ElasticSearchTestMatcher(Matcher):
|
||||||
self.calls = 0
|
self.calls = 0
|
||||||
|
|
||||||
@es_connected(default=[])
|
@es_connected(default=[])
|
||||||
def __call__(self, failure_lines):
|
@with_failure_lines
|
||||||
rv = []
|
def __call__(self, text_log_errors):
|
||||||
self.lines += len(failure_lines)
|
return super(ElasticSearchTestMatcher, self).__call__(text_log_errors)
|
||||||
for failure_line in failure_lines:
|
|
||||||
if failure_line.action != "test_result" or not failure_line.message:
|
def query_best(self, text_log_error):
|
||||||
logger.debug("Skipped elasticsearch matching")
|
failure_line = text_log_error.metadata.failure_line
|
||||||
continue
|
if failure_line.action != "test_result" or not failure_line.message:
|
||||||
match = ESMatch(message={"query": failure_line.message[:1024],
|
logger.debug("Skipped elasticsearch matching")
|
||||||
"type": "phrase"})
|
return
|
||||||
search = (TestFailureLine.search()
|
match = ESMatch(message={"query": failure_line.message[:1024],
|
||||||
.filter("term", test=failure_line.test)
|
"type": "phrase"})
|
||||||
.filter("term", status=failure_line.status)
|
search = (TestFailureLine.search()
|
||||||
.filter("term", expected=failure_line.expected)
|
.filter("term", test=failure_line.test)
|
||||||
.filter("exists", field="best_classification")
|
.filter("term", status=failure_line.status)
|
||||||
.query(match))
|
.filter("term", expected=failure_line.expected)
|
||||||
if failure_line.subtest:
|
.filter("exists", field="best_classification")
|
||||||
search = search.filter("term", subtest=failure_line.subtest)
|
.query(match))
|
||||||
try:
|
if failure_line.subtest:
|
||||||
self.calls += 1
|
search = search.filter("term", subtest=failure_line.subtest)
|
||||||
resp = search.execute()
|
try:
|
||||||
except:
|
self.calls += 1
|
||||||
logger.error("Elastic search lookup failed: %s %s %s %s %s",
|
resp = search.execute()
|
||||||
failure_line.test, failure_line.subtest, failure_line.status,
|
except:
|
||||||
failure_line.expected, failure_line.message)
|
logger.error("Elastic search lookup failed: %s %s %s %s %s",
|
||||||
raise
|
failure_line.test, failure_line.subtest, failure_line.status,
|
||||||
scorer = MatchScorer(failure_line.message)
|
failure_line.expected, failure_line.message)
|
||||||
matches = [(item, item.message) for item in resp]
|
raise
|
||||||
best_match = scorer.best_match(matches)
|
scorer = MatchScorer(failure_line.message)
|
||||||
if best_match:
|
matches = [(item, item.message) for item in resp]
|
||||||
logger.debug("Matched using elastic search test matcher")
|
best_match = scorer.best_match(matches)
|
||||||
rv.append(Match(failure_line,
|
if best_match:
|
||||||
ClassifiedFailure.objects.get(
|
return (best_match[1].best_classification, best_match[0])
|
||||||
id=best_match[1].best_classification),
|
|
||||||
best_match[0]))
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class CrashSignatureMatcher(Matcher):
|
class CrashSignatureMatcher(Matcher):
|
||||||
"""Matcher that looks for crashes with identical signature"""
|
"""Matcher that looks for crashes with identical signature"""
|
||||||
|
|
||||||
def __call__(self, failure_lines):
|
@with_failure_lines
|
||||||
rv = []
|
def __call__(self, text_log_errors):
|
||||||
|
return super(CrashSignatureMatcher, self).__call__(text_log_errors)
|
||||||
|
|
||||||
for failure_line in failure_lines:
|
@id_window(size=20000,
|
||||||
if (failure_line.action != "crash" or failure_line.signature is None
|
time_budget=250)
|
||||||
or failure_line.signature == "None"):
|
def query_best(self, text_log_error):
|
||||||
continue
|
failure_line = text_log_error.metadata.failure_line
|
||||||
matching_failures = FailureMatch.objects.filter(
|
|
||||||
failure_line__action="crash",
|
|
||||||
failure_line__signature=failure_line.signature).exclude(
|
|
||||||
ignored_line | Q(failure_line__job_guid=failure_line.job_guid)
|
|
||||||
).select_related('failure_line').order_by(
|
|
||||||
"-score",
|
|
||||||
"-classified_failure")
|
|
||||||
|
|
||||||
score_multiplier = 10
|
if (failure_line.action != "crash" or
|
||||||
matching_failures_same_test = matching_failures.filter(
|
failure_line.signature is None or
|
||||||
failure_line__test=failure_line.test)
|
failure_line.signature == "None"):
|
||||||
|
return
|
||||||
|
|
||||||
best_match = time_window(matching_failures_same_test, timedelta(days=7), 250,
|
matching_failures = (TextLogErrorMatch.objects
|
||||||
lambda x: (-x.score, -x.classified_failure_id))
|
.filter(text_log_error___metadata__failure_line__action="crash",
|
||||||
if not best_match:
|
text_log_error___metadata__failure_line__signature=failure_line.signature)
|
||||||
score_multiplier = 8
|
.exclude(ignored_line |
|
||||||
best_match = time_window(matching_failures, timedelta(days=7), 250,
|
Q(text_log_error__step__job=text_log_error.step.job))
|
||||||
lambda x: (-x.score, -x.classified_failure_id))
|
.select_related('text_log_error',
|
||||||
|
'text_log_error___metadata')
|
||||||
|
.order_by("-score", "-classified_failure"))
|
||||||
|
|
||||||
if best_match:
|
return [matching_failures.filter(text_log_error___metadata__failure_line__test=failure_line.test),
|
||||||
logger.debug("Matched using crash signature matcher")
|
(matching_failures, (8, 10))]
|
||||||
score = best_match.score * score_multiplier / 10
|
|
||||||
rv.append(Match(failure_line,
|
|
||||||
best_match.classified_failure,
|
|
||||||
score))
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class MatchScorer(object):
|
class MatchScorer(object):
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.db import (IntegrityError,
|
||||||
from mozlog.formatters.tbplformatter import TbplFormatter
|
from mozlog.formatters.tbplformatter import TbplFormatter
|
||||||
|
|
||||||
from treeherder.model.models import (FailureLine,
|
from treeherder.model.models import (FailureLine,
|
||||||
|
Job,
|
||||||
TextLogError,
|
TextLogError,
|
||||||
TextLogErrorMetadata,
|
TextLogErrorMetadata,
|
||||||
TextLogSummary,
|
TextLogSummary,
|
||||||
|
@ -14,7 +15,6 @@ from treeherder.model.models import (FailureLine,
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def crossreference_job(job):
|
def crossreference_job(job):
|
||||||
"""Populate the TextLogSummary and TextLogSummaryLine tables for a
|
"""Populate the TextLogSummary and TextLogSummaryLine tables for a
|
||||||
job. Specifically this function tries to match the
|
job. Specifically this function tries to match the
|
||||||
|
@ -27,9 +27,21 @@ def crossreference_job(job):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _crossreference(job)
|
if job.autoclassify_status >= Job.CROSSREFERENCED:
|
||||||
|
logger.debug("Job %i already crossreferenced" % job.id)
|
||||||
|
return (TextLogError.objects
|
||||||
|
.filter(step__job=job)
|
||||||
|
.exists() and
|
||||||
|
FailureLine.objects
|
||||||
|
.filter(job_guid=job.guid)
|
||||||
|
.exists())
|
||||||
|
rv = _crossreference(job)
|
||||||
|
job.autoclassify_status = Job.CROSSREFERENCED
|
||||||
|
job.save(update_fields=['autoclassify_status'])
|
||||||
|
return rv
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
logger.warning("IntegrityError crossreferencing error lines for job %s" % job.id)
|
logger.warning("IntegrityError crossreferencing error lines for job %s" % job.id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -43,7 +55,7 @@ def _crossreference(job):
|
||||||
text_log_errors = TextLogError.objects.filter(
|
text_log_errors = TextLogError.objects.filter(
|
||||||
step__job=job).order_by('line_number')
|
step__job=job).order_by('line_number')
|
||||||
|
|
||||||
# If we don't have failure lines and text log errors nothing will happen
|
# If we don't have both failure lines and text log errors nothing will happen
|
||||||
# so return early
|
# so return early
|
||||||
if not (failure_lines.exists() and text_log_errors.exists()):
|
if not (failure_lines.exists() and text_log_errors.exists()):
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -96,5 +96,8 @@ def crossreference_error_lines(job_id, priority):
|
||||||
autoclassify.apply_async(
|
autoclassify.apply_async(
|
||||||
args=[job_id],
|
args=[job_id],
|
||||||
routing_key="autoclassify.%s" % priority)
|
routing_key="autoclassify.%s" % priority)
|
||||||
|
elif not settings.AUTOCLASSIFY_JOBS:
|
||||||
|
job.autoclassify_status = Job.SKIPPED
|
||||||
|
job.save(update_fields=['autoclassify_status'])
|
||||||
else:
|
else:
|
||||||
logger.debug("Job %i didn't have any crossreferenced lines, skipping autoclassify " % job_id)
|
logger.debug("Job %i didn't have any crossreferenced lines, skipping autoclassify " % job_id)
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
# Original SQL:
|
||||||
|
#
|
||||||
|
# BEGIN;
|
||||||
|
# --
|
||||||
|
# -- Add field autoclassify_status to job
|
||||||
|
# --
|
||||||
|
# ALTER TABLE `job` ADD COLUMN `autoclassify_status` integer DEFAULT 0 NOT NULL;
|
||||||
|
# ALTER TABLE `job` ALTER COLUMN `autoclassify_status` DROP DEFAULT;
|
||||||
|
# COMMIT;
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('model', '0004_duplicate_failure_classifications'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunSQL(
|
||||||
|
sql="""
|
||||||
|
--
|
||||||
|
-- Add field autoclassify_status to job
|
||||||
|
--
|
||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
ALTER TABLE `job` ADD COLUMN `autoclassify_status` integer DEFAULT 0 NOT NULL;
|
||||||
|
ALTER TABLE `job` ALTER COLUMN `autoclassify_status` DROP DEFAULT;
|
||||||
|
""",
|
||||||
|
reverse_sql="""
|
||||||
|
--
|
||||||
|
-- Add field autoclassify_status to job
|
||||||
|
--
|
||||||
|
ALTER TABLE `job` DROP COLUMN `autoclassify_status` CASCADE;
|
||||||
|
""",
|
||||||
|
state_operations=[
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='autoclassify_status',
|
||||||
|
field=models.IntegerField(default=0, choices=[(0, 'pending'), (1, 'crossreferenced'), (2, 'autoclassified'), (3, 'skipped'), (255, 'failed')]),
|
||||||
|
),
|
||||||
|
])]
|
|
@ -7,7 +7,6 @@ import time
|
||||||
from collections import (OrderedDict,
|
from collections import (OrderedDict,
|
||||||
defaultdict)
|
defaultdict)
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -667,6 +666,7 @@ class Job(models.Model):
|
||||||
repository = models.ForeignKey(Repository)
|
repository = models.ForeignKey(Repository)
|
||||||
guid = models.CharField(max_length=50, unique=True)
|
guid = models.CharField(max_length=50, unique=True)
|
||||||
project_specific_id = models.PositiveIntegerField(null=True)
|
project_specific_id = models.PositiveIntegerField(null=True)
|
||||||
|
autoclassify_status = models.IntegerField(choices=AUTOCLASSIFY_STATUSES, default=PENDING)
|
||||||
|
|
||||||
coalesced_to_guid = models.CharField(max_length=50, null=True,
|
coalesced_to_guid = models.CharField(max_length=50, null=True,
|
||||||
default=None)
|
default=None)
|
||||||
|
@ -736,45 +736,31 @@ class Job(models.Model):
|
||||||
action="truncated").count() > 0:
|
action="truncated").count() > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
classified_failure_lines_count = FailureLine.objects.filter(
|
classified_error_count = TextLogError.objects.filter(
|
||||||
best_classification__isnull=False,
|
_metadata__best_classification__isnull=False,
|
||||||
job_guid=self.guid).count()
|
step__job=self).count()
|
||||||
|
|
||||||
if classified_failure_lines_count == 0:
|
if classified_error_count == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
from treeherder.model.error_summary import get_filtered_error_lines
|
from treeherder.model.error_summary import get_filtered_error_lines
|
||||||
|
|
||||||
return classified_failure_lines_count == len(get_filtered_error_lines(self))
|
return classified_error_count == len(get_filtered_error_lines(self))
|
||||||
|
|
||||||
def is_fully_verified(self):
|
def is_fully_verified(self):
|
||||||
if FailureLine.objects.filter(job_guid=self.guid,
|
|
||||||
action="truncated").count() > 0:
|
|
||||||
logger.error("Job %s truncated storage of FailureLines" % self.guid)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Line is not fully verified if there are either structured failure lines
|
# Line is not fully verified if there are either structured failure lines
|
||||||
# with no best failure, or unverified unstructured lines not associated with
|
# with no best failure, or unverified unstructured lines not associated with
|
||||||
# a structured line
|
# a structured line
|
||||||
|
|
||||||
unverified_failure_lines = FailureLine.objects.filter(
|
unverified_errors = TextLogError.objects.filter(
|
||||||
best_is_verified=False,
|
_metadata__best_is_verified=False,
|
||||||
job_guid=self.guid).count()
|
step__job=self).count()
|
||||||
|
|
||||||
if unverified_failure_lines:
|
if unverified_errors:
|
||||||
logger.error("Job %s has unverified FailureLines" % self.guid)
|
logger.error("Job %r has unverified TextLogErrors" % self)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unverified_text_lines = TextLogSummaryLine.objects.filter(
|
logger.info("Job %r is fully verified" % self)
|
||||||
verified=False,
|
|
||||||
failure_line=None,
|
|
||||||
summary__job_guid=self.guid).count()
|
|
||||||
|
|
||||||
if unverified_text_lines:
|
|
||||||
logger.error("Job %s has unverified TextLogSummary" % self.guid)
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info("Job %s is fully verified" % self.guid)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_after_verification(self, user):
|
def update_after_verification(self, user):
|
||||||
|
@ -798,13 +784,13 @@ class Job(models.Model):
|
||||||
|
|
||||||
def get_manual_classification_line(self):
|
def get_manual_classification_line(self):
|
||||||
"""
|
"""
|
||||||
Return the FailureLine from a job if it can be manually classified as a side effect
|
Return the TextLogError from a job if it can be manually classified as a side effect
|
||||||
of the overall job being classified.
|
of the overall job being classified.
|
||||||
Otherwise return None.
|
Otherwise return None.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
failure_lines = [FailureLine.objects.get(job_guid=self.guid)]
|
text_log_error = TextLogError.objects.get(step__job=self)
|
||||||
except (FailureLine.DoesNotExist, FailureLine.MultipleObjectsReturned):
|
except (TextLogError.DoesNotExist, TextLogError.MultipleObjectsReturned):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Only propagate the classification if there is exactly one unstructured failure
|
# Only propagate the classification if there is exactly one unstructured failure
|
||||||
|
@ -815,19 +801,20 @@ class Job(models.Model):
|
||||||
|
|
||||||
# Check that some detector would match this. This is being used as an indication
|
# Check that some detector would match this. This is being used as an indication
|
||||||
# that the autoclassifier will be able to work on this classification
|
# that the autoclassifier will be able to work on this classification
|
||||||
if not any(detector(failure_lines)
|
if not any(detector([text_log_error])
|
||||||
for detector in Matcher.objects.registered_detectors()):
|
for detector in Matcher.objects.registered_detectors()):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return failure_lines[0]
|
return text_log_error
|
||||||
|
|
||||||
def update_autoclassification_bug(self, bug_number):
|
def update_autoclassification_bug(self, bug_number):
|
||||||
failure_line = self.get_manual_classification_line()
|
text_log_error = self.get_manual_classification_line()
|
||||||
|
|
||||||
if failure_line is None:
|
if text_log_error is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
classification = failure_line.best_classification
|
classification = (text_log_error.metadata.best_classification if text_log_error.metadata
|
||||||
|
else None)
|
||||||
if classification and classification.bug_number is None:
|
if classification and classification.bug_number is None:
|
||||||
return classification.set_bug(bug_number)
|
return classification.set_bug(bug_number)
|
||||||
|
|
||||||
|
@ -975,18 +962,18 @@ class JobNoteManager(models.Manager):
|
||||||
|
|
||||||
# Only insert bugs for verified failures since these are automatically
|
# Only insert bugs for verified failures since these are automatically
|
||||||
# mirrored to ES and the mirroring can't be undone
|
# mirrored to ES and the mirroring can't be undone
|
||||||
classified_failures = ClassifiedFailure.objects.filter(
|
bug_numbers = set(ClassifiedFailure.objects
|
||||||
best_for_lines__job_guid=job.guid,
|
.filter(best_for_errors__text_log_error__step__job=job,
|
||||||
best_for_lines__best_is_verified=True)
|
best_for_errors__best_is_verified=True)
|
||||||
|
.exclude(bug_number=None)
|
||||||
|
.values_list('bug_number', flat=True))
|
||||||
|
|
||||||
text_log_summary_lines = TextLogSummaryLine.objects.filter(
|
# Legacy
|
||||||
summary__job_guid=job.guid, verified=True).exclude(
|
bug_numbers |= set(TextLogSummaryLine.objects
|
||||||
bug_number=None)
|
.filter(summary__job_guid=job.guid,
|
||||||
|
verified=True)
|
||||||
bug_numbers = {item.bug_number
|
.exclude(bug_number=None)
|
||||||
for item in chain(classified_failures,
|
.values_list('bug_number', flat=True))
|
||||||
text_log_summary_lines)
|
|
||||||
if item.bug_number}
|
|
||||||
|
|
||||||
for bug_number in bug_numbers:
|
for bug_number in bug_numbers:
|
||||||
BugJobMap.objects.get_or_create(job=job,
|
BugJobMap.objects.get_or_create(job=job,
|
||||||
|
@ -1045,12 +1032,34 @@ class JobNote(models.Model):
|
||||||
self.job.save()
|
self.job.save()
|
||||||
|
|
||||||
# if a manually filed job, update the autoclassification information
|
# if a manually filed job, update the autoclassification information
|
||||||
if self.user:
|
if not self.user:
|
||||||
if self.failure_classification.name in [
|
return
|
||||||
"intermittent", "intermittent needs filing"]:
|
|
||||||
failure_line = self.job.get_manual_classification_line()
|
if self.failure_classification.name not in [
|
||||||
if failure_line:
|
"intermittent", "intermittent needs filing"]:
|
||||||
failure_line.update_autoclassification()
|
return
|
||||||
|
|
||||||
|
text_log_error = self.job.get_manual_classification_line()
|
||||||
|
if not text_log_error:
|
||||||
|
return
|
||||||
|
bug_numbers = set(BugJobMap.objects
|
||||||
|
.filter(job=self.job)
|
||||||
|
.values_list('bug_id', flat=True))
|
||||||
|
|
||||||
|
existing_bugs = set(ClassifiedFailure.objects
|
||||||
|
.filter(error_matches__text_log_error=text_log_error)
|
||||||
|
.values_list('bug_number', flat=True))
|
||||||
|
|
||||||
|
add_bugs = (bug_numbers - existing_bugs)
|
||||||
|
if not add_bugs:
|
||||||
|
return
|
||||||
|
|
||||||
|
manual_detector = Matcher.objects.get(name="ManualDetector")
|
||||||
|
for bug_number in add_bugs:
|
||||||
|
classification, _ = text_log_error.set_classification(manual_detector,
|
||||||
|
bug_number=bug_number)
|
||||||
|
if len(add_bugs) == 1 and not existing_bugs:
|
||||||
|
text_log_error.mark_best_classification_verified(classification)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super(JobNote, self).save(*args, **kwargs)
|
super(JobNote, self).save(*args, **kwargs)
|
||||||
|
@ -1067,24 +1076,6 @@ class JobNote(models.Model):
|
||||||
self.who)
|
self.who)
|
||||||
|
|
||||||
|
|
||||||
class FailureLineManager(models.Manager):
|
|
||||||
def unmatched_for_job(self, job):
|
|
||||||
return FailureLine.objects.filter(
|
|
||||||
job_guid=job.guid,
|
|
||||||
repository=job.repository,
|
|
||||||
classified_failures=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def for_jobs(self, *jobs, **filters):
|
|
||||||
failures = FailureLine.objects.filter(
|
|
||||||
job_guid__in=[item.guid for item in jobs],
|
|
||||||
**filters)
|
|
||||||
failures_by_job = defaultdict(list)
|
|
||||||
for item in failures:
|
|
||||||
failures_by_job[item.job_guid].append(item)
|
|
||||||
return failures_by_job
|
|
||||||
|
|
||||||
|
|
||||||
class FailureLine(models.Model):
|
class FailureLine(models.Model):
|
||||||
# We make use of prefix indicies for several columns in this table which
|
# We make use of prefix indicies for several columns in this table which
|
||||||
# can't be expressed in django syntax so are created with raw sql in migrations.
|
# can't be expressed in django syntax so are created with raw sql in migrations.
|
||||||
|
@ -1129,9 +1120,6 @@ class FailureLine(models.Model):
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
objects = FailureLineManager()
|
|
||||||
# TODO: add indexes once we know which queries will be typically executed
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'failure_line'
|
db_table = 'failure_line'
|
||||||
index_together = (
|
index_together = (
|
||||||
|
@ -1199,7 +1187,9 @@ class FailureLine(models.Model):
|
||||||
return classification, new_link
|
return classification, new_link
|
||||||
|
|
||||||
def mark_best_classification_verified(self, classification):
|
def mark_best_classification_verified(self, classification):
|
||||||
if classification not in self.classified_failures.all():
|
if (classification and
|
||||||
|
classification.id not in self.classified_failures.values_list('id', flat=True)):
|
||||||
|
logger.debug("Adding new classification to TextLogError")
|
||||||
manual_detector = Matcher.objects.get(name="ManualDetector")
|
manual_detector = Matcher.objects.get(name="ManualDetector")
|
||||||
self.set_classification(manual_detector, classification=classification)
|
self.set_classification(manual_detector, classification=classification)
|
||||||
|
|
||||||
|
@ -1302,23 +1292,26 @@ class ClassifiedFailure(models.Model):
|
||||||
# ON matches.classified_failure_id = <other.id> AND
|
# ON matches.classified_failure_id = <other.id> AND
|
||||||
# matches.failure_line_id = failure_match.failue_line_id
|
# matches.failure_line_id = failure_match.failue_line_id
|
||||||
delete_ids = []
|
delete_ids = []
|
||||||
for match in self.matches.all():
|
for Match, key, matches in [(TextLogErrorMatch, "text_log_error",
|
||||||
try:
|
self.error_matches.all()),
|
||||||
existing = FailureMatch.objects.get(classified_failure=other,
|
(FailureMatch, "failure_line",
|
||||||
failure_line=match.failure_line)
|
self.matches.all())]:
|
||||||
if match.score > existing.score:
|
for match in matches:
|
||||||
existing.score = match.score
|
kwargs = {key: getattr(match, key)}
|
||||||
existing.save()
|
existing = Match.objects.filter(classified_failure=other, **kwargs)
|
||||||
delete_ids.append(match.id)
|
if existing:
|
||||||
except FailureMatch.DoesNotExist:
|
for existing_match in existing:
|
||||||
match.classified_failure = other
|
if match.score > existing_match.score:
|
||||||
match.save()
|
existing_match.score = match.score
|
||||||
FailureMatch.objects.filter(id__in=delete_ids).delete()
|
existing_match.save()
|
||||||
|
delete_ids.append(match.id)
|
||||||
|
else:
|
||||||
|
match.classified_failure = other
|
||||||
|
match.save()
|
||||||
|
Match.objects.filter(id__in=delete_ids).delete()
|
||||||
FailureLine.objects.filter(best_classification=self).update(best_classification=other)
|
FailureLine.objects.filter(best_classification=self).update(best_classification=other)
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
# TODO: add indexes once we know which queries will be typically executed
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'classified_failure'
|
db_table = 'classified_failure'
|
||||||
|
|
||||||
|
@ -1510,6 +1503,32 @@ class TextLogStep(models.Model):
|
||||||
'finished_line_number')
|
'finished_line_number')
|
||||||
|
|
||||||
|
|
||||||
|
class TextLogErrorManager(models.Manager):
|
||||||
|
def unmatched_for_job(self, job):
|
||||||
|
"""Return a the text log errors for a specific job that have
|
||||||
|
no associated ClassifiedFailure.
|
||||||
|
|
||||||
|
:param job: Job associated with the text log errors"""
|
||||||
|
return TextLogError.objects.filter(
|
||||||
|
step__job=job,
|
||||||
|
classified_failures=None,
|
||||||
|
).prefetch_related('step', '_metadata', '_metadata__failure_line')
|
||||||
|
|
||||||
|
def for_jobs(self, *jobs, **filters):
|
||||||
|
"""Return a dict of {job: [text log errors]} for a set of jobs, filtered by
|
||||||
|
caller-provided django filters.
|
||||||
|
|
||||||
|
:param jobs: Jobs associated with the text log errors
|
||||||
|
:param filters: filters to apply to text log errors"""
|
||||||
|
error_lines = TextLogError.objects.filter(
|
||||||
|
step__job__in=jobs,
|
||||||
|
**filters)
|
||||||
|
lines_by_job = defaultdict(list)
|
||||||
|
for item in error_lines:
|
||||||
|
lines_by_job[item.step.job].append(item)
|
||||||
|
return lines_by_job
|
||||||
|
|
||||||
|
|
||||||
class TextLogError(models.Model):
|
class TextLogError(models.Model):
|
||||||
"""
|
"""
|
||||||
A detected error line in the textual (unstructured) log
|
A detected error line in the textual (unstructured) log
|
||||||
|
@ -1520,6 +1539,8 @@ class TextLogError(models.Model):
|
||||||
line = models.TextField()
|
line = models.TextField()
|
||||||
line_number = models.PositiveIntegerField()
|
line_number = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
objects = TextLogErrorManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "text_log_error"
|
db_table = "text_log_error"
|
||||||
unique_together = ('step', 'line_number')
|
unique_together = ('step', 'line_number')
|
||||||
|
@ -1538,6 +1559,91 @@ class TextLogError(models.Model):
|
||||||
from treeherder.model import error_summary
|
from treeherder.model import error_summary
|
||||||
return error_summary.bug_suggestions_line(self)
|
return error_summary.bug_suggestions_line(self)
|
||||||
|
|
||||||
|
def best_automatic_match(self, min_score=0):
|
||||||
|
return (TextLogErrorMatch.objects
|
||||||
|
.filter(text_log_error__id=self.id,
|
||||||
|
score__gt=min_score)
|
||||||
|
.order_by("-score",
|
||||||
|
"-classified_failure_id")
|
||||||
|
.select_related('classified_failure')
|
||||||
|
.first())
|
||||||
|
|
||||||
|
def set_classification(self, matcher, classification=None, bug_number=None,
|
||||||
|
mark_best=False):
|
||||||
|
with transaction.atomic():
|
||||||
|
if classification is None:
|
||||||
|
if bug_number:
|
||||||
|
classification, _ = ClassifiedFailure.objects.get_or_create(
|
||||||
|
bug_number=bug_number)
|
||||||
|
else:
|
||||||
|
classification = ClassifiedFailure.objects.create()
|
||||||
|
|
||||||
|
new_link = TextLogErrorMatch(
|
||||||
|
text_log_error=self,
|
||||||
|
classified_failure=classification,
|
||||||
|
matcher=matcher,
|
||||||
|
score=1)
|
||||||
|
new_link.save()
|
||||||
|
|
||||||
|
if self.metadata and self.metadata.failure_line:
|
||||||
|
new_link_failure = FailureMatch(
|
||||||
|
failure_line=self.metadata.failure_line,
|
||||||
|
classified_failure=classification,
|
||||||
|
matcher=matcher,
|
||||||
|
score=1)
|
||||||
|
new_link_failure.save()
|
||||||
|
|
||||||
|
if mark_best:
|
||||||
|
self.mark_best_classification(classification)
|
||||||
|
|
||||||
|
return classification, new_link
|
||||||
|
|
||||||
|
def mark_best_classification(self, classification):
|
||||||
|
if self.metadata is None:
|
||||||
|
TextLogErrorMetadata.objects.create(
|
||||||
|
text_log_error=self,
|
||||||
|
best_classification=classification)
|
||||||
|
else:
|
||||||
|
self.metadata.best_classification = classification
|
||||||
|
self.metadata.save(update_fields=['best_classification'])
|
||||||
|
|
||||||
|
if self.metadata.failure_line:
|
||||||
|
self.metadata.failure_line.best_classification = classification
|
||||||
|
self.metadata.failure_line.save(update_fields=['best_classification'])
|
||||||
|
|
||||||
|
self.metadata.failure_line.elastic_search_insert()
|
||||||
|
|
||||||
|
def mark_best_classification_verified(self, classification):
|
||||||
|
if classification not in self.classified_failures.all():
|
||||||
|
manual_detector = Matcher.objects.get(name="ManualDetector")
|
||||||
|
self.set_classification(manual_detector, classification=classification)
|
||||||
|
|
||||||
|
if self.metadata is None:
|
||||||
|
TextLogErrorMetadata.objects.create(text_log_error=self,
|
||||||
|
best_classification=classification,
|
||||||
|
best_is_verified=True)
|
||||||
|
else:
|
||||||
|
self.metadata.best_classification = classification
|
||||||
|
self.metadata.best_is_verified = True
|
||||||
|
self.metadata.save()
|
||||||
|
if self.metadata.failure_line:
|
||||||
|
self.metadata.failure_line.best_classification = classification
|
||||||
|
self.metadata.failure_line.best_is_verified = True
|
||||||
|
self.metadata.failure_line.save()
|
||||||
|
self.metadata.failure_line.elastic_search_insert()
|
||||||
|
|
||||||
|
def update_autoclassification(self):
|
||||||
|
"""
|
||||||
|
If a job is manually classified and has a single line in the logs matching a single
|
||||||
|
TextLogError, but the TextLogError has not matched any ClassifiedFailure, add a
|
||||||
|
new match due to the manual classification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
manual_detector = Matcher.objects.get(name="ManualDetector")
|
||||||
|
|
||||||
|
classification, _ = self.set_classification(manual_detector)
|
||||||
|
self.mark_best_classification_verified(classification)
|
||||||
|
|
||||||
|
|
||||||
class TextLogErrorMetadata(models.Model):
|
class TextLogErrorMetadata(models.Model):
|
||||||
"""Optional, mutable, data that can be associated with a TextLogError."""
|
"""Optional, mutable, data that can be associated with a TextLogError."""
|
||||||
|
|
|
@ -261,6 +261,9 @@ class JobsViewSet(viewsets.ViewSet):
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
status_map = {k: v for k, v in Job.AUTOCLASSIFY_STATUSES}
|
||||||
|
resp["autoclassify_status"] = status_map[job.autoclassify_status]
|
||||||
|
|
||||||
return Response(resp)
|
return Response(resp)
|
||||||
|
|
||||||
def list(self, request, project):
|
def list(self, request, project):
|
||||||
|
@ -542,6 +545,11 @@ class JobsViewSet(viewsets.ViewSet):
|
||||||
status=HTTP_404_NOT_FOUND)
|
status=HTTP_404_NOT_FOUND)
|
||||||
textlog_errors = (TextLogError.objects
|
textlog_errors = (TextLogError.objects
|
||||||
.filter(step__job=job)
|
.filter(step__job=job)
|
||||||
|
.select_related("_metadata",
|
||||||
|
"_metadata__failure_line")
|
||||||
|
.prefetch_related("classified_failures",
|
||||||
|
"matches",
|
||||||
|
"matches__matcher")
|
||||||
.order_by('id'))
|
.order_by('id'))
|
||||||
return Response(serializers.TextLogErrorSerializer(textlog_errors,
|
return Response(serializers.TextLogErrorSerializer(textlog_errors,
|
||||||
many=True,
|
many=True,
|
||||||
|
|
|
@ -216,11 +216,16 @@ class FailureLineNoStackSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TextLogErrorMetadataSerializer(serializers.ModelSerializer):
|
class TextLogErrorMetadataSerializer(serializers.ModelSerializer):
|
||||||
|
failure_line = FailureLineNoStackSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TextLogErrorMetadata
|
model = models.TextLogErrorMetadata
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class TextLogErrorSerializer(serializers.ModelSerializer):
|
class TextLogErrorSerializer(serializers.ModelSerializer):
|
||||||
|
matches = FailureMatchSerializer(many=True)
|
||||||
|
classified_failures = ClassifiedFailureSerializer(many=True)
|
||||||
bug_suggestions = NoOpSerializer(read_only=True)
|
bug_suggestions = NoOpSerializer(read_only=True)
|
||||||
metadata = TextLogErrorMetadataSerializer(read_only=True)
|
metadata = TextLogErrorMetadataSerializer(read_only=True)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.status import (HTTP_200_OK,
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
from treeherder.model.models import (ClassifiedFailure,
|
||||||
|
TextLogError)
|
||||||
|
from treeherder.webapp.api import (pagination,
|
||||||
|
serializers)
|
||||||
|
from treeherder.webapp.api.utils import as_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TextLogErrorViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = serializers.TextLogErrorSerializer
|
||||||
|
queryset = TextLogError.objects.prefetch_related("classified_failures",
|
||||||
|
"matches",
|
||||||
|
"matches__matcher",
|
||||||
|
"_metadata",
|
||||||
|
"_metadata__failure_line").all()
|
||||||
|
pagination_class = pagination.IdPagination
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def _update(self, data, user, many=True):
|
||||||
|
ids = []
|
||||||
|
error_line_ids = set()
|
||||||
|
classification_ids = set()
|
||||||
|
bug_number_classifications = {}
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
line_id = item.get("id")
|
||||||
|
if line_id is None:
|
||||||
|
return "No text log error id provided", HTTP_400_BAD_REQUEST
|
||||||
|
try:
|
||||||
|
line_id = int(line_id)
|
||||||
|
except ValueError:
|
||||||
|
return "Text log error id was not an integer", HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
error_line_ids.add(line_id)
|
||||||
|
|
||||||
|
classification_id = item.get("best_classification")
|
||||||
|
|
||||||
|
if classification_id is not None:
|
||||||
|
classification_ids.add(classification_id)
|
||||||
|
|
||||||
|
bug_number = item.get("bug_number")
|
||||||
|
|
||||||
|
if (not classification_id and
|
||||||
|
bug_number is not None and
|
||||||
|
bug_number not in bug_number_classifications):
|
||||||
|
bug_number_classifications[bug_number], _ = (
|
||||||
|
ClassifiedFailure.objects.get_or_create(bug_number=bug_number))
|
||||||
|
|
||||||
|
ids.append((line_id, classification_id, bug_number))
|
||||||
|
|
||||||
|
error_lines = as_dict(
|
||||||
|
TextLogError.objects
|
||||||
|
.prefetch_related('classified_failures')
|
||||||
|
.filter(id__in=error_line_ids), "id")
|
||||||
|
|
||||||
|
if len(error_lines) != len(error_line_ids):
|
||||||
|
missing = error_line_ids - set(error_lines.keys())
|
||||||
|
return ("No text log error with id: {0}".format(", ".join(missing)),
|
||||||
|
HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
classifications = as_dict(
|
||||||
|
ClassifiedFailure.objects.filter(id__in=classification_ids), "id")
|
||||||
|
|
||||||
|
if len(classifications) != len(classification_ids):
|
||||||
|
missing = classification_ids - set(classifications.keys())
|
||||||
|
return ("No classification with id: {0}".format(", ".join(missing)),
|
||||||
|
HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
for line_id, classification_id, bug_number in ids:
|
||||||
|
logger.debug("line_id: %s, classification_id: %s, bug_number: %s" %
|
||||||
|
(line_id, classification_id, bug_number))
|
||||||
|
error_line = error_lines[line_id]
|
||||||
|
if classification_id is not None:
|
||||||
|
logger.debug("Using classification id")
|
||||||
|
classification = classifications[classification_id]
|
||||||
|
if bug_number is not None and bug_number != classification.bug_number:
|
||||||
|
logger.debug("Updating classification bug number")
|
||||||
|
classification = classification.set_bug(bug_number)
|
||||||
|
elif bug_number is not None:
|
||||||
|
logger.debug("Using bug number")
|
||||||
|
classification = bug_number_classifications[bug_number]
|
||||||
|
else:
|
||||||
|
logger.debug("Using null classification")
|
||||||
|
classification = None
|
||||||
|
|
||||||
|
error_line.mark_best_classification_verified(classification)
|
||||||
|
error_line.step.job.update_after_verification(user)
|
||||||
|
|
||||||
|
# Force failure line to be reloaded, including .classified_failures
|
||||||
|
rv = (TextLogError.objects
|
||||||
|
.prefetch_related('classified_failures')
|
||||||
|
.filter(id__in=error_line_ids))
|
||||||
|
|
||||||
|
if not many:
|
||||||
|
rv = rv[0]
|
||||||
|
|
||||||
|
return (serializers.TextLogErrorSerializer(rv, many=many).data,
|
||||||
|
HTTP_200_OK)
|
||||||
|
|
||||||
|
def update(self, request, pk=None):
|
||||||
|
data = {"id": pk}
|
||||||
|
for k, v in request.data.iteritems():
|
||||||
|
if k not in data:
|
||||||
|
data[k] = v
|
||||||
|
|
||||||
|
body, status = self._update([data], request.user, many=False)
|
||||||
|
return Response(body, status=status)
|
||||||
|
|
||||||
|
def update_many(self, request):
|
||||||
|
body, status = self._update(request.data, request.user, many=True)
|
||||||
|
|
||||||
|
if status == HTTP_404_NOT_FOUND:
|
||||||
|
# 404 doesn't make sense for updating many since the path is always
|
||||||
|
# valid, so if we get an invalid id instead return 400
|
||||||
|
status = HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
return Response(body, status=status)
|
|
@ -18,6 +18,7 @@ from treeherder.webapp.api import (artifact,
|
||||||
resultset,
|
resultset,
|
||||||
runnable_jobs,
|
runnable_jobs,
|
||||||
seta,
|
seta,
|
||||||
|
text_log_error,
|
||||||
text_log_summary,
|
text_log_summary,
|
||||||
text_log_summary_line)
|
text_log_summary_line)
|
||||||
|
|
||||||
|
@ -122,6 +123,9 @@ default_router.register(r'failure-line', failureline.FailureLineViewSet,
|
||||||
default_router.register(r'classified-failure',
|
default_router.register(r'classified-failure',
|
||||||
classifiedfailure.ClassifiedFailureViewSet,
|
classifiedfailure.ClassifiedFailureViewSet,
|
||||||
base_name='classified-failure')
|
base_name='classified-failure')
|
||||||
|
default_router.register(r'text-log-error',
|
||||||
|
text_log_error.TextLogErrorViewSet,
|
||||||
|
base_name='text-log-error')
|
||||||
default_router.register(r'text-log-summary',
|
default_router.register(r'text-log-summary',
|
||||||
text_log_summary.TextLogSummaryViewSet,
|
text_log_summary.TextLogSummaryViewSet,
|
||||||
base_name='text-log-summary')
|
base_name='text-log-summary')
|
||||||
|
|
Загрузка…
Ссылка в новой задаче