testXHTML.py 34.3 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4
##############################################################################
#
# Copyright (c) 2007 Nexedi SARL and Contributors. All Rights Reserved.
5 6
#               Fabien Morin <fabien@nexedi.com
#               Jacek Medrzycki <jacek@erp5.pl>
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################

30 31
import unittest
import os
32
import urllib
33
import requests
34

Lingnan Wu's avatar
Lingnan Wu committed
35
from subprocess import Popen, PIPE
36
from Testing import ZopeTestCase
37 38
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.CMFCore.utils import getToolByName
39 40
from zLOG import LOG
from xml.dom import minidom
41 42 43 44 45 46 47
# You can invoke same tests in your favourite collection of business templates
# by using TestXHTMLMixin like the following :
#
# from Products.ERP5.tests.testERP5XHTML import TestXHTMLMixin
# class TestMyXHTML(TestXHTMLMixin):
#   def getBusinessTemplateList(self):
#     return (...)
48

49
class TestXHTMLMixin(ERP5TypeTestCase):
50

51
  # some forms have intentionally empty listbox selections like RSS generators
52
  FORM_LISTBOX_EMPTY_SELECTION_PATH_LIST = ['erp5_web_widget_library/WebSection_viewContentListAsRSS',
53
                                            'erp5_diff/ERP5Site_viewDiffTwoObjectDialog',]
54
  JSL_IGNORE_FILE_LIST = (
55 56
        'diff2html.js',
        'diff2html-ui.js',
57 58 59 60 61 62 63 64 65
        'dream_graph_editor/lib/handlebars.min.js',
        'dream_graph_editor/lib/jquery-ui.js',
        'dream_graph_editor/lib/jquery.js',
        'dream_graph_editor/lib/jquery.jsplumb.js',
        'dream_graph_editor/lib/jquery.simulate.js',
        'dream_graph_editor/lib/qunit.js',
        'dream_graph_editor/lib/springy.js',
        'handlebars.js',
        'jio.js',
66
        'jslint.js',
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
        'pdf_js/build/pdf.js',
        'pdf_js/build/pdf.worker.js',
        'pdf_js/compatibility.js',
        'pdf_js/debugger.js',
        'pdf_js/l10n.js',
        'pdf_js/viewer.js',
        'renderjs.js',
        'require.js',
        'require.min.js',
        'rsvp.js',
        'wz_dragdrop.js',
        )
  JSL_IGNORE_SKIN_LIST = (
        'erp5_ace_editor',
        'erp5_code_mirror',
        'erp5_fckeditor',
83
        'erp5_ckeditor',
84 85
        'erp5_jquery',
        'erp5_jquery_ui',
86 87 88
        'erp5_pivot_table',
        'erp5_sql_browser',
        'erp5_dhtmlx_scheduler',
89 90
        'erp5_svg_editor',
        )
91

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
  HTML_IGNORE_FILE_LIST = (
        'gadget_erp5_side_by_side_diff.html',
        )
  # NOTE: Here the difference between the JSL_IGNORE_SKIN_LIST is that we also
  # consider the folders inside the skin. In this way, we can include multiple
  # HTML files at once which are inside some folder in any skin folder.
  HTML_IGNORE_SKIN_FOLDER_LIST = (
        'erp5_jquery',
        'erp5_fckeditor',
        'erp5_ckeditor',
        'erp5_svg_editor',
        'erp5_jquery_ui',
        'erp5_dms/pdf_js',
        'erp5_test_result/test_result_js',
        )

108 109 110 111 112 113 114 115
  def changeSkin(self, skin_name):
    """
      Change current Skin
    """
    request = self.app.REQUEST
    self.getPortal().portal_skins.changeSkin(skin_name)
    request.set('portal_skin', skin_name)

116 117 118 119 120 121 122 123 124 125 126 127
  def getFieldList(self, form, form_path):
    try:
      for field in form.get_fields(include_disabled=1):
        if field.getTemplateField() is not None:
          try:
            if field.get_value('enabled'):
              yield field
          except Exception:
            yield field
    except AttributeError, e:
      ZopeTestCase._print("%s is broken: %s" % (form_path, e))

128 129 130 131
  def test_deadProxyFields(self):
    # check that all proxy fields defined in business templates have a valid
    # target
    skins_tool = self.portal.portal_skins
132
    error_list = []
133 134 135 136 137 138 139

    for skin_name, skin_folder_string in skins_tool.getSkinPaths():
      skin_folder_id_list = skin_folder_string.split(',')
      self.changeSkin(skin_name)

      for skin_folder_id in skin_folder_id_list:
        for field_path, field in skins_tool[skin_folder_id].ZopeFind(
140
                  skins_tool[skin_folder_id],
141 142 143
                  obj_metatypes=['ProxyField'], search_sub=1):
          template_field = field.getTemplateField(cache=False)
          if template_field is None:
