# -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2010-2012 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## """ 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__) 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. """ if data is None: data = {} if context is None: context = {} if rname.startswith('report.'): rname_s = rname[7:] else: rname_s = rname _logger.log(logging.TEST, " - Trying %s.create(%r)", rname, ids) res = openerp.report.render_report(cr, uid, ids, rname_s, data, 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 == 'foobar': # TODO pass else: _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format) return False _logger.log(logging.TEST, " + 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 '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 registry = openerp.registry(cr.dbname) def log_test(msg, *args): _logger.log(logging.TEST, " - " + 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_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 = registry['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 = registry[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 = registry[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) _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') == '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: _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'] = registry[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']) _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! fn = getattr(registry[datas['res_model']], b['name']) if not fn: _logger.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: _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',{}) 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 = registry[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: _logger.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 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: