diff options
| author | Raphael Collet <rco@openerp.com> | 2016-09-02 15:58:39 +0200 |
|---|---|---|
| committer | Raphael Collet <rco@openerp.com> | 2016-09-02 17:28:12 +0200 |
| commit | 9e64f9f9514194d74e166b032add8511182f2677 (patch) | |
| tree | 5086710c04c6b923be18a6d1d97b3f0f3a2a9c4a /odoo/tools/test_reports.py | |
| parent | 11812b0b9e82b2b1b9041d8250ce01e7b4a5f7ee (diff) | |
[REF] openerp: move `openerp` to `odoo`
Diffstat (limited to 'odoo/tools/test_reports.py')
| -rw-r--r-- | odoo/tools/test_reports.py | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/odoo/tools/test_reports.py b/odoo/tools/test_reports.py new file mode 100644 index 00000000000..ab924b5b4d1 --- /dev/null +++ b/odoo/tools/test_reports.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +""" Helper functions for reports testing. + + Please /do not/ import this file by default, but only explicitly call it + through the code of yaml tests. +""" + +import openerp +import openerp.report +import openerp.tools as tools +import logging +from openerp.tools.safe_eval import safe_eval +from subprocess import Popen, PIPE +import os +import tempfile + +_logger = logging.getLogger(__name__) +_test_logger = logging.getLogger('openerp.tests') + + +def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, report_type=None): + """ Try to render a report <rname> with contents of ids + + This function should also check for common pitfalls of reports. + """ + if data is None: + data = {} + if context is None: + context = {} + if rname.startswith('report.'): + rname_s = rname[7:] + else: + rname_s = rname + _test_logger.info(" - Trying %s.create(%r)", rname, ids) + + res = openerp.report.render_report(cr, uid, ids, rname_s, data, context=context) + if not isinstance(res, tuple): + raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \ + (rname, type(res))) + (res_data, res_format) = res + + if not res_data: + raise ValueError("Report %s produced an empty result!" % rname) + + if tools.config['test_report_directory']: + file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data) + + _logger.debug("Have a %s report for %s, will examine it", res_format, rname) + if res_format == 'pdf': + if res_data[:5] != '%PDF-': + raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10])) + + res_text = False + try: + fd, rfname = tempfile.mkstemp(suffix=res_format) + os.write(fd, res_data) + os.close(fd) + + proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE) + stdout, stderr = proc.communicate() + res_text = tools.ustr(stdout) + os.unlink(rfname) + except Exception: + _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.") + + if res_text is not False: + for line in res_text.split('\n'): + if ('[[' in line) or ('[ [' in line): + _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:]) + # TODO more checks, what else can be a sign of a faulty report? + elif res_format == 'html': + pass + else: + _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format) + return False + + _test_logger.info(" + Report %s produced correctly.", rname) + return True + +def try_report_action(cr, uid, action_id, active_model=None, active_ids=None, + wiz_data=None, wiz_buttons=None, + context=None, our_module=None): + """Take an ir.action.act_window and follow it until a report is produced + + :param action_id: the integer id of an action, or a reference to xml id + of the act_window (can search [our_module.]+xml_id + :param active_model, active_ids: call the action as if it had been launched + from that model+ids (tree/form view action) + :param wiz_data: a dictionary of values to use in the wizard, if needed. + They will override (or complete) the default values of the + wizard form. + :param wiz_buttons: a list of button names, or button icon strings, which + should be preferred to press during the wizard. + Eg. 'OK' or 'fa-print' + :param our_module: the name of the calling module (string), like 'account' + """ + if not our_module and isinstance(action_id, basestring): + if '.' in action_id: + our_module = action_id.split('.', 1)[0] + + context = dict(context or {}) + # TODO context fill-up + + env = openerp.api.Environment(cr, uid, context) + + def log_test(msg, *args): + _test_logger.info(" - " + msg, *args) + + datas = {} + if active_model: + datas['model'] = active_model + if active_ids: + datas['ids'] = active_ids + + if not wiz_buttons: + wiz_buttons = [] + + if isinstance(action_id, basestring): + if '.' in action_id: + _, act_xmlid = action_id.split('.', 1) + else: + if not our_module: + raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id) + act_xmlid = action_id + action_id = '%s.%s' % (our_module, action_id) + action = env.ref(action_id) + act_model, act_id = action._name, action.id + else: + assert isinstance(action_id, (long, int)) + act_model = 'ir.action.act_window' # assume that + act_id = action_id + act_xmlid = '<%s>' % act_id + + def _exec_action(action, datas, env): + # taken from client/modules/action/main.py:84 _exec_action() + if isinstance(action, bool) or 'type' not in action: + return + # Updating the context : Adding the context of action in order to use it on Views called from buttons + context = dict(env.context) + if datas.get('id',False): + context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)}) + context1 = action.get('context', {}) + if isinstance(context1, basestring): + context1 = safe_eval(context1, dict(context)) + context.update(context1) + env = env(context=context) + if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']: + for key in ('res_id', 'res_model', 'view_type', 'view_mode', + 'limit', 'search_view', 'auto_search', 'search_view_id'): + datas[key] = action.get(key, datas.get(key, None)) + + view_id = False + if action.get('views', []): + if isinstance(action['views'],list): + view_id = action['views'][0][0] + datas['view_mode']= action['views'][0][1] + else: + if action.get('view_id', False): + view_id = action['view_id'][0] + elif action.get('view_id', False): + view_id = action['view_id'][0] + + assert datas['res_model'], "Cannot use the view without a model" + # Here, we have a view that we need to emulate + log_test("will emulate a %s view: %s#%s", + action['view_type'], datas['res_model'], view_id or '?') + + view_res = env[datas['res_model']].fields_view_get(view_id, action['view_type']) + assert view_res and view_res.get('arch'), "Did not return any arch for the view" + view_data = {} + if view_res.get('fields'): + view_data = env[datas['res_model']].default_get(list(view_res['fields'])) + if datas.get('form'): + view_data.update(datas.get('form')) + if wiz_data: + view_data.update(wiz_data) + _logger.debug("View data is: %r", view_data) + + for fk, field in view_res.get('fields',{}).items(): + # Default fields returns list of int, while at create() + # we need to send a [(6,0,[int,..])] + if field['type'] in ('one2many', 'many2many') \ + and view_data.get(fk, False) \ + and isinstance(view_data[fk], list) \ + and not isinstance(view_data[fk][0], tuple) : + view_data[fk] = [(6, 0, view_data[fk])] + + action_name = action.get('name') + try: + from xml.dom import minidom + cancel_found = False + buttons = [] + dom_doc = minidom.parseString(view_res['arch']) + if not action_name: + action_name = dom_doc.documentElement.getAttribute('name') + + for button in dom_doc.getElementsByTagName('button'): + button_weight = 0 + if button.getAttribute('special') == 'cancel': + cancel_found = True + continue + if button.getAttribute('icon') == 'fa-times-circle': + cancel_found = True + continue + if button.getAttribute('default_focus') == '1': + button_weight += 20 + if button.getAttribute('string') in wiz_buttons: + button_weight += 30 + elif button.getAttribute('icon') in wiz_buttons: + button_weight += 10 + string = button.getAttribute('string') or '?%s' % len(buttons) + + buttons.append({ + 'name': button.getAttribute('name'), + 'string': string, + 'type': button.getAttribute('type'), + 'weight': button_weight, + }) + except Exception, e: + _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True) + raise AssertionError(e.args[0]) + + if not datas['res_id']: + # it is probably an orm_memory object, we need to create + # an instance + datas['res_id'] = env[datas['res_model']].create(view_data).id + + if not buttons: + raise AssertionError("view form doesn't have any buttons to press!") + + buttons.sort(key=lambda b: b['weight']) + _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons])) + + res = None + while buttons and not res: + b = buttons.pop() + log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string']) + if not b['type']: + log_test("the \"%s\" button has no type, cannot use it", b['string']) + continue + if b['type'] == 'object': + #there we are! press the button! + rec = env[datas['res_model']].browse(datas['res_id']) + func = getattr(rec, b['name'], None) + if not func: + _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name']) + continue + res = func() + break + else: + _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s", + action_name, b['string'], b['type']) + return res + + elif action['type']=='ir.actions.report.xml': + if 'window' in datas: + del datas['window'] + if not datas: + datas = action.get('datas') + if not datas: + datas = action.get('data') + datas = datas.copy() + ids = datas.get('ids') + if 'ids' in datas: + del datas['ids'] + res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module) + return res + else: + raise Exception("Cannot handle action of type %s" % act_model) + + log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id) + action = env[act_model].browse(act_id).read()[0] + assert action, "Could not read action %s[%s]" % (act_model, act_id) + loop = 0 + while action: + loop += 1 + # This part tries to emulate the loop of the Gtk client + if loop > 100: + _logger.info("Passed %d loops, giving up", loop) + raise Exception("Too many loops at action") + log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop) + result = _exec_action(action, datas, env) + if not isinstance(result, dict): + break + datas = result.get('datas', {}) + if datas: + del result['datas'] + action = result + + return True |
