testdef, exécuter les tests automatiquement (#74302) #110

Merged
vdeniaud merged 7 commits from wip/74302-testdef-executer-les-tests-autom into main 2023-02-28 10:23:29 +01:00
21 changed files with 682 additions and 87 deletions

View File

@ -9,6 +9,7 @@ from wcs.blocks import BlockDef
from wcs.categories import BlockCategory
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.testdef import TestDef, TestResult
from wcs.workflows import Workflow
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
@ -536,3 +537,50 @@ def test_block_field_statistics_data_update(pub):
formdata.refresh_from_storage()
assert formdata.statistics_data == {'bool': [True]}
def test_block_test_results(pub):
create_superuser(pub)
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
block.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.BlockField(id='0', label='test', type='block:%s' % block.slug),
]
formdef.store()
app = login(get_app(pub))
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click(href=re.compile('123/$'))
resp.form['varname'] = 'test'
resp = resp.form.submit('submit').follow()
assert TestResult.count() == 0
pub.site_options.set('options', 'enable-tests', 'true')
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
pub.site_options.write(fd)
resp = resp.click(href=re.compile('123/$'))
resp.form['varname'] = 'test_2'
resp = resp.form.submit('submit').follow()
assert TestResult.count() == 0
formdata = formdef.data_class()()
formdata.just_created()
formdata.data['1'] = 'a'
formdata.store()
testdef = TestDef.create_from_formdata(formdef, formdata)
testdef.name = 'First test'
testdef.store()
resp = resp.click(href=re.compile('123/$'))
resp.form['varname'] = 'test_3'
resp = resp.form.submit('submit').follow()
assert TestResult.count() == 1

View File