Jérome Perrin's avatar
Jérome Perrin committed
144
            # Base_viewRelatedObjectList (used for proxy listbox ids on
145 146
            # relation fields) is an exception, the proxy field has no target
            # by default.
Jérome Perrin's avatar
Jérome Perrin committed
147
            if field_path != 'Base_viewRelatedObjectList/listbox':
148 149
              error_list.append((skin_name, field_path, field.get_value('form_id'),
                                 field.get_value('field_id')))
150 151

    if error_list:
152 153
      message = '\nDead proxy field list%s\n' \
                    % '\n\t'.join(str(e) for e in error_list)
154
      self.fail(message)
155

156 157
  def test_configurationOfFieldLibrary(self):
    error_list = []
158 159
    for business_template in self.portal.portal_templates.searchFolder(
          title=['erp5_trade']):
160 161 162
      # XXX Impossible to filter by installation state, as it is not catalogued
      business_template = business_template.getObject()
      for modifiable_field in business_template.BusinessTemplate_getModifiableFieldList():
163
        # Do not consider 'Check delegated values' as an error
164 165
        if modifiable_field.choice_item_list[0][1] not in \
            ("0_check_delegated_value", "0_keep_non_proxy_field"):
166 167
          error_list.append((modifiable_field.object_id,
                            modifiable_field.choice_item_list[0][0]))
168
    if error_list:
169
      message = '%s fields to modify' % len(error_list)
170 171 172
      message += '\n\t' + '\n\t'.join(fieldname + ": " + message
                                       for fieldname, message in error_list)
      self.fail(message)
173

174 175 176 177 178 179 180 181
  def test_portalTypesDomainTranslation(self):
    # according to bt5-Module.Creation.Guidelines document, module
    # portal_types should be translated using erp5_ui, and normal ones, using
    # erp5_content
    error_list = []
    portal_types_module = self.portal.portal_types
    for portal_type in portal_types_module.contentValues(portal_type=\
        'Base Type'):
182
      if portal_type.getId().endswith('Module'):
183
        for k, v in portal_type.getPropertyTranslationDomainDict().items():
184
          if k in ('title', 'short_title') and v.getDomainName() != 'erp5_ui':
185
            error_list.append('"%s" should use erp5_ui for %s' % \
186
                (portal_type.getId(), k))
187
    if error_list:
188 189
      message = '\nBad portal_type domain translation list%s\n' \
                    % '\n\t'.join(error_list)
190 191
      self.fail(message)

192 193 194 195 196 197
  def test_emptySelectionNameInListbox(self):
    # check all empty selection name in listboxes
    skins_tool = self.portal.portal_skins
    error_list = []
    for form_path, form in skins_tool.ZopeFind(
              skins_tool, obj_metatypes=['ERP5 Form'], search_sub=1):
198
      for field in self.getFieldList(form, form_path):
Fabien Morin's avatar
Fabien Morin committed
199
        if field.getRecursiveTemplateField().meta_type == 'ListBox':
200
          selection_name = field.get_value("selection_name")
201
          if selection_name in ("",None) and \
202
            form_path not in self.FORM_LISTBOX_EMPTY_SELECTION_PATH_LIST:
203
            error_list.append(form_path)
204
    self.assertEqual(error_list, [])
205

206
  def test_duplicatingSelectionNameInListbox(self):
207
    """
208
    Check for duplicating selection name in listboxes.
209
    Usually we should not have duplicates except in some rare cases
210 211
    described in SkinsTool_getDuplicateSelectionNameDict
    """
212 213 214 215 216
    portal_skins = self.portal.portal_skins
    duplicating_selection_name_dict = portal_skins.SkinsTool_getDuplicateSelectionNameDict()
    self.assertFalse(duplicating_selection_name_dict,
                     "Repeated listbox selection names:\n" +
                     portal_skins.SkinsTool_checkDuplicateSelectionName())
217

Lingnan Wu's avatar
Lingnan Wu committed
218 219 220
  def test_javascript_lint(self):
    skins_tool = self.portal.portal_skins
    path_list = []
221 222
    for script_path, script in skins_tool.ZopeFind(skins_tool,
        obj_metatypes=('File','DTML Method','DTML Document'), search_sub=1):
Lingnan Wu's avatar
Lingnan Wu committed
223
      if script_path.endswith('.js'):
