From 5918f087330bc9e6666b3fe13bfdba2815dfc9ee Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Sun, 19 Dec 2010 22:06:08 +0200 Subject: [PATCH] test_reports: first draft of a full action/wizard/report tester Use with a yaml test like: - I will test the action reports - !python {model: res.partner}: | from tools import test_reports reload(test_reports) # ;) test_reports.try_report_action(cr, uid, 'action_account_balance_menu', our_module='account') bzr revid: p_christ@hol.gr-20101219200608-ffl2k3qiexjxvp48 --- bin/tools/test_reports.py | 243 +++++++++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 3 deletions(-) diff --git a/bin/tools/test_reports.py b/bin/tools/test_reports.py index b6d0ec6b714..c52985df8c7 100644 --- a/bin/tools/test_reports.py +++ b/bin/tools/test_reports.py @@ -28,16 +28,21 @@ import netsvc import tools import logging +import pooler +from tools.safe_eval import safe_eval from subprocess import Popen, PIPE import os import tempfile -def try_report(cr, uid, rname, ids, data=None, context=None): +def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None): """ Try to render a report with contents of ids This function should also check for common pitfalls of reports. """ - log = logging.getLogger('tools.test_reports') + if our_module: + log = logging.getLogger('tests.%s' % our_module) + else: + log = logging.getLogger('tools.test_reports') if data is None: data = {} if context is None: @@ -46,7 +51,7 @@ def try_report(cr, uid, rname, ids, data=None, context=None): rname_s = rname[7:] else: rname_s = rname - log.debug("Trying %s.create(%r)", rname, ids) + log.log(netsvc.logging.TEST, " - Trying %s.create(%r)", rname, ids) res = netsvc.LocalService(rname).create(cr, uid, ids, data, context) if not isinstance(res, tuple): raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \ @@ -86,6 +91,238 @@ def try_report(cr, uid, rname, ids, data=None, context=None): pass else: log.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format) + return False + log.log(netsvc.logging.TEST, "Report %s produced correctly.", rname) return True + +def try_report_action(cr, uid, action_id, res_model=None, res_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 res_model, res_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 'gtk-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] + + if context is None: + context = {} + else: + context = context.copy() # keep it local + # TODO context fill-up + + pool = pooler.get_pool(cr.dbname) + if our_module: + log = logging.getLogger('tests.%s' % our_module) + else: + log = logging.getLogger('tools.test_reports') + + def log_test(msg, *args): + log.log(netsvc.logging.TEST, " - " + msg, *args) + + datas = {} + if res_model: + datas['model'] = res_model + if res_ids: + datas['ids'] = res_ids + + if not wiz_buttons: + wiz_buttons = [] + + if isinstance(action_id, basestring): + if '.' in action_id: + act_module, 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_module = our_module + act_xmlid = action_id + act_model, act_id = pool.get('ir.model.data').get_object_reference(cr, uid, act_module, act_xmlid) + 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, context): + # 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 + if datas.get('id',False): + context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)}) + context.update(safe_eval(action.get('context','{}'), context.copy())) + if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']: + for key in ('res_id', 'res_model', 'view_type', 'view_mode', + 'limit', 'auto_refresh', '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 = pool.get(datas['res_model']).fields_view_get(cr, uid, view_id, action['view_type'], context) + assert view_res and view_res.get('arch'), "Did not return any arch for the view" + view_data = {} + if view_res.get('fields',{}).keys(): + view_data = pool.get(datas['res_model']).default_get(cr, uid, view_res['fields'].keys(), context) + if datas.get('form'): + view_data.update(datas.get('form')) + if wiz_data: + view_data.update(wiz_data) + log.debug("View data is: %r", view_data) + + 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') == 'gtk-cancel': + 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: + log.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'] = pool.get(datas['res_model']).create(cr, uid, view_data, context) + + if not buttons: + raise AssertionError("view form doesn't have any buttons to press!") + + buttons.sort(key=lambda b: b['weight']) + log.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! + fn = getattr(pool.get(datas['res_model']), b['name']) + if not fn: + log.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name']) + continue + res = fn(cr, uid, [datas['res_id'],], context) + break + else: + log.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.server': + #res = rpc.session.rpc_exec_auth('/object', 'execute', 'ir.actions.server', 'run', [action['id']], context) + #if res: + #if not isinstance(res, list): + #res = [res] + #for r in res: + #self._exec_action(r, datas, context) + + #elif action['type']=='ir.actions.wizard': + #win=None + #if 'window' in datas: + #win=datas['window'] + #del datas['window'] + #wizard.execute(action['wiz_name'], datas, parent=win, context=context) + + #elif action['type']=='ir.actions.report.custom': + #if 'window' in datas: + #win=datas['window'] + #del datas['window'] + #datas['report_id'] = action['report_id'] + #self.exec_report('custom', datas, context) + + #elif action['type']=='ir.actions.report.int': + #if 'window' in datas: + #win=datas['window'] + #del datas['window'] + #self.exec_report(action['report_name'], datas) + + elif action['type']=='ir.actions.report.xml': + if 'window' in datas: + del datas['window'] + if not datas: + datas = action.get('datas',{}) + 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 = pool.get(act_model).read(cr, uid, act_id, context=context) + 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: + log.error("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, context) + if not isinstance(result, dict): + break + datas = result.get('datas', {}) + if datas: + del result['datas'] + action = result + + return True + + #eof