Skip to content

Commit

Permalink
Merge pull request tobyqin#66 from tobyqin/rerun_result
Browse files Browse the repository at this point in the history
Rerun result improvement
  • Loading branch information
tobyqin authored Oct 31, 2017
2 parents e6b8db0 + 076a49b commit 329accb
Show file tree
Hide file tree
Showing 16 changed files with 135 additions and 19 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
8. [ ] better loading experience
9. [x] refactor js scripts
10.[>] manage css file via require js
11.[ ] auto clean up result files
2 changes: 1 addition & 1 deletion testcube/core/api/client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from testcube.core.models import TestClient

version = '1.2'
version = '1.3'


@csrf_exempt
Expand Down
2 changes: 1 addition & 1 deletion testcube/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class Meta:
fields = (
'id', 'stdout', 'get_reset_status_display', 'get_outcome_display',
'duration', 'run_on', 'reset_on', 'reset_by',
'test_client', 'reset_reason')
'test_client', 'reset_reason', 'error')

depth = 1

Expand Down
2 changes: 1 addition & 1 deletion testcube/core/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class ResultFileViewSet(viewsets.ModelViewSet):
class ResetResultViewSet(viewsets.ModelViewSet):
queryset = ResetResult.objects.all()
serializer_class = ResetResultSerializer
filter_fields = ()
filter_fields = ('id',)
search_fields = filter_fields