224 225 226
        x = script_path.split('/', 1)
        if not (x[0] in self.JSL_IGNORE_SKIN_LIST or
                x[1] in self.JSL_IGNORE_FILE_LIST):
Lingnan Wu's avatar
Lingnan Wu committed
227
          path_list.append(script_path)
228 229 230
    portal_skins_path = self.portal.getId() + '/portal_skins/'
    args = ('jsl', '-stdin', '-nologo', '-nosummary', '-conf',
            os.path.join(os.path.dirname(__file__), 'jsl.conf'))
231
    error_list = []
232 233
    for path in path_list:
      check_path = portal_skins_path + path
Lingnan Wu's avatar
Lingnan Wu committed
234 235
      body = self.publish(check_path).getBody()
      try:
236 237
        stdout, stderr = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE,
                               close_fds=True).communicate(body)
Lingnan Wu's avatar
Lingnan Wu committed
238 239
      except OSError, e:
        raise OSError, '%r\n%r' % (os.environ, e)
240 241 242 243 244
      if stdout:
        error_list.append((check_path, stdout))
    if error_list:
      message = '\n'.join(["%s\n%s\n" % error for error in error_list])
      self.fail(message)
Xiaowu Zhang's avatar
Xiaowu Zhang committed
245 246

  def test_html_file(self):
247 248 249 250 251
    skins_tool = self.portal.portal_skins
    path_list = []
    for script_path, script in skins_tool.ZopeFind(
              skins_tool, obj_metatypes=['File'], search_sub=1):
      if script_path.endswith('.html'):
252 253 254 255 256
        x = script_path.split('/', 1)
        if not x[1] in self.HTML_IGNORE_FILE_LIST:
          is_required_check_path = False
        for ignore_folder_name in self.HTML_IGNORE_SKIN_FOLDER_LIST:
          if  script_path.startswith(ignore_folder_name):
257 258 259 260
            is_required_check_path = False
            break;
        if is_required_check_path:
          path_list.append(script_path)
Xiaowu Zhang's avatar
Xiaowu Zhang committed
261

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    def validate_html_file(source_path):
      message = ['Using %s validator to parse the file "%s"'
                 ' with warnings%sdisplayed :'
                % (validator.name, source_path,
                   validator.show_warnings and ' ' or ' NOT ')]
      source = self.publish(source_path).getBody()
      result_list_list = validator.getErrorAndWarningList(source)
      severity_list = ['Error']
      if validator.show_warnings:
        severity_list.append('Warning')
      for i, severity in enumerate(severity_list):
        for line, column, msg in result_list_list[i]:
          if line is None and column is None:
            message.append('%s: %s' % (severity, msg))
          else:
            message.append('%s: line %s column %s : %s' %
                           (severity, line, column, msg))
      return len(message) == 1, '\n'.join(message)
Xiaowu Zhang's avatar
Xiaowu Zhang committed
280

281 282
    def html_file(check_path):
      self.assert_(*validate_html_file(source_path=check_path))
Xiaowu Zhang's avatar
Xiaowu Zhang committed
283

284 285 286 287
    portal_skins_path = '%s/portal_skins' % self.portal.getId()
    for path in path_list:
      check_path = '%s/%s' % (portal_skins_path, path)
      html_file(check_path)
Xiaowu Zhang's avatar
Xiaowu Zhang committed
288

Ivan Tyagov's avatar
Ivan Tyagov committed
289
  def test_PythonScriptSyntax(self):
290
    """
Ivan Tyagov's avatar
Ivan Tyagov committed
291 292
    Check that Python Scripts syntax is correct.
    """
293 294 295 296 297
    for tool in (self.portal.portal_skins, self.portal.portal_workflow):
      for script_path, script in tool.ZopeFind(
                tool, obj_metatypes=['Script (Python)'], search_sub=1):
        if script.errors!=():
          # we need to add script id as well in test failure
298
          self.assertEqual('%s : %s' %(script_path, script.errors), ())
Ivan Tyagov's avatar
Ivan Tyagov committed
299 300

  def test_SkinItemId(self):
301
    """
Ivan Tyagov's avatar
Ivan Tyagov committed
302 303 304 305 306 307 308 309
    Check that skin item id is acquiring is correct.
    """
    skins_tool = self.portal.portal_skins
    for skin_folder in skins_tool.objectValues('Folder'):
      for skin_item in skin_folder.objectValues():
        if skin_item.meta_type not in ('File', 'Image', 'DTML Document', 'DTML Method',):
          skin_item_id = skin_item.id
          self.assertEqual(skin_item_id, skin_folder[skin_item_id].id)
310

