Skip to content

Commit

Permalink
Merge 2363e9e into 851d088
Browse files Browse the repository at this point in the history
  • Loading branch information
UnDarkle committed Jun 20, 2019
2 parents 851d088 + 2363e9e commit 794cf2f
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 18 deletions.
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -3,7 +3,7 @@
from setuptools import setup, find_packages


__version__ = "7.4.1"
__version__ = "7.5.0"

result_handlers = [
"db = rotest.core.result.handlers.db_handler:DBHandler",
Expand Down Expand Up @@ -50,6 +50,7 @@
'cached_property',
],
extras_require={
':python_version=="2.7"': ['statistics'],
"dev": [
"pytest",
"pytest-django",
Expand Down
3 changes: 2 additions & 1 deletion src/rotest/api/common/__init__.py
Expand Up @@ -3,4 +3,5 @@
ReleaseResourcesParamsModel,
ResourceDescriptorModel,
UpdateFieldsParamsModel,
LockResourcesParamsModel)
LockResourcesParamsModel,
StatisticsRequestModel)
11 changes: 11 additions & 0 deletions src/rotest/api/common/models.py
Expand Up @@ -22,6 +22,17 @@ class TokenModel(AbstractAPIModel):
]


class StatisticsRequestModel(AbstractAPIModel):
"""Model that contains name of a test or component."""
PROPERTIES = [
StringField(name="test_name", required=True),
NumberField(name="max_sample_size", required=False),
NumberField(name="min_duration_cut", required=False),
NumberField(name="max_iterations", required=False),
NumberField(name="acceptable_ratio", required=False)
]