@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET
import pytest
import responses
from django.utils.timezone import now
from pyquery import PyQuery
from webtest import Upload
@ -17,6 +18,7 @@ from wcs.data_sources import NamedDataSource
from wcs.formdef import FormDef
from wcs.qommon.errors import ConnectionError
from wcs.qommon.http_request import HTTPRequest
from wcs.testdef import TestResult
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
from wcs.wscalls import NamedWsCall
@ -3792,3 +3794,53 @@ def test_form_field_statistics_data_update(pub):
formdata.refresh_from_storage()
assert formdata.statistics_data == {'bool': [True]}
def test_forms_last_test_result(pub, formdef):
TestResult.wipe()
create_superuser(pub)
create_role(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/forms/1/')
assert '<h2>form title</h2>' in resp.text
pub.site_options.set('options', 'enable-tests', 'true')
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
pub.site_options.write(fd)
resp = app.get('/backoffice/forms/1/')
assert '<h2>form title</h2>' in resp.text
test_result = TestResult()
test_result.object_type = formdef.get_table_name()
test_result.object_id = formdef.id
test_result.timestamp = now()
test_result.success = True
test_result.reason = ''
test_result.results = []
test_result.store()
for url in ('/backoffice/forms/1/', '/backoffice/forms/1/fields/'):
resp = app.get(url)
assert '<h2>form title <a href="%s"' % test_result.get_admin_url() in resp.text
assert 'test-success' in resp.text
assert 'test-failure' not in resp.text
test_result.success = False
test_result.store()
for url in ('/backoffice/forms/1/', '/backoffice/forms/1/fields/'):
resp = app.get(url)
assert 'test-failure' in resp.text
assert 'test-success' not in resp.text
test_result.success = True
test_result.id = None
test_result.store()
assert TestResult.count() == 2
for url in ('/backoffice/forms/1/', '/backoffice/forms/1/fields/'):
resp = app.get(url)
assert 'test-success' in resp.text
assert 'test-failure' not in resp.text

View File

@ -9,7 +9,7 @@ from webtest import Upload
from wcs import fields
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.testdef import TestDef
from wcs.testdef import TestDef, TestResult
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
from .test_all import create_superuser
@ -32,6 +32,7 @@ def pub():
FormDef.wipe()
TestDef.wipe()
TestResult.wipe()
return pub
@ -64,14 +65,6 @@ def test_tests_page(pub):
formdef = FormDef()
formdef.name = 'test title'
formdef.fields = [
fields.PageField(
id='0',
label='1st page',
type='page',
post_conditions=[
{'condition': {'type': 'django', 'value': 'form_var_text == "a"'}, 'error_message': 'Error'}
],
),
fields.StringField(id='1', label='Text', varname='text'),
]
formdef.store()
@ -81,7 +74,6 @@ def test_tests_page(pub):
resp = app.get(formdef.get_admin_url())
resp = resp.click('Tests')
assert 'There are no tests yet.' in resp.text
assert 'Run' not in resp.text
assert 'Tests cannot be created because there are no completed forms.' in resp.text
assert 'New' not in resp.text
@ -95,7 +87,6 @@ def test_tests_page(pub):
resp = app.get(formdef.get_admin_url())
resp = resp.click('Tests')
assert 'There are no tests yet.' in resp.text
assert 'Run' not in resp.text
assert 'New tests cannot be created' not in resp.text
resp = resp.click('New')
@ -107,9 +98,6 @@ def test_tests_page(pub):
assert 'First test' in resp.text
assert 'no tests yet' not in resp.text
resp = resp.click('Run')
assert 'First test: Success!' in resp.text
formdata = formdef.data_class()()
formdata.just_created()
formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
@ -125,10 +113,6 @@ def test_tests_page(pub):
assert 'First test' in resp.text
assert 'Second test' in resp.text
resp = resp.click('Run')
assert 'First test: Success!' in resp.text
assert 'Second test: Page 1 post condition was not met (form_var_text == &quot;a&quot;).' in resp.text
def test_tests_page_formdefs_isolation(pub):
formdef = FormDef()
@ -282,8 +266,8 @@ def test_tests_edit(pub):
formdata = formdef.data_class()()
formdata.just_created()
formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
formdata.data['1'] = 'aaa'
formdata.data['3'] = 'bbb'
formdata.data['1'] = 'test 1'
formdata.data['3'] = 'test 2'
formdata.user_id = user.id
formdata.store()
@ -294,13 +278,88 @@ def test_tests_edit(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
assert 'aaa' in resp.text
assert 'bbb' in resp.text
assert 'test 1' in resp.text
assert 'test 2' in resp.text
resp = resp.click('Edit')
resp.form['f1'] = 'xxx'
resp.form['f1'] = 'test 3'
resp = resp.form.submit('submit')
resp = resp.form.submit('submit').follow() # change nothing on second page
assert 'aaa' not in resp.text
assert 'xxx' in resp.text
assert 'bbb' in resp.text
assert 'test 1' not in resp.text
assert 'test 3' in resp.text
assert 'test 2' in resp.text
def test_tests_manual_run(pub):
user = create_superuser(pub)
formdef = FormDef()
formdef.name = 'test title'
formdef.fields = [
fields.StringField(id='1', label='String', varname='string'),
]
formdef.store()
app = login(get_app(pub))
resp = app.get(formdef.get_admin_url() + 'tests/')
resp = resp.click('Test results')
assert 'No test results yet.' in resp.text
assert 'Run tests' not in resp.text
# create test
formdata = formdef.data_class()()
formdata.just_created()
formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
formdata.data['1'] = 'a'
formdata.user_id = user.id
formdata.store()
testdef = TestDef.create_from_formdata(formdef, formdata)
testdef.name = 'First test'
testdef.store()
resp = app.get('/backoffice/forms/1/tests/results/')
assert 'No test results yet.' in resp.text
resp = resp.click('Run tests')
assert resp.location == 'http://example.net/backoffice/forms/1/tests/results/1/'
resp = resp.follow()
assert 'Started by: Manual run.' in resp.text
assert len(resp.pyquery('tr')) == 1
assert 'Success!' in resp.text
resp = resp.click('First test')
assert 'Edit' in resp.text
resp = app.get('/backoffice/forms/1/tests/results/')
assert 'No test results yet.' not in resp.text
assert len(resp.pyquery('tr')) == 1
assert len(resp.pyquery('span.test-success')) == 1
assert len(resp.pyquery('span.test-failure')) == 0
# add required field
formdef.fields.append(fields.StringField(id='2', label='String', varname='string'))
formdef.store()
resp = resp.click('Run tests')
assert resp.location == 'http://example.net/backoffice/forms/1/tests/results/2/'
resp = app.get('/backoffice/forms/1/tests/results/')
assert len(resp.pyquery('tr')) == 2
assert len(resp.pyquery('span.test-success')) == 1
assert len(resp.pyquery('span.test-failure')) == 1
resp = resp.click('#2')
assert 'Started by: Manual run.' in resp.text
assert 'Success!' not in resp.text
assert 'Empty value for field' in resp.text
assert 'disabled' not in resp.text
TestDef.remove_object(testdef.id)
resp = app.get('/backoffice/forms/1/tests/results/2/')
assert 'disabled' in resp.text
# access unknown test result
app.get('/backoffice/forms/1/tests/results/42/', status=404)

View File

@ -15,7 +15,9 @@ from wcs.fields import CommentField, ItemField, PageField, StringField
from wcs.formdef import FormDef
from wcs.mail_templates import MailTemplate
from wcs.qommon.form import UploadedFile
from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.misc import localstrftime
from wcs.testdef import TestDef
from wcs.wf.form import WorkflowFormFieldsFormDef
from wcs.workflows import Workflow, WorkflowVariablesFieldsFormDef
from wcs.wscalls import NamedWsCall
@ -26,7 +28,9 @@ from .utilities import clean_temporary_pub, create_temporary_pub, get_app, login
@pytest.fixture
def pub(emails):
pub = create_temporary_pub(lazy_mode=True)
pub = create_temporary_pub()
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
pub.set_app_dir(req)
pub.cfg['identification'] = {'methods': ['password']}
pub.cfg['language'] = {'language': 'en'}
pub.write_cfg()
@ -1196,3 +1200,64 @@ def test_category_snapshot_browse(pub):
assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
with pytest.raises(IndexError):
resp = resp.click('Edit')
def test_snapshots_test_results(pub):
user = create_superuser(pub)
pub.site_options.set('options', 'enable-tests', 'true')
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
pub.site_options.write(fd)
formdef = FormDef()
formdef.name = 'test title'
formdef.fields = [
StringField(id='1', label='String', varname='string'),
]
formdef.store()
app = login(get_app(pub))
# make a change while there are no tests
resp = app.get('/backoffice/forms/1/fields/1/')
resp.form['label'] = 'New label'
resp.form.submit('submit').follow()
resp = app.get('/backoffice/forms/1/history/')
assert 'New label' in resp.text
assert '/tests/results/' not in resp.text
assert resp.pyquery('td.test-result').text().strip() == ''
# create test
formdata = formdef.data_class()()
formdata.just_created()
formdata.data['1'] = 'a'
formdata.user_id = user.id
formdata.store()
testdef = TestDef.create_from_formdata(formdef, formdata)
testdef.name = 'First test'
testdef.store()
# add field
resp = app.get('/backoffice/forms/1/fields/')
resp.forms[0]['label'] = 'Foobar'
resp.forms[0]['type'] = 'string'
resp.forms[0].submit().follow()
# field is required, tests failed
resp = app.get('/backoffice/forms/1/history/')
assert 'Foobar' in resp.text
assert '/tests/results/' in resp.text
assert len(resp.pyquery('span.test-failure')) == 1
assert len(resp.pyquery('span.test-success')) == 0
# make field optional
resp = app.get('/backoffice/forms/1/fields/2/')
resp.form['required'] = False
resp.form.submit('submit').follow()
resp = app.get('/backoffice/forms/1/history/')
assert 'Foobar' in resp.text
assert len(resp.pyquery('span.test-failure')) == 1
assert len(resp.pyquery('span.test-success')) == 1

View File

@ -20,7 +20,7 @@ from wcs import compat, custom_views, sessions, sql
from wcs.qommon import force_str
from wcs.qommon.tokens import Token
from wcs.roles import Role
from wcs.testdef import TestDef
from wcs.testdef import TestDef, TestResult
from wcs.tracking_code import TrackingCode
from wcs.users import User
@ -167,6 +167,7 @@ def create_temporary_pub(pickle_mode=False, lazy_mode=False):
sql.Audit.do_table()
sql.do_meta_table()
TestDef.do_table()
TestResult.do_table()
sql.WorkflowTrace.do_table()
sql.init_global_table()

View File

@ -563,6 +563,7 @@ class FieldsDirectory(Directory):
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<h2>%s') % self.objectdef.name
r += utils.last_test_result_block(self.objectdef)
if self.page_id:
current_page_no = 0
for field in self.objectdef.fields:

View File

@ -697,7 +697,9 @@ class FormDefPage(Directory):
DateWidget.prepare_javascript()
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % self.formdef.name
r += htmltext('<h2>%s') % self.formdef.name
r += utils.last_test_result_block(self.formdef)
r += htmltext('</h2>')
r += htmltext('<span class="actions">')
if not self.formdef.is_readonly():
r += htmltext('<a rel="popup" href="title">%s</a>') % _('change title')

View File

@ -17,18 +17,21 @@
import json
from django.template.loader import render_to_string
from quixote import get_response, get_session, redirect
from django.utils.timezone import now
from quixote import get_publisher, get_request, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.backoffice.management import FormBackofficeEditPage, FormBackOfficeStatusPage
from wcs.forms.common import FormStatusPage
from wcs.qommon import _, template
from wcs.qommon import _, misc, template
from wcs.qommon.afterjobs import AfterJob
from wcs.qommon.backoffice.listing import pagination_links
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.errors import TraversalError
from wcs.qommon.form import FileWidget, Form, SingleSelectWidget, StringWidget
from wcs.qommon.storage import Equal, StrictNotEqual
from wcs.testdef import TestDef, TestError
from wcs.testdef import TestDef, TestError, TestResult
class TestPage(FormBackOfficeStatusPage):
@ -97,11 +100,12 @@ class TestPage(FormBackOfficeStatusPage):
class TestsDirectory(Directory):
_q_exports = ['', 'new', 'run', ('import', 'p_import')]
_q_exports = ['', 'new', ('import', 'p_import'), 'results']
section = 'tests'
def __init__(self, objectdef):
self.objectdef = objectdef
self.results = TestResultsDirectory(objectdef)
def _q_traverse(self, path):
get_response().breadcrumb.append(('tests/', _('Tests')))
@ -122,18 +126,20 @@ class TestsDirectory(Directory):
for x in self.objectdef.fields
),
}
return template.QommonTemplateResponse(templates=['wcs/backoffice/tests.html'], context=context)
get_response().add_javascript(['popup.js'])
return template.QommonTemplateResponse(
templates=['wcs/backoffice/tests.html'], context=context, is_django_native=True
)
def new(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
formadata_options = sorted(
[
(x.id, '%s - %s' % (x.id_display, x.user or _('Unknown User')))
for x in self.objectdef.data_class().select([StrictNotEqual('status', 'draft')])
],
key=lambda x: x[1],
)
formadata_options = [
(x.id, '%s - %s' % (x.id_display, x.user or _('Unknown User')))
for x in self.objectdef.data_class().select(
[StrictNotEqual('status', 'draft')], order_by='-receipt_time'
)
]
form.add(SingleSelectWidget, 'formdata_id', title=_('Form'), required=True, options=formadata_options)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
@ -157,22 +163,6 @@ class TestsDirectory(Directory):
return redirect('.')
def run(self):
get_response().breadcrumb.append(('run', _('Run tests')))
testdef = TestDef.select(
[Equal('object_type', self.objectdef.get_table_name()), Equal('object_id', self.objectdef.id)]
)
for test in testdef:
try:
test.run(self.objectdef)
except TestError as e:
test.error = e
return template.QommonTemplateResponse(
templates=['wcs/backoffice/test_results.html'], context={'tests': testdef}
)
def p_import(self):
form = Form(enctype='multipart/form-data')
@ -209,3 +199,130 @@ class TestsDirectory(Directory):
get_session().message = ('info', _('Test "%s" has been successfully imported.') % testdef.name)
return redirect('.')
class TestResultPage(Directory):
_q_exports = ['']
def __init__(self, component, objectdef):
try:
self.test_result = TestResult.get(component)
except KeyError:
raise TraversalError()
self.objectdef = objectdef
def _q_traverse(self, path):
get_response().breadcrumb.append(
(str(self.test_result.id) + '/', _('Result #%s') % self.test_result.id)
)
return super()._q_traverse(path)
def _q_index(self):
testdefs = TestDef.select(
[Equal('object_type', self.objectdef.get_table_name()), Equal('object_id', self.objectdef.id)]
)
testdefs_by_id = {x.id: x for x in testdefs}
for test in self.test_result.results:
if test['id'] in testdefs_by_id:
test['url'] = testdefs_by_id[test['id']].get_admin_url()
return template.QommonTemplateResponse(
templates=['wcs/backoffice/test-result.html'],
context={'test_result': self.test_result},
is_django_native=True,
)
class TestResultsDirectory(Directory):
_q_exports = ['', 'run']
section = 'test_results'
def __init__(self, objectdef):
self.objectdef = objectdef
def _q_traverse(self, path):
get_response().breadcrumb.append(('results/', _('Test results')))
html_top('test-results', '%s - %s' % (self.objectdef.name, _('Test results')))
return super()._q_traverse(path)
def _q_lookup(self, component):
return TestResultPage(component, self.objectdef)
def _q_index(self):
criterias = [
Equal('object_type', self.objectdef.get_table_name()),
Equal('object_id', self.objectdef.id),
]
offset = misc.get_int_or_400(get_request().form.get('offset', 0))
limit = misc.get_int_or_400(get_request().form.get('limit', 25))
total_count = TestResult.count(criterias)
context = {
'test_results': TestResult.select(criterias, offset=offset, limit=limit, order_by='-id'),
'has_testdefs': bool(TestDef.count(criterias)),
'pagination_links': pagination_links(offset, limit, total_count, load_js=False),
}
return template.QommonTemplateResponse(
templates=['wcs/backoffice/test-results.html'], context=context, is_django_native=True
)
def run(self):
test_result = TestsAfterJob.run_tests(self.objectdef, _('Manual run.'))
return redirect(test_result.get_admin_url())
class TestsAfterJob(AfterJob):
def __init__(self, objectdef, reason, snapshot=None, **kwargs):
super().__init__(
objectdef_class=objectdef.__class__,
objectdef_id=objectdef.id,
reason=reason,
snapshot_id=snapshot.id if snapshot else None,
**kwargs,
)
def execute(self):
objectdef = self.kwargs['objectdef_class'].get(self.kwargs['objectdef_id'])
reason = self.kwargs['reason']
result = self.run_tests(objectdef, reason)
if result and self.kwargs['snapshot_id'] is not None:
snapshot = get_publisher().snapshot_class.get(self.kwargs['snapshot_id'])
snapshot.test_result_id = result.id
snapshot.store()
@staticmethod
def run_tests(objectdef, reason):
testdefs = TestDef.select(
[Equal('object_type', objectdef.get_table_name()), Equal('object_id', objectdef.id)]
)
if not testdefs:
return
for test in testdefs:
try:
test.run(objectdef)
except TestError as e:
test.error = str(e)
test_result = TestResult()
test_result.object_type = objectdef.get_table_name()
test_result.object_id = objectdef.id
test_result.timestamp = now()
test_result.success = not any(hasattr(test, 'error') for test in testdefs)
test_result.reason = str(reason)
test_result.results = [
{
'id': test.id,
'name': str(test),
'error': getattr(test, 'error', None),
}
for test in testdefs
]
test_result.results.sort(key=lambda x: bool(x['error']))
test_result.store()
return test_result

View File

@ -16,11 +16,13 @@
import time
from django.template.loader import render_to_string
from quixote import get_publisher, get_request
from quixote.html import TemplateIO, htmltext
from wcs.qommon import _
from wcs.qommon.misc import localstrftime
from wcs.qommon.storage import Equal
def last_modification_block(obj):
@ -88,3 +90,24 @@ def snapshot_info_block(snapshot, url_prefix='../../', url_suffix=''):
r += htmltext(' <a class="button disabled" href="#">&Gt;</a>')
r += htmltext('</p>')
return r.getvalue()
def last_test_result_block(objectdef):
from wcs.testdef import TestResult
if not get_publisher().has_site_option('enable-tests'):
return ''
test_results = TestResult.select(
[
Equal('object_type', objectdef.get_table_name()),
Equal('object_id', objectdef.id),
],
order_by='-id',
)
if not test_results:
return ''
context = {'result': test_results[0], 'add_link': True}
return ' ' + htmltext(render_to_string('wcs/backoffice/includes/test-result-fragment.html', context))

View File

@ -28,6 +28,7 @@ from wcs.formdef import FormdefImportError
from wcs.qommon import _, errors, misc, template
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.form import Form, RadiobuttonsWidget, StringWidget
from wcs.qommon.storage import Equal
from wcs.workflows import WorkflowImportError
@ -47,7 +48,8 @@ class SnapshotsDirectory(Directory):
def _q_index(self):
html_top('', _('History'))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/snapshots.html'], context={'view': self}
templates=['wcs/backoffice/snapshots.html'],
context={'view': self, 'enable_tests': get_publisher().has_site_option('enable-tests')},
)
def save(self):
@ -213,8 +215,14 @@ class SnapshotsDirectory(Directory):
}
def snapshots(self):
from wcs.testdef import TestResult
current_date = None
snapshots = get_publisher().snapshot_class.select_object_history(self.obj)
test_results = TestResult.select(
[Equal('object_type', self.obj.get_table_name()), Equal('object_id', self.obj.id)]
)
test_results_by_id = {x.id: x for x in test_results}
day_snapshot = None
for snapshot in snapshots:
if snapshot.timestamp.date() != current_date:
@ -224,6 +232,7 @@ class SnapshotsDirectory(Directory):
day_snapshot = snapshot
else:
day_snapshot.day_other_count += 1
snapshot.test_result = test_results_by_id.get(snapshot.test_result_id)
return snapshots
def _q_lookup(self, component):