Nicolas Delaby's avatar
Nicolas Delaby committed
311
  def test_callableListMethodInListbox(self):
312 313 314 315 316
    # check all list_method in listboxes
    skins_tool = self.portal.portal_skins
    error_list = []
    for form_path, form in skins_tool.ZopeFind(
              skins_tool, obj_metatypes=['ERP5 Form'], search_sub=1):
317
      for field in self.getFieldList(form, form_path):
Fabien Morin's avatar
Fabien Morin committed
318
        if field.getRecursiveTemplateField().meta_type == 'ListBox':
319 320 321
          list_method = field.get_value("list_method")
          if list_method:
            if isinstance(list_method, str):
322
              method = getattr(self.portal, list_method, None)
323 324 325
            else:
              method = list_method
            if not callable(method):
326
              error_list.append((form_path, list_method))
327
    self.assertEqual(error_list, [])
328

329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
  def test_callableCountMethodInListbox(self):
    # check all count_method in listboxes
    skins_tool = self.portal.portal_skins
    error_list = []
    for form_path, form in skins_tool.ZopeFind(
              skins_tool, obj_metatypes=['ERP5 Form'], search_sub=1):
      for field in self.getFieldList(form, form_path):
        if field.getRecursiveTemplateField().meta_type == 'ListBox':
          count_method = field.get_value("count_method")
          if count_method:
            if isinstance(count_method, str):
              method = getattr(self.portal, count_method, None)
            else:
              method = count_method
            if not callable(method):
              error_list.append((form_path, count_method))
    self.assertEqual(error_list, [])

347 348 349 350 351 352 353
  def test_listActionInListbox(self):
    # check all list_action in listboxes
    skins_tool = self.portal.portal_skins
    error_list = []
    for form_path, form in skins_tool.ZopeFind(
              skins_tool, obj_metatypes=['ERP5 Form'], search_sub=1):
      for field in self.getFieldList(form, form_path):
Fabien Morin's avatar
Fabien Morin committed
354
        if field.getRecursiveTemplateField().meta_type == 'ListBox':
355 356 357 358
          list_action = field.get_value("list_action")
          if list_action and list_action != 'list': # We assume that 'list'
                                                    # list_action exists
            if isinstance(list_action, str):
359 360
              # list_action can be a fully qualified URL, we care for last part of it
              list_action = list_action.split('/')[-1].split('?')[0]
361 362 363 364
              try:
                method = self.portal.restrictedTraverse(list_action)
              except KeyError:
                method = None
365 366 367 368 369 370
              if method is None:
                # list_action can actually exists but not in current skin, check if it can be found in portal_skins
                found_list_action_list = skins_tool.ZopeFind(skins_tool, obj_ids=[list_action], search_sub=1)
                if found_list_action_list:
                  method = found_list_action_list[0][1]
                  ZopeTestCase._print("List action %s for %s is not part of current skin but do exists in another skin folder.\n" % (list_action, form_path))
371 372 373
            else:
              method = list_action
            if not callable(method):
374 375 376
              error_list.append('Form %s/%s : list_action "%s" is not callable.'\
                  % (form_path, field.id, list_action))
    self.assert_(not len(error_list), '\n'.join(error_list))
377

378 379
  def test_moduleListMethod(self):
    """Make sure that module's list method works."""
380
    error_list = []
381 382
    for document in self.portal.contentValues():
      if document.portal_type.endswith(' Module'):
383
        if document.getTranslatedTitle() not in document.list(reset=1):
384 385
          error_list.append(document.id)
    self.assertEqual([], error_list)
386

387 388 389 390
  def test_preferenceViewDuplication(self):
    """Make sure that preference view is not duplicated."""
    preference_view_id_dict = {}
    def addPreferenceView(folder_id, view_id):
Jérome Perrin's avatar
Jérome Perrin committed
391
      preference_view_id_dict.setdefault(view_id, []).append('%s.%s' % (folder_id, view_id))
392
    error_list = []
Jérome Perrin's avatar
Jérome Perrin committed
393 394
    for skin_folder in self.portal.portal_skins.objectValues():
      if skin_folder.isPrincipiaFolderish:
395
        for id_ in skin_folder.objectIds():
396
          if id_.startswith('Preference_view'):
Jérome Perrin's avatar
Jérome Perrin committed
397
            addPreferenceView(skin_folder.id, id_)
398
    for view_id, location_list in preference_view_id_dict.items():
Jérome Perrin's avatar
Jérome Perrin committed
399
      if len(location_list) > 1:
400 401 402
        error_list.extend(location_list)
    self.assertEqual(error_list, [])