class ResourceDescriptorModel(AbstractAPIModel):
"""Descriptor of a resource.
Expand Down
9 changes: 9 additions & 0 deletions src/rotest/api/common/responses.py
Expand Up @@ -24,6 +24,15 @@ class TokenResponseModel(AbstractResponse):
]


class TestStatisticsResponse(AbstractResponse):
"""Returns statistics of a test."""
PROPERTIES = [
NumberField(name="min", required=True),
NumberField(name="avg", required=True),
NumberField(name="max", required=True)
]


class FailureResponseModel(AbstractResponse):
"""Returns when an invalid request is received."""
PROPERTIES = [
Expand Down
14 changes: 2 additions & 12 deletions src/rotest/api/request_token.py
Expand Up @@ -15,12 +15,7 @@


class RequestToken(DjangoRequestView):
"""Initialize the tests run data.
Args:
tests_tree (dict): contains the hierarchy of the tests in the run.
run_data (dict): contains additional data about the run.
"""
"""Create a session for the client and return its unique token."""
URI = "tests/get_token"
DEFAULT_MODEL = GenericModel
DEFAULT_RESPONSES = {
Expand All @@ -33,12 +28,7 @@ class RequestToken(DjangoRequestView):

@session_middleware
def get(self, request, sessions, *args, **kwargs):
"""Initialize the tests run data.
Args:
tests_tree (dict): contains the hierarchy of the tests in the run.
run_data (dict): contains additional data about the run.
"""
"""Create a session for the client and return its unique token."""
session_token = str(uuid.uuid4())
sessions[session_token] = SessionData()
response = {
Expand Down
1 change: 1 addition & 0 deletions src/rotest/api/test_control/__init__.py
Expand Up @@ -7,4 +7,5 @@
from .add_test_result import AddTestResult
from .update_run_data import UpdateRunData
from .start_composite import StartComposite
from .get_statistics import GetTestStatistics
from .update_resources import UpdateResources
57 changes: 57 additions & 0 deletions src/rotest/api/test_control/get_statistics.py
@@ -0,0 +1,57 @@
# pylint: disable=unused-argument, no-self-use
from __future__ import absolute_import

from six.moves import http_client
from swaggapi.api.builder.server.response import Response
from swaggapi.api.builder.server.exceptions import BadRequest
from swaggapi.api.builder.server.request import DjangoRequestView

from rotest.api.common.models import StatisticsRequestModel
from rotest.api.test_control.middleware import session_middleware
from rotest.core.utils.test_statistics import clean_data, collect_durations
from rotest.api.common.responses import (TestStatisticsResponse,
FailureResponseModel)


class GetTestStatistics(DjangoRequestView):
"""Get statistics for a test or component."""
URI = "tests/get_statistics"
DEFAULT_MODEL = StatisticsRequestModel
DEFAULT_RESPONSES = {
http_client.OK: TestStatisticsResponse,
http_client.BAD_REQUEST: FailureResponseModel
}
TAGS = {
"get": ["Tests"]
}

@session_middleware
def get(self, request, sessions, *args, **kwargs):
"""Initialize the tests run data."""
parameters = {"test_name": request.model.test_name}
if request.model.max_sample_size is not None:
parameters["max_size"] = request.model.max_sample_size

test_durations = collect_durations(**parameters)
if len(test_durations) < 1:
raise BadRequest("No test history found!")

parameters = {"durations": test_durations}
if request.model.min_duration_cut is not None:
parameters["min_duration_cut"] = request.model.min_duration_cut
if request.model.max_iterations is not None:
parameters["max_iterations"] = request.model.max_iterations
if request.model.acceptable_ratio is not None:
parameters["acceptable_ratio"] = request.model.acceptable_ratio

test_durations = clean_data(**parameters)
if len(test_durations) < 1:
raise BadRequest("Test history disparity too wide!")

response = {
"min": min(test_durations),
"avg": sum(test_durations) / len(test_durations),
"max": max(test_durations)
}

return Response(response, status=http_client.OK)
4 changes: 3 additions & 1 deletion src/rotest/api/urls.py
Expand Up @@ -22,7 +22,8 @@
StartComposite,
ShouldSkip,
AddTestResult,
UpdateResources)
UpdateResources,
GetTestStatistics)

requests = [
RequestToken,
Expand All @@ -45,6 +46,7 @@
ShouldSkip,
AddTestResult,
UpdateResources,
GetTestStatistics,

# Signatures
GetOrCreate
Expand Down
75 changes: 75 additions & 0 deletions src/rotest/core/utils/test_statistics.py
@@ -0,0 +1,75 @@
"""Module for statistics collection on tests."""
from __future__ import absolute_import
from statistics import mean, median, pstdev

from rotest.core.models import CaseData


CUT_OFF_FACTOR = 1.5


def collect_durations(test_name, max_size=300):
"""Return re durations of successful runs tests with the given name.
Args:
test_name (str): name of the test to search, e.g. "MyTest.test_method".
max_size (number): maximal number of tests to collect.
Returns:
list. collected tests after filtering.
"""
latest_tests = CaseData.objects.filter(name=test_name,
exception_type=0,
start_time__isnull=False,
end_time__isnull=False)

latest_tests = latest_tests.order_by('-id')
durations = ((test.end_time - test.start_time).total_seconds()
for test in latest_tests)

durations = [x for x in durations if x > 0]

return durations[:max_size]


def remove_anomalies(durations):
"""Return a list with less anomalies in the numeric values.
Args:
durations (list): list of numeric values.
Returns:
list. list with less anomalies.
"""
avg = (mean(durations) + median(durations)) / 2
deviation = pstdev(durations)
cut_off = deviation * CUT_OFF_FACTOR
lower_limit = avg - cut_off
upper_limit = avg + cut_off
return [x for x in durations if lower_limit < x < upper_limit]


def clean_data(durations, min_duration_cut=0.5,
max_iterations=3, acceptable_ratio=2):
"""Return a list with less anomalies in the numeric values.
Args:
durations (list): list of numeric values.
min_duration_cut (number): ignore tests under the given duration.
max_iterations (number): max anomalies removal iterations.
acceptable_ratio (number): acceptable ration between max and min
values, under which don't try to remove anomalies.
Returns:
list. filtered list of durations.
"""
durations = [x for x in durations if x > min_duration_cut]
iteration_index = 0
while len(durations) > 1 and iteration_index < max_iterations:
if min(durations) * acceptable_ratio >= max(durations):
break

durations = remove_anomalies(durations)
iteration_index += 1

return durations
38 changes: 37 additions & 1 deletion src/rotest/management/client/client.py
Expand Up @@ -7,11 +7,12 @@

from rotest.common import core_log
from rotest.api.request_token import RequestToken
from rotest.api.common.models import GenericModel
from rotest.api.resource_control import UpdateFields
from rotest.api.common import UpdateFieldsParamsModel
from rotest.api.test_control import GetTestStatistics
from rotest.management.common.parsers import JSONParser
from rotest.api.common.responses import FailureResponseModel
from rotest.api.common.models import GenericModel, StatisticsRequestModel
from rotest.management.common.resource_descriptor import ResourceDescriptor
from rotest.common.config import (DJANGO_MANAGER_PORT,
RESOURCE_REQUEST_TIMEOUT, API_BASE_URL)
Expand Down Expand Up @@ -111,3 +112,38 @@ def update_fields(self, model, filter_dict=None, **kwargs):

if isinstance(response, FailureResponseModel):
raise Exception(response.details)

def get_statistics(self, test_name,
max_sample_size=None,
min_duration_cut=None,
max_iterations=None,
acceptable_ratio=None):
"""Request test duration statistics.
Args:
test_name (str): name of the test to search,
e.g. "MyTest.test_method".
max_sample_size (number): maximal number of tests to collect.
min_duration_cut (number): ignore tests under the given duration.
max_iterations (number): max anomalies removal iterations.
acceptable_ratio (number): acceptable ration between max and min
values, under which don't try to remove anomalies.
Returns:
dict. dictionary containing the min, max and avg test durations.
"""
request_data = StatisticsRequestModel({
"test_name": test_name,
"max_sample_size": max_sample_size,
"min_duration_cut": min_duration_cut,
"max_iterations": max_iterations,
"acceptable_ratio": acceptable_ratio})

response = self.requester.request(GetTestStatistics,
data=request_data,
method="get")

if isinstance(response, FailureResponseModel):
raise RuntimeError(response.details)

return response.body
59 changes: 57 additions & 2 deletions tests/api/test_control/test_test_control.py
Expand Up @@ -3,13 +3,14 @@
from __future__ import absolute_import

from functools import partial
from datetime import datetime, timedelta

from future.builtins import next
from six.moves import http_client
from django.test import Client, TransactionTestCase

from rotest.core.models import RunData
from rotest.core.models.case_data import TestOutcome
from rotest.core.models.case_data import CaseData, TestOutcome
from rotest.management.client.result_client import ClientResultManager

from tests.api.utils import request
Expand Down Expand Up @@ -99,14 +100,68 @@ def test_update_resources(self):
def test_should_skip(self):
"""Assert that the request has the right server response."""
response, content = self.requester(path="tests/should_skip",
params={
json_data={
"token": self.token,
"test_id":
self.test_case.identifier
}, method="get")
self.assertEqual(response.status_code, http_client.OK)
self.assertFalse(content.should_skip)

def test_get_statistics_cleaning(self):
"""Check the functionality of the test statistics request."""
now = datetime.now()
durations = (4, 5, 6, 17)
test_name = "SomeTest"
for duration in durations:
CaseData.objects.create(
name=test_name,
success=True,
start_time=now,
end_time=now + timedelta(seconds=duration),
exception_type=0)

response, content = self.requester(path="tests/get_statistics",
json_data={
"test_name": test_name,
"max_sample_size": None,
"min_duration_cut": None,
"max_iterations": None,
"acceptable_ratio": None
}, method="get")

self.assertEqual(response.status_code, http_client.OK)
self.assertEqual(content.min, 4)
self.assertEqual(content.max, 6)
self.assertEqual(content.avg, 5)

def test_get_statistics_no_cleaning(self):
"""Check the functionality of the test statistics request."""
now = datetime.now()
durations = (4, 5, 6, 17)
test_name = "SomeTest"
for duration in durations:
CaseData.objects.create(
name=test_name,
success=True,
start_time=now,
end_time=now + timedelta(seconds=duration),
exception_type=0)

response, content = self.requester(path="tests/get_statistics",
json_data={
"test_name": test_name,
"max_sample_size": None,
"min_duration_cut": None,
"max_iterations": 0,
"acceptable_ratio": None
}, method="get")

self.assertEqual(response.status_code, http_client.OK)
self.assertEqual(content.min, 4)
self.assertEqual(content.max, 17)
self.assertEqual(content.avg, 8)

def test_add_test_result(self):
"""Assert that the request has the right server response."""
response, _ = self.requester(
Expand Down

0 comments on commit 794cf2f

Please sign in to comment.