View File

@ -20,7 +20,7 @@ import uuid
import xml.etree.ElementTree as ET
from contextlib import contextmanager
from quixote import get_publisher, get_request
from quixote import get_publisher, get_request, get_response
from quixote.html import htmltag, htmltext
from . import data_sources, fields
@ -96,6 +96,14 @@ class BlockDef(StorableObject):
for field in objdef.get_all_fields():
if field.key == 'block' and field.type == 'block:%s' % self.slug:
objdef.store()
if get_publisher().has_site_option('enable-tests') and get_response():
from wcs.admin.tests import TestsAfterJob
context = _('in field block "%s"') % field.label
get_response().add_after_job(
TestsAfterJob(objdef, reason='%s (%s)' % (comment, context))
)
break
def get_new_field_id(self):

View File

@ -2623,3 +2623,13 @@ div#main-content > h3.field-edit--subtitle {
.journal-table--datetime {
width: 10em;
}
span.test-success::before {
font-family: FontAwesome;
content: "\f00c"; /* check */
}
span.test-failure::before {
font-family: FontAwesome;
content: "\f00d"; /* times */
}

View File

@ -19,7 +19,7 @@ import re
import xml.etree.ElementTree as ET
from django.utils.timezone import now
from quixote import get_publisher, get_session
from quixote import get_publisher, get_response, get_session
from wcs.qommon import _, misc
@ -130,6 +130,7 @@ class Snapshot:
serialization = None
patch = None
label = None # (named snapshot)
test_result_id = None
# cache
_instance = None
@ -183,6 +184,13 @@ class Snapshot:
# else: keep serialization and ignore patch
obj.store()
if get_publisher().has_site_option('enable-tests') and get_response():
from wcs.admin.tests import TestsAfterJob
get_response().add_after_job(
TestsAfterJob(instance, reason=obj.label or obj.comment, snapshot=obj)
)
@classmethod
def get_recent_changes(cls, object_types=None, user=None, limit=5, offset=0):
elements = cls._get_recent_changes(object_types=object_types, user=user, limit=limit, offset=offset)