403 404 405 406 407 408 409 410 411 412 413 414 415 416
class TestXHTML(TestXHTMLMixin):

  run_all_test = 1

  def getTitle(self):
    return "XHTML Test"

  @staticmethod
  def getBusinessTemplateList():
    """  """
    return ( # dependency order
      'erp5_core_proxy_field_legacy',
      'erp5_base',
      'erp5_simulation',
417
      'erp5_pdm',
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
      'erp5_trade',

      'erp5_pdf_editor',
      'erp5_accounting',
      'erp5_invoicing',

      'erp5_apparel',

##    'erp5_banking_core',
##    'erp5_banking_cash',
##    'erp5_banking_check',
##    'erp5_banking_inventory',

      'erp5_budget',
      'erp5_public_accounting_budget',

      'erp5_consulting',

      'erp5_ingestion',
      'erp5_ingestion_mysql_innodb_catalog',
      'erp5_crm',
439
      'erp5_interface_post',
440 441 442 443 444 445 446 447

      'erp5_jquery',
      'erp5_jquery_ui',
      'erp5_web',
      'erp5_dms',
      'erp5_email_reader',
      'erp5_commerce',
      'erp5_credential',
448
      'erp5_test_result',
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479

      'erp5_forge',

      'erp5_immobilisation',

      'erp5_item',

      'erp5_mrp',

      'erp5_payroll',

      'erp5_project',

      'erp5_calendar',

      'erp5_advanced_invoicing',

      'erp5_odt_style',

      'erp5_administration',

      'erp5_knowledge_pad',
      'erp5_knowledge_pad_ui_test',
      'erp5_km',
      'erp5_ui_test',
      'erp5_dms_ui_test',

      'erp5_trade_proxy_field_legacy', # it is necessary until all bt are well
                                       # reviewed. Many bt like erp5_project are
                                       # using obsolete field library of trade
      'erp5_xhtml_style',
480
      'erp5_dhtml_style',
481 482 483 484 485 486 487 488 489 490 491 492 493 494
      'erp5_jquery_plugin_svg_editor',
      'erp5_jquery_plugin_spinbtn',
      'erp5_jquery_plugin_jquerybbq',
      'erp5_jquery_plugin_svgicon',
      'erp5_jquery_plugin_jgraduate',
      'erp5_jquery_plugin_hotkey',
      'erp5_jquery_plugin_elastic',
      'erp5_jquery_plugin_colorpicker',
      'erp5_jquery_plugin_jqchart',
      'erp5_jquery_plugin_sheet',
      'erp5_jquery_plugin_mbmenu',
      'erp5_jquery_plugin_wdcalendar',
      'erp5_svg_editor',
      'erp5_jquery_sheet_editor',
495
      'erp5_graph_editor',
496 497 498 499 500 501 502 503 504 505 506 507
      'erp5_ui_test',
      'erp5_l10n_fr', # install at least one localization business template
                      # because some language switching widgets are only
                      # present if there is more than one available language.
    )

  def afterSetUp(self):
    self.portal = self.getPortal()

    uf = self.getPortal().acl_users
    uf._doAddUser('seb', '', ['Manager'], [])

508
    self.loginByUserName('seb')
509 510 511 512 513 514 515 516
    self.enableDefaultSitePreference()

  def enableDefaultSitePreference(self):
    portal_preferences = getToolByName(self.portal, 'portal_preferences')
    default_site_preference = portal_preferences.default_site_preference
    if self.portal.portal_workflow.isTransitionPossible(default_site_preference, 'enable'):
      default_site_preference.enable()

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570

class NuValidator(object):

  def __init__(self, show_warnings):
    self.show_warnings = show_warnings
    self.name = 'nu'

  def _parse_validation_results(self, validator_url, response):
    """
    parses the validation results, returns a list of tuples:
    line_number, col_number, error description
    """
    if response.status_code != 200:
      return [
        [(None, None,
          'Contacting the external validator %s failed with status: %i' %
            (validator_url, response.status_code))],
        []
      ]

    content_type = response.headers.get('Content-Type', None)
    if content_type != 'application/json;charset=utf-8':
      return [[(None, None, 'Unsupported validator response content type %s' %
                            content_type)], []]

    result = response.json()

    error_list = []
    warning_list = []
    for message in result['messages']:
      if message['type'] == 'info':
        severity_list = warning_list
      else:
        severity_list = error_list
      txt = message['message'].encode('UTF-8')
      if 'extract' in message:
        txt += ': %s' % message['extract'].encode('UTF-8')
      severity_list.append([message['lastLine'], message['lastColumn'], txt])
    return [error_list, warning_list]

  def getErrorAndWarningList(self, page_source):
    '''
      retrun two list : a list of errors and an other for warnings
    '''
    validator_url = 'https://validator.erp5.net/'
    response = requests.post(validator_url,
                             data=page_source.encode('UTF-8'),
                             params={'out': 'json'},
                             headers={
                               'Content-Type': 'text/html; charset=UTF-8'
                             })
    return self._parse_validation_results(validator_url, response)