@list_route()
Expand Down
4 changes: 3 additions & 1 deletion testcube/core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ def _parse_command(command, result, reset):
will be parsed according under current result context.
"""
try:
cmd = command.format(result=result, reset=reset)
assert isinstance(result, TestResult)
run_variables = result.test_run.run_variables.data_json if result.test_run.run_variables else {}
cmd = command.format(result=result, reset=reset, **run_variables)
return cmd, None
except Exception as e:
logger.exception('Failed to parse command: {}'.format(command))
Expand Down
3 changes: 2 additions & 1 deletion testcube/runner/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .views import ProfileViewSet, TaskViewSet
from .views import ProfileViewSet, TaskViewSet, RunVariablesViewSet


def api_registration(router):
router.register('run_variables', RunVariablesViewSet)
router.register('profiles', ProfileViewSet)
router.register('tasks', TaskViewSet)
33 changes: 33 additions & 0 deletions testcube/runner/migrations/0002_auto_20171030_1544.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-30 07:44
from __future__ import unicode_literals

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('core', '0010_auto_20171009_1634'),
('runner', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='RunVariables',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.TextField(default=None, null=True)),
('created_on', models.DateTimeField(auto_now_add=True)),
('updated_on', models.DateTimeField(auto_now=True)),
('test_run',
models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='run_variables',
to='core.TestRun')),
],
),
migrations.AlterField(
model_name='task',
name='status',
field=models.IntegerField(choices=[(-1, 'Pending'), (0, 'Sent'), (1, 'Error')], default=-1),
),
]
14 changes: 13 additions & 1 deletion testcube/runner/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
from django.db import models

from testcube.core.models import Product
from testcube.core.models import Product, TestRun
from testcube.utils import to_json


class RunVariables(models.Model):
test_run = models.OneToOneField(TestRun, on_delete=models.CASCADE, related_name='run_variables')
data = models.TextField(null=True, default=None)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)

@property
def data_json(self):
return to_json(self.data)


class Profile(models.Model):
Expand Down
5 changes: 5 additions & 0 deletions testcube/runner/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from .models import *


class RunVariablesSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RunVariables
fields = '__all__'

class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
Expand Down
7 changes: 7 additions & 0 deletions testcube/runner/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
from .serializers import *


class RunVariablesViewSet(viewsets.ModelViewSet):
queryset = RunVariables.objects.all()
serializer_class = RunVariablesSerializer
filter_fields = ('test_run',)
search_fields = filter_fields


class ProfileViewSet(viewsets.ModelViewSet):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
Expand Down
19 changes: 19 additions & 0 deletions testcube/static/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,22 @@ Then add finish command to run last step.
```
testcube-client --finish-run -x "**/results/*.xml" -i "**/*.png"
```

## Advanced feature: Reset a test result

TestCube provide the feature to reset a failed test result, there are lots of reason when a test failed, sometimes you want to run the failed test again, **reset** means rerun a failed test.

From test result detail page, you can see a reset tab provide reset feature, before it works, you have to do a few works:

A. Setup a job to process reset work
- This job requires varialbes so your test engine can run the failed test case and generate xunit files, e.g. testcase name, environments, result id
- Upload the xunits by command: `testcube-client --reset-result 123 -x "**/*.xml"`

B. Add job A as the reset command to a product
- Open /api/profiles/ to add a profile to your product, with step A command

C. Setup a job to handle reset tasks automatically, e.g. running every 5 minutes
- command: `testcube-client --handle-task`

When you reset a result, TestCube will add a pending task with required variable, job C will handle this task automatically, then run a reset command according to current product profile, if the command has been run successfully, job A will be executed, and pending task will be marked as done. Once job A done, it will upload generated results to TestCube, at the same time, the failed results will be updated to new state, the reset process finished.

26 changes: 22 additions & 4 deletions testcube/static/modules/result-detail.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
define(['jquery', './table-support', './chart-support', './utils', 'bootstrapTable', 'bootstrapSelect'],
function ($, support, chart, utils) {
define(['jquery', './table-support', './chart-support', './utils', 'bootstrap-dialog', 'bootstrapTable', 'bootstrapSelect'],
function ($, support, chart, utils, BootstrapDialog) {

"use strict";
let f = support.formatter;
Expand Down Expand Up @@ -41,7 +41,7 @@ define(['jquery', './table-support', './chart-support', './utils', 'bootstrapTab
{title: 'Duration', field: 'duration', formatter: f.durationFormatter},
{title: 'Status', field: 'get_reset_status_display'},
{title: 'Outcome', field: 'get_outcome_display'},
{title: 'Detail', field: 'stdout', formatter: f.resetDetailFormatter}
{title: 'Detail', field: 'error', formatter: f.resetDetailFormatter}
];


Expand Down Expand Up @@ -90,7 +90,8 @@ define(['jquery', './table-support', './chart-support', './utils', 'bootstrapTab
pagination: false,
sortable: false,
showFooter: false,
columns: resultResetsColumns
columns: resultResetsColumns,
onPostBody: setupResetResultDetailViewEvent
});

if (result.testcase) {
Expand All @@ -112,6 +113,23 @@ define(['jquery', './table-support', './chart-support', './utils', 'bootstrapTab
}
}

function setupResetResultDetailViewEvent() {
$('.reset-result').click(function () {
waitForLoading();
let output = $(this).attr('data-text');
output = output.replace('||', '\n\n').replace('||', '\n---------- OUTPUT --------\n');
let detail = `<pre><code data-language="log">${output}</code></pre>`;
BootstrapDialog.show({
title: 'Reset Detail',
message: detail,
size: BootstrapDialog.SIZE_WIDE
});

utils.startLogHighlight();
loadingCompleted();
});
}

function resultHistoryTableDataHandler(data) {
window.app.resultHistory = data;
return data.results;
Expand Down
8 changes: 6 additions & 2 deletions testcube/static/modules/table-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ define(['moment', './utils', 'bootstrapTable'], function (moment, utils) {
return `<a href="${url}" data-toggle="lightbox" data-title="${filename}" data-gallery="result-gallery">${url}</a>`;
};

formatter.resetDetailFormatter = function (detail) {
return `<a href="#" title="${detail}">View</a>`;
formatter.resetDetailFormatter = function (error) {
let message = 'Nothing';
if (error) {
message = error.message + '||' + error.stacktrace + '||' + error.stdout;
}
return `<a class="reset-result" data-text="${message}">View</a>`;
};

support.defaultTableOptions = {
Expand Down
2 changes: 1 addition & 1 deletion testcube/static/require-config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
let version = '1.7';
let version = '1.8';

function jsVersion(id, url) {
return url.includes('libs/') ? '' : '?v' + version;
Expand Down
10 changes: 10 additions & 0 deletions testcube/tests/test_core_api_views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import os

from django.contrib.auth.models import User
from django.test import TestCase as TC, Client

from testcube.core.models import Configuration, TestCase, Team, Product, TestResult, TestRun, TestClient
from testcube.runner.models import RunVariables


class ModelsTestCase(TC):
Expand Down Expand Up @@ -111,3 +113,11 @@ def test_get_run_tags(self):
r = self.client.get(api)
expected = [('tag3', 2), ('tag4', 2), ('tag5', 2), ('tag1', 1), ('tag2', 1)]
assert r.data == expected, r.data

def test_use_run_variables(self):
self.client.login(username='admin', password='admin')
run = TestRun.objects.create(name='test-run', owner='test', start_by='test', product=self.product)
env_vars = json.dumps(dict(os.environ))
var = RunVariables.objects.create(test_run=run, data=env_vars)
assert var.data == env_vars
assert isinstance(var.data_json, dict)
16 changes: 10 additions & 6 deletions testcube/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,7 @@ def setup_logger(log_dir=None, debug=False):


def append_json(origin_txt, field, value):
try:
obj = json.loads(origin_txt)
except:
from testcube.settings import logger
logger.warning('Cannot parse to json: {}'.format(origin_txt))
obj = {}
obj = to_json(origin_txt)

if field in obj:
obj[field] += '|*|' + value
Expand All @@ -68,3 +63,12 @@ def append_json(origin_txt, field, value):
obj[field] = value

return json.dumps(obj)


def to_json(data_text):
try:
return json.loads(data_text)
except:
from testcube.settings import logger
logger.exception('Cannot parse to json: {}'.format(data_text))
return {}

0 comments on commit 329accb

Please sign in to comment.