Skip to content

Commit

Permalink
Merge pull request #114 from gregoil/finish_signature_handler
Browse files Browse the repository at this point in the history
Finish remote signature handler
  • Loading branch information
UnDarkle committed Nov 13, 2018
2 parents f755e0c + 0d9f176 commit ecc411d
Show file tree
Hide file tree
Showing 28 changed files with 356 additions and 299 deletions.
13 changes: 12 additions & 1 deletion docs/output_handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ is higher or equal to ``INFO`` (``INFO``, ``WARNING``, ``ERROR``,
``CRITICAL``).

Excel
===========
=====

Sometimes, you want to have a better visualization of the results. Rotest can
output the results into a human-readable :file:`results.xls` file, which can be
Expand Down Expand Up @@ -149,3 +149,14 @@ debugging or evaluation.
Those artifacts are saved in the artifacts directory of Rotest. It is
recommended to make this folder a shared folder between all your users.
For more about this location, see :ref:`configurations`.

Signature
=========

This handler saves in the remote DB patterns for errors and failures
it encounters. You can also link the signatures to issues in your bug tracking system,
e.g. JIRA. In the next encounters the handler will issue a warning with the
supplied link via the log. The relevant option is ``-o signature``.

To see the patterns, change them, and add links - go to the admin page
of the server under core/signatures.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import absolute_import
from setuptools import setup, find_packages

__version__ = "5.1.3"
__version__ = "5.2.0"

result_handlers = [
"db = rotest.core.result.handlers.db_handler:DBHandler",
Expand Down
11 changes: 11 additions & 0 deletions src/rotest/api/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,14 @@ class StartTestRunParamsModel(AbstractAPIModel):
ModelField(name="tests", model=TestModel, required=True),
ModelField(name="run_data", model=RunDataModel, required=True)
]


class SignatureControlParamsModel(AbstractAPIModel):
"""Model structure of signature control operation.
Args:
error (str): error message.
"""
PROPERTIES = [
StringField(name="error", required=True),
]
16 changes: 15 additions & 1 deletion src/rotest/api/common/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from swaggapi.api.builder.common.response import AbstractResponse
from swaggapi.api.builder.common.fields import (StringField,
ModelField,
ArrayField, BoolField)
ArrayField,
BoolField,
NumberField)

from rotest.api.common import GenericModel

Expand Down Expand Up @@ -44,3 +46,15 @@ class ShouldSkipResponse(AbstractResponse):
BoolField(name="should_skip", required=True),
StringField(name="reason", required=True)
]


class SignatureResponse(AbstractResponse):
"""Returns in response to get or create signature data action.
The response contains data of a matching signature if there is one.
"""
PROPERTIES = [
BoolField(name="is_new", required=True),
NumberField(name="id", required=True),
StringField(name="link", required=True)
]
1 change: 1 addition & 0 deletions src/rotest/api/signature_control/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .get_or_create import GetOrCreate
68 changes: 68 additions & 0 deletions src/rotest/api/signature_control/get_or_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# pylint: disable=unused-argument
from __future__ import absolute_import

import re

from six.moves import http_client

from swaggapi.api.builder.server.response import Response
from swaggapi.api.builder.server.request import DjangoRequestView

from rotest.core.models import SignatureData
from rotest.api.common.responses import SignatureResponse
from rotest.api.common.models import SignatureControlParamsModel


class GetOrCreate(DjangoRequestView):
"""Get or create error signature in the DB.
Args:
error (str): string of the error.
"""
URI = "signatures/get_or_create"
DEFAULT_MODEL = SignatureControlParamsModel
DEFAULT_RESPONSES = {
http_client.OK: SignatureResponse,
}
TAGS = {
"post": ["Signatures"]
}

@staticmethod
def _match_signatures(error_str):
"""Return the data of the matched signature.
Args:
error_str (str): exception traceback string.
Returns:
SignatureData. the signature of the given exception.
"""
for signature in SignatureData.objects.all():
signature_reg = re.compile(signature.pattern,
re.MULTILINE)
if signature_reg.match(error_str):
return signature

return None

def post(self, request, *args, **kwargs):
"""Get signature data for an error or create a new one."""
error_message = request.model.error
# Normalize newline char
error_message = error_message.replace("\r\n", "\n")

match = self._match_signatures(error_message)

is_new = False
if match is None:
is_new = True
pattern = SignatureData.create_pattern(error_message)
match = SignatureData.objects.create(link="",
pattern=pattern)

return Response({
"is_new": is_new,
"id": match.id,
"link": match.link
}, status=http_client.OK)
15 changes: 11 additions & 4 deletions src/rotest/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from swaggapi.api.openapi.models import Info, License, Tag

from rotest.api.request_token import RequestToken
from rotest.api.signature_control import GetOrCreate
from rotest.api.resource_control import (CleanupUser,
LockResources,
ReleaseResources,
Expand All @@ -19,19 +20,20 @@
StopComposite,
StartComposite,
ShouldSkip,
AddTestResult, UpdateResources)
AddTestResult,
UpdateResources)

requests = [
RequestToken,

# resources
# Resources
LockResources,
ReleaseResources,
CleanupUser,
QueryResources,
UpdateFields,

# tests
# Tests
StartTestRun,
UpdateRunData,
StopTest,
Expand All @@ -40,7 +42,10 @@
StartComposite,
ShouldSkip,
AddTestResult,
UpdateResources
UpdateResources,

# Signatures
GetOrCreate
]