571 572 573 574 575 576 577 578 579 580 581 582
class W3Validator(object):

  def __init__(self, validator_path, show_warnings):
    self.validator_path = validator_path
    self.show_warnings = show_warnings
    self.name = 'w3c'

  def _parse_validation_results(self, result):
    """
    parses the validation results, returns a list of tuples:
    line_number, col_number, error description
    """
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
    # Output is a set of headers then the XML content.
    header_txt, body_txt = result.split('\n\n', 1)
    # First, search the X-W3C headers
    validator_status = 'Unknown'
    error_count = -1
    warning_count = -1

    for header_line in header_txt.split('\n'):
      if header_line.startswith('X-W3C-Validator-Status: '):
        validator_status = header_line[len('X-W3C-Validator-Status: '):]
      elif header_line.startswith('X-W3C-Validator-Errors: '):
        error_count = int(header_line[len('X-W3C-Validator-Errors: '):])
      elif header_line.startswith('X-W3C-Validator-Warnings: '):
        warning_count = int(header_line[len('X-W3C-Validator-Warnings: '):])

    if validator_status == 'Valid':
      return [[], []]
    if validator_status != 'Invalid':
      return [[(None, None, 'Wrong validator status: %s' % validator_status)], []]

    # Parsing is invalid
604
    result_list_list = []
605 606 607 608 609 610
    try:
      xml_doc = minidom.parseString(result)
    except:
      import sys
      print >> sys.stderr, "Could not parse result:\n%s" % result
      raise
611 612 613 614
    for severity in 'm:error', 'm:warning':
      result_list = []
      for error in xml_doc.getElementsByTagName(severity):
        result = []
615 616 617 618 619 620

        # Ignore warning about using direct input mode (W28)
        messageid_list = error.getElementsByTagName('m:messageid')
        if messageid_list and messageid_list[0].firstChild.nodeValue == 'W28':
          continue

621 622 623 624 625 626 627 628
        for name in 'm:line', 'm:col', 'm:message':
          element_list = error.getElementsByTagName(name)
          if element_list:
            result.append(element_list[0].firstChild.nodeValue)
          else:
            result.append(None)
        result_list.append(tuple(result))
      result_list_list.append(result_list)
629 630
    if (len(result_list_list[0]) != error_count) or (len(result_list_list[1]) != warning_count):
      result_list_list[0].append((None, None, 'Could not parse all errors/warnings'))
631
    return result_list_list
632 633 634 635 636

  def getErrorAndWarningList(self, page_source):
    '''
      retrun two list : a list of errors and an other for warnings
    '''
637 638
    source = 'fragment=%s&output=soap12' % urllib.quote_plus(
      page_source.encode('utf-8'))
639 640 641 642 643 644 645
    stdout, stderr = Popen(self.validator_path,
            stdin=PIPE, stdout=PIPE, stderr=PIPE,
            close_fds=True,
            env={"CONTENT_LENGTH": str(len(source)),
                 "REQUEST_METHOD": "POST"}).communicate(source)
    # Output is a set of headers then the XML content.
    return self._parse_validation_results(
646
      stdout.split('\n\n', 1)[1])
647 648 649 650


class TidyValidator(object):

Jérome Perrin's avatar
Jérome Perrin committed
651
  def __init__(self, validator_path, show_warnings):
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    self.validator_path = validator_path
    self.show_warnings = show_warnings
    self.name = 'tidy'

  def _parse_validation_results(self, result):
    """
    parses the validation results, returns a list of tuples:
    line_number, col_number, error description
    """
    error_list=[]
    warning_list=[]

    for i in result:
      data = i.split(' - ')
      if len(data) >= 2:
        data[1] = data[1].replace('\n','')
        if data[1].startswith('Error: '):
          location_list = data[0].split(' ')
          line = location_list[1]
          column = location_list[3]
          message = data[1].split(': ')[1]
          error_list.append((line, column, message))
        elif data[1].startswith('Warning: '):
          location_list = data[0].split(' ')
          line = location_list[1]
          column = location_list[3]
          message = data[1].split(': ')[1]
          warning_list.append((line, column, message))
    return (error_list, warning_list)

  def getErrorAndWarningList(self, page_source):
    '''
      retrun two list : a list of errors and an other for warnings
    '''