View File

@ -1389,7 +1389,8 @@ def do_snapshots_table():
comment TEXT,
serialization TEXT,
patch TEXT,
label VARCHAR
label VARCHAR,
test_result_id INTEGER
)'''
% table_name
)
@ -1401,9 +1402,10 @@ def do_snapshots_table():
)
existing_fields = {x[0] for x in cur.fetchall()}
# migrations
if 'patch' not in existing_fields:
cur.execute('''ALTER TABLE %s ADD COLUMN patch TEXT''' % table_name)
# generic migration for new columns
for field_name, field_type in Snapshot._table_static_fields:
if field_name not in existing_fields:
cur.execute('''ALTER TABLE %s ADD COLUMN %s %s''' % (table_name, field_name, field_type))
needed_fields = {x[0] for x in Snapshot._table_static_fields}
@ -3820,6 +3822,7 @@ class Snapshot(SqlMixin, wcs.snapshots.Snapshot):
('serialization', 'text'),
('patch', 'text'),
('label', 'varchar'),
('test_result_id', 'integer'),
]
_table_select_skipped_fields = ['serialization', 'patch']
@ -3867,7 +3870,7 @@ class Snapshot(SqlMixin, wcs.snapshots.Snapshot):
def _row2ob(cls, row, **kwargs):
o = cls()
for field, value in zip(cls._table_static_fields, tuple(row)):
if field[1] in ('serial', 'timestamptz'):
if field[1] in ('serial', 'timestamptz', 'integer'):
setattr(o, field[0], value)
elif field[1] in ('varchar', 'text'):
setattr(o, field[0], str_encode(value))
@ -4302,6 +4305,95 @@ class TestDef(SqlMixin):
return []
class TestResult(SqlMixin):
_table_name = 'test_result'
_table_static_fields = [
('id', 'serial'),
('object_type', 'varchar'),
('object_id', 'varchar'),
('timestamp', 'timestamptz'),
('success', 'boolean'),
('reason', 'varchar'),
('results', 'jsonb[]'),
]
id = None
@classmethod
@guard_postgres
def do_table(cls, conn=None, cur=None):
conn, cur = get_connection_and_cursor()
table_name = cls._table_name
cur.execute(
'''SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = %s''',
(table_name,),
)
if cur.fetchone()[0] == 0:
cur.execute(
'''CREATE TABLE %s (id SERIAL PRIMARY KEY,
object_type varchar NOT NULL,
object_id varchar NOT NULL,
timestamp timestamptz,
success boolean NOT NULL,
reason varchar NOT NULL,
results jsonb[]
)'''
% table_name
)
conn.commit()
cur.close()
@guard_postgres
def store(self):
sql_dict = {x[0]: getattr(self, x[0], None) for x in self._table_static_fields if x[0] != 'id'}
conn, cur = get_connection_and_cursor()
column_names = list(sql_dict.keys())
column_values = []
for name in column_names:
value = '%%(%s)s' % name
if name == 'results':
value += '::jsonb[]'
column_values.append(value)
if not self.id:
sql_statement = '''INSERT INTO %s (id, %s)
VALUES (DEFAULT, %s)
RETURNING id''' % (
self._table_name,
', '.join(column_names),
', '.join(column_values),
)
cur.execute(sql_statement, sql_dict)
self.id = cur.fetchone()[0]
else:
sql_dict['id'] = self.id
sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
self._table_name,
', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]),
)
cur.execute(sql_statement, sql_dict)
conn.commit()
cur.close()
@classmethod
def _row2ob(cls, row, **kwargs):
o = cls.__new__(cls)
for attr, value in zip([x[0] for x in cls._table_static_fields], row):
setattr(o, attr, value)
return o
@classmethod
def get_data_fields(cls):
return []
class WorkflowTrace(SqlMixin):
_table_name = 'workflow_traces'
_table_static_fields = [
@ -4966,7 +5058,7 @@ def get_period_total(
# latest migration, number + description (description is not used
# programmaticaly but will make sure git conflicts if two migrations are
# separately added with the same number)
SQL_LEVEL = (82, 'add statistics data column to wcs_all_forms, for real')
SQL_LEVEL = (83, 'add test_result table')
def migrate_global_views(conn, cur):
@ -5092,10 +5184,11 @@ def migrate():
raise RuntimeError()
if sql_level < 1: # 1: introduction of tracking_code table
do_tracking_code_table()
if sql_level < 63:
if sql_level < 83:
# 42: create snapshots table
# 54: add patch column
# 63: add index
# 83: add test_result table
do_snapshots_table()
if sql_level < 50:
# 49: store Role in SQL
@ -5148,6 +5241,9 @@ def migrate():
if sql_level < 78:
# 78: add audit table
Audit.do_table()
if sql_level < 83:
# 83: add test_result table
TestResult.do_table()
if sql_level < 52:
# 2: introduction of formdef_id in views
# 5: add concerned_roles_array, is_at_endpoint and fts to views

View File

@ -0,0 +1,11 @@
{% load i18n %}{% spaceless %}
{% if add_link %}<a href="{{ result.get_admin_url }}">{% endif %}
{% if result and result.success %}
<span class="test-success" title="{% trans "Success" %}" aria-label="{% trans "Success" %}"></span>
{% elif result %}
<span class="test-failure" title="{% trans "Failure" %}" aria-label="{% trans "Failure" %}"></span>
{% endif %}
{% if add_link %}</a>{% endif %}
{% endspaceless %}

View File

@ -22,6 +22,7 @@
<th colspan="3">{% trans "Description" %}</th>
<th>{% trans 'User' %}</th>
<th colspan="2">{% trans 'Actions' %}</th>
{% if enable_tests %}<th>{% trans "Tests" %}</th>{% endif %}
</thead>
<tbody class="snapshots-list">
{% for snapshot in snapshots %}
@ -58,6 +59,11 @@
<a href="{{snapshot.id}}/export">{% trans "Export" %}</a>
</td>
{% if enable_tests %}
<td class="test-result">
{% include "wcs/backoffice/includes/test-result-fragment.html" with result=snapshot.test_result add_link=True %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,35 @@
{% extends "wcs/backoffice.html" %}
{% load i18n %}
{% block appbar-title %}{% trans "Result" %} #{{ test_result.id }}{% endblock %}
{% block body %}
<div class="section">
<h3>{% trans "Details" %}</h3>
<div>
<ul>
<li>{% trans "Started by:" %} {{ test_result.reason }}</li>
<li>{% trans "Date:" %} {{ test_result.timestamp }}</li>
</ul>
</div>
</div>
<div class="section">
<h3>Result</h3>
<div>
<table class="main">
<thead>
<th>{% trans "Name" %}</th>
<th>{% trans "Result" %}</th>
</thead>
<tbody>
{% for test in test_result.results %}
<tr>
<td><a {% if test.url %}href="{{ test.url }}"{% else %}disabled{% endif %}>{{ test.name }}</a></td>
<td>{% firstof test.error _("Success!") %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "wcs/backoffice.html" %}
{% load i18n %}
{% block appbar-title %}{% trans "Test results" %}{% endblock %}
{% block appbar-actions %}
{% if has_testdefs %}
<a href="run">{% trans "Run tests" %}</a>
{% endif %}
{% endblock %}
{% block body %}
{% if test_results %}
<div class="section">
<table class="main">
<thead>
<th>{% trans "Identifier" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Started by" %}</th>
<th>{% trans "Status" %}</th>
</thead>
<tbody>
{% for result in test_results %}
<tr>
<td><a href="{{ result.id }}/">#{{ result.id }}</a></td>
<td>{{ result.timestamp }}</td>
<td>{{ result.reason }}</td>
<td>{% include "wcs/backoffice/includes/test-result-fragment.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ pagination_links|safe }}
</div>
{% else %}
<div class="infonotice"><p>{% trans "No test results yet." %}</p></div>
{% endif %}
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "wcs/backoffice/base.html" %}
{% load i18n %}
{% block appbar-title %}{% trans "Tests results" %}{% endblock %}
{% block content %}
<div class="section">
<ul>
{% for test in tests %}
<li>{{ test }}{% trans ":" %} {% firstof test.error _("Success!") %}</li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -1,18 +1,19 @@
{% extends "wcs/backoffice/base.html" %}
{% extends "wcs/backoffice.html" %}
{% load i18n %}
{% block appbar-title %}{% trans "Tests" %}{% endblock %}
{% block appbar-actions %}
<a href="import" rel="popup">{% trans "Import" %}</a>
{% if testdefs %}
<a href="run">{% trans "Run tests" %}</a>
{% endif %}
<a class="extra-actions-menu-opener"></a>
<a href="results/">{% trans "Test results" %}</a>
{% if formdata and not has_deprecated_fields %}
<a href="new" rel="popup">{% trans "New" %}</a>
{% endif %}
<ul class="extra-actions-menu">
<li><a href="import" rel="popup">{% trans "Import" %}</a></li>
</ul>
{% endblock %}
{% block content %}
{% block body %}
<div class="section">
<h3>{% trans "Test form data" %}</h3>

View File

@ -48,6 +48,10 @@ class TestDef(sql.TestDef):
def __str__(self):
return self.name
def get_admin_url(self):
base_url = get_publisher().get_backoffice_url()
return '%s/forms/%s/tests/%s/' % (base_url, self.object_id, self.id)
def store(self, *args, comment=None, snapshot_store_user=True, **kwargs):
if not self.slug:
existing_slugs = {
@ -302,3 +306,18 @@ class TestDef(sql.TestDef):
testdef.store()
return testdef
class TestResult(sql.TestResult):
_names = 'test_result'
object_type = None # (formdef, carddef, etc.)
object_id = None
timestamp = None
success = None
reason = None # reason for tests execution
results = None # results for each test associated to object
def get_admin_url(self):
base_url = get_publisher().get_backoffice_url()
return '%s/forms/%s/tests/results/%s/' % (base_url, self.object_id, self.id)