info = Info(title="Rotest OpenAPI",
Expand All @@ -49,6 +54,8 @@
license=License(name="MIT"))
tags = [Tag(name="Tests",
description="All requests for managing remote test handler"),
Tag(name="Signatures",
description="All requests for managing signatures handler"),
Tag(name="Resources",
description="All requests for managing resources")]
swagger = Swagger(info, mount_url="api", requests=requests, tags=tags)
Expand Down
2 changes: 1 addition & 1 deletion src/rotest/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class CaseDataAdmin(TestDataAdmin):

class SignatureDataAdmin(admin.ModelAdmin):
"""ModelAdmin for :class:`rotest.core.models.SignatureData` model."""
fields = ['name', 'link', 'pattern']
fields = ['link', 'pattern']


class CaseInline(TestDataInline):
Expand Down
21 changes: 11 additions & 10 deletions src/rotest/core/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,23 +223,24 @@ def test_run_blocks(self):
for test in self:
test(self.result)

all_issues = []

for block in self:
all_issues.extend(block.get_short_errors())

if self.had_error():
error_blocks_list = [block.data.name for block in self if
block.had_error()]
flow_result_str = 'The following components had errors:' \
' {}'.format(error_blocks_list)
flow_result = 'The flow ended in error:\n ' \
'{}'.format('\n '.join(all_issues))

failure = AssertionError(flow_result_str)
failure = AssertionError(flow_result)
self.result.addError(self, (failure.__class__, failure, None))
return

if not self.was_successful():
failed_blocks_list = [block.data.name for block in self if
not block.was_successful()]
flow_result_str = 'The following components have failed:' \
' {}'.format(failed_blocks_list)
flow_result = 'The flow ended in failure:\n ' \
'{}'.format('\n '.join(all_issues))

failure = AssertionError(flow_result_str)
failure = AssertionError(flow_result)
self.result.addFailure(self, (failure.__class__, failure, None))
return

Expand Down
20 changes: 15 additions & 5 deletions src/rotest/core/flow_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,10 @@ def __init__(self, indexer=count(), base_work_dir=ROTEST_WORK_DIR,

@classmethod
def parametrize(cls, **parameters):
"""Return a class instantiator for this class with the given args.
"""Return a clone of this class with the given args in the common.
Use this method (or its syntactic sugar 'params') to pass values to
components under a flow.
Note:
This class method does not instantiate the component, but states
values be injected into it after it would be initialized.
"""
new_common = cls.common.copy()
new_common.update(**parameters)
Expand All @@ -164,6 +160,20 @@ def share_data(self, override_previous=True, **parameters):
self._set_parameters(override_previous=override_previous,
**parameters)

def get_short_errors(self):
"""Get short description of errors and failures.
Yields:
str. bottom line of all the errors.
"""
if not self.was_successful():
for traceback in self.data.traceback.split(
CaseData.TB_SEPARATOR):

traceback = traceback.strip(" \n")
bottom_line = traceback.rsplit("\n", 1)[-1].strip()
yield "{}: {}".format(self.data.name, bottom_line)

@classmethod
def get_test_method_name(cls):
"""Return the test method name to run.
Expand Down
23 changes: 23 additions & 0 deletions src/rotest/core/migrations/0004_auto_20181111_0319.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0003_rundata_config'),
]

operations = [
migrations.RemoveField(
model_name='signaturedata',
name='name',
),
migrations.AlterField(
model_name='signaturedata',
name='link',
field=models.CharField(max_length=200),
),
]
19 changes: 19 additions & 0 deletions src/rotest/core/migrations/0005_auto_20181112_0631.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0004_auto_20181111_0319'),
]

operations = [
migrations.AlterField(
model_name='signaturedata',
name='pattern',
field=models.TextField(max_length=1000),
),
]
33 changes: 24 additions & 9 deletions src/rotest/core/models/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# pylint: disable=no-init,old-style-class
from __future__ import absolute_import

import re

from django.db import models
from future.builtins import object

from rotest.common.django_utils.fields import NameField


class SignatureData(models.Model):
"""Contain & manage signatures about test exceptions.
Expand All @@ -22,21 +22,36 @@ class SignatureData(models.Model):
link (str): link to the issue page.
pattern (str): pattern of the signature.
"""
MAX_LINK_LENGTH = 100
MAX_LINK_LENGTH = 200
MAX_PATTERN_LENGTH = 1000

name = NameField(unique=True)
link = models.CharField(max_length=MAX_LINK_LENGTH)
pattern = models.CharField(max_length=MAX_PATTERN_LENGTH)
pattern = models.TextField(max_length=MAX_PATTERN_LENGTH)

class Meta(object):
"""Define the Django application for this model."""
app_label = 'core'

@staticmethod
def create_pattern(error_message):
"""Create a pattern from a failure or error message.
args:
error_message (str): error message to parse.
returns:
str. pattern for the given error message.
"""
return re.sub(r"\d+(\\.\d+)?(e\\-?\d+)?", ".+",
re.escape(error_message))

def __unicode__(self):
"""Django version of __str__"""
return self.name
return "Signature {}".format(self.id)

def __repr__(self):
"""Unique Representation for data"""
return self.name
return self.__unicode__()

def save(self, *args, **kwargs):
"""Override Django's 'save' to normalize newline char."""
self.pattern = self.pattern.replace("\r\n", "\n")
super(SignatureData, self).save(*args, **kwargs)
Loading

0 comments on commit ecc411d

Please sign in to comment.