686 687 688
    stdout, stderr = Popen('%s -e -q -utf8' % self.validator_path,
            stdin=PIPE, stdout=PIPE, stderr=PIPE,
            close_fds=True).communicate(page_source)
689 690 691 692 693 694 695
    return self._parse_validation_results(stderr)


def validate_xhtml(validator, source, view_name, bt_name):
  '''
    validate_xhtml return True if there is no error on the page, False else.
    Now it's possible to show warnings, so, if the option is set to True on the
696
    validator object, and there is some warning on the page, the function
697 698 699
    return False, even if there is no error.
  '''
  # display some information when test faild to facilitate debugging
700
  message = ['Using %s validator to parse the view "%s" (from %s bt)'
Julien Muchembled's avatar
typo  
Julien Muchembled committed
701
             ' with warnings%sdisplayed :'
702
             % (validator.name, view_name, bt_name,
Julien Muchembled's avatar
typo  
Julien Muchembled committed
703
                validator.show_warnings and ' ' or ' NOT ')]
704

705
  result_list_list = validator.getErrorAndWarningList(source)
706

707 708 709
  severity_list = ['Error']
  if validator.show_warnings:
    severity_list.append('Warning')
710

711 712 713 714 715 716 717
  for i, severity in enumerate(severity_list):
    for line, column, msg in result_list_list[i]:
      if line is None and column is None:
        message.append('%s: %s' % (severity, msg))
      else:
        message.append('%s: line %s column %s : %s' %
                       (severity, line, column, msg))
718

719
  return len(message) == 1, '\n'.join(message)
720 721 722 723 724


def makeTestMethod(validator, module_id, portal_type, view_name, bt_name):

  def createSubContent(content, portal_type_list):
Jérome Perrin's avatar
Jérome Perrin committed
725
    if not portal_type_list:
726
      return content
Jérome Perrin's avatar
Jérome Perrin committed
727 728 729 730 731
    if portal_type_list == [content.getPortalType()]:
      return content
    return createSubContent(
               content.newContent(portal_type=portal_type_list[0]),
               portal_type_list[1:])
732

733 734
  def testMethod(self):
    module = getattr(self.portal, module_id)
735 736 737 738
    portal_type_list = portal_type.split('/')

    object = createSubContent(module, portal_type_list)
    view = getattr(object, view_name)
739 740 741
    self.assert_(*validate_xhtml( validator=validator,
                                  source=view(),
                                  view_name=view_name,
742
                                  bt_name=bt_name))
743 744
  return testMethod

745 746 747
def testPortalTypeViewRecursivly(test_class, validator, module_id,
    business_template_info, business_template_info_list, portal_type_list,
    portal_type_path_dict, base_path, tested_portal_type_list):
748
  '''
749
  This function go on all portal_type recursivly if the portal_type could
750 751 752 753 754 755 756 757
  contain other portal_types and make a test for all view that have action
  '''
  # iteration over all allowed portal_types inside the module/portal_type
  for portal_type in portal_type_list:
    portal_path = portal_type_path_dict[portal_type]
    if portal_type not in tested_portal_type_list:
      # this portal type haven't been tested yet

758
      backuped_module_id = module_id
759 760 761 762 763 764 765 766 767 768 769 770 771 772
      backuped_business_template_info = business_template_info

      if not business_template_info.actions.has_key(portal_type):
        # search in other bt :
        business_template_info = None
        for bt_info in business_template_info_list:
          if bt_info.actions.has_key(portal_type):
            business_template_info = bt_info
            break
        if not business_template_info:
          LOG("Can't find the action :", 0, portal_type)
          break
        # create the object in portal_trash module
        module_id = 'portal_trash'
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
      for business_template_info in business_template_info_list:
        if portal_type not in business_template_info.actions:
          continue
        for action_information in business_template_info.actions[portal_type]:
          if (action_information['category'] in ('object_view', 'object_list') and
              action_information['visible']==1 and
              action_information['action'].startswith('string:${object_url}/') and
              len(action_information['action'].split('/'))==2):
            view_name = action_information['action'].split('/')[-1].split('?')[0]
            method = makeTestMethod(validator,
                                    module_id,
                                    portal_path,
                                    view_name,
                                    business_template_info.title)
            method_name = ('test_%s_%s_%s' %
                          (business_template_info.title,
                            str(portal_type).replace(' ','_'), # can be unicode
                            view_name))
            method.__name__ = method_name
            setattr(test_class, method_name, method)
            module_id = backuped_module_id
794 795 796 797 798

      # add the portal_type to the tested portal_types. This avoid to test many
      # times a Portal Type wich is many bt.
      tested_portal_type_list.append(portal_type)

799 800 801
      new_portal_type_list = []
      for tmp_business_template_info in business_template_info_list:
        new_portal_type_list.extend(tmp_business_template_info.allowed_content_types.get(portal_type, ()))
802 803 804 805 806 807 808 809 810 811 812 813 814 815 816
      new_portal_type_path_dict = {}

      if base_path != '':
        next_base_path = '%s/%s' % (base_path, portal_type)
      # Module portal_type not to have been added to the path because
      # this portal type object already existing
      elif 'Module' not in portal_type:
        next_base_path = portal_type
      else:
        next_base_path = ''

      for pt in new_portal_type_list:
        if next_base_path != '' and 'Module' not in pt:
          new_portal_type_path_dict[pt] = '%s/%s' % (next_base_path, pt)
        else:
817
          new_portal_type_path_dict[pt] = pt
818 819
      testPortalTypeViewRecursivly(test_class=test_class,
                       validator=validator,
820 821
                       module_id=module_id,
                       business_template_info=backuped_business_template_info,
822
                       business_template_info_list=business_template_info_list,
823
                       portal_type_list=new_portal_type_list,
824 825 826 827
                       portal_type_path_dict=new_portal_type_path_dict,
                       base_path=next_base_path,
                       tested_portal_type_list=tested_portal_type_list)

828
def addTestMethodDynamically(test_class, validator, target_business_templates):
829 830
  from Products.ERP5.tests.utils import BusinessTemplateInfoTar
  from Products.ERP5.tests.utils import BusinessTemplateInfoDir
831 832
  business_template_info_list = []

833 834 835
  for url, _ in ERP5TypeTestCase._getBTPathAndIdList(target_business_templates):
    if os.path.isdir(url):
      business_template_info = BusinessTemplateInfoDir(url)
836
    else:
837 838
      business_template_info = BusinessTemplateInfoTar(url)
    business_template_info_list.append(business_template_info)
839

840 841
  tested_portal_type_list = []
  for business_template_info in business_template_info_list:
842
    for module_id, module_portal_type in business_template_info.modules.items():
Jérome Perrin's avatar
Jérome Perrin committed
843
      portal_type_list = [module_portal_type, ] + \
844
            business_template_info.allowed_content_types.get(module_portal_type, [])
845
      portal_type_path_dict = dict(zip(portal_type_list, portal_type_list))
846 847
      testPortalTypeViewRecursivly(test_class=test_class,
                       validator=validator,
848 849
                       module_id=module_id,
                       business_template_info=business_template_info,
850
                       business_template_info_list=business_template_info_list,
851
                       portal_type_list=portal_type_list,
852 853 854 855 856
                       portal_type_path_dict=portal_type_path_dict,
                       base_path = '',
                       tested_portal_type_list=tested_portal_type_list)


857 858 859
# Three validators are available : nu, tidy and the w3c validator
# It's hightly recommanded to use the nu validator which validates html5
validator_to_use = 'nu'
860 861 862 863 864 865 866
show_warnings = True

validator = None

# tidy or w3c may not be installed in livecd. Then we will skip xhtml validation tests.
# create the validator object
if validator_to_use == 'w3c':
867 868
  validator_path_list = os.environ.get('CGI_PATH',
    '/usr/lib/cgi-bin:/usr/lib/cgi-bin/w3c-markup-validator').split(os.pathsep)
869
  for path in validator_path_list:
870
    validator_path = os.path.join(path, 'check')
871 872 873
    if os.path.exists(validator_path):
      validator = W3Validator(validator_path, show_warnings)
      break
874
  else:
875
    print 'No w3c validator found at', validator_path_list
876

Jérome Perrin's avatar
Jérome Perrin committed
877
elif validator_to_use == 'tidy':
878 879 880 881 882 883 884 885
  error = False
  warning = False
  validator_path = '/usr/bin/tidy'
  if not os.path.exists(validator_path):
    print 'tidy is not installed at %s' % validator_path
  else:
    validator = TidyValidator(validator_path, show_warnings)

886 887 888
elif validator_to_use == 'nu':
  validator = NuValidator(show_warnings)

889
def test_suite():
890
  # add the tests
891 892 893
  if validator is not None:
    # add erp5_core to the list here to not return it
    # on getBusinessTemplateList call
894
    addTestMethodDynamically(TestXHTML, validator,
895
      ('erp5_core',) + TestXHTML.getBusinessTemplateList())
896 897 898
  suite = unittest.TestSuite()
  suite.addTest(unittest.makeSuite(TestXHTML))
  return suite