cubicweb/devtools/test/unittest_i18n.py
author Denis Laxalde <denis.laxalde@logilab.fr>
Thu, 09 Mar 2017 16:36:33 +0100
changeset 12053 c3c9f2e1424c
parent 11822 adf0212d1dcc
child 12546 465ee7fb16e3
permissions -rw-r--r--
[pyramid] Add a "pyramid" instance configuration type In a new module 'cubicweb.pyramid.config' we define a "pyramid" instance configuration type. The noticeable feature of this configuration is that it manages a 'development.ini' file that gets installed in application home (along with `.conf` file). This file is templated and includes generated values for secrets of session and authtk tokens. This means that we can just call: pserve etc/cubicweb.d/<appname>/development.ini or gunicorn --paste etc/cubicweb.d/<appname>/development.ini -b :8080 just after instance creation to get a pyramid instance running without having to hack around a 'pyramid.ini' file. This patch drops 'development.ini' from skeleton and moves it in cubicweb/pyramid so that it gets installed at instance creation which is more appropriate than in cube creation. The new configuration class sets "cubicweb.bwcompat" setting to false so it is not intended to replace the "all-in-one" configuration type (which would require a bit more work). This configuration is close to the the 'repository' configuration type with just a couple of options from WebConfiguration that are needed for Pyramid (anonymous user/password plus some miscellaneous options that I'm not so sure are really needed). Note, in particular, that we do not pull CORS settings to be injected as a WSGI middleware like in wsgi_application_from_cwconfig() since I believe this should be left as an end-user responsibility and since this can be defined in a standard way in paste configuration. This configuration inherits from ServerConfiguration but registers the same appobjects as WebConfiguration. In cubicweb.web.request._CubicWebRequestBase, we guard against access to "uiprops" and "datadir_url" of the config because this new "pyramid" config does not have these (this does not make sense without bwcompat mode). At some point, we should either avoid using `cw_request`'s pyramid request attribute or make cubicweb's web request really independant of existing implementation and drop these assumptions.

# -*- coding: iso-8859-1 -*-
# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""unit tests for i18n messages generator"""

from contextlib import contextmanager
from io import StringIO, BytesIO
import os
import os.path as osp
import sys
from subprocess import PIPE, Popen, STDOUT
from unittest import TestCase, main

from six import PY2
from mock import patch

from cubicweb.devtools import devctl
from cubicweb.devtools.testlib import BaseTestCase

DATADIR = osp.join(osp.abspath(osp.dirname(__file__)), 'data')


def load_po(fname):
    """load a po file and  return a set of encountered (msgid, msgctx)"""
    msgs = set()
    msgid = msgctxt = None
    with open(fname) as fobj:
        for line in fobj:
            if line.strip() in ('', '#'):
                continue
            if line.startswith('msgstr'):
                assert not (msgid, msgctxt) in msgs
                msgs.add((msgid, msgctxt))
                msgid = msgctxt = None
            elif line.startswith('msgid'):
                msgid = line.split(' ', 1)[1][1:-1]
            elif line.startswith('msgctx'):
                msgctxt = line.split(' ', 1)[1][1: -1]
            elif msgid is not None:
                msgid += line[1:-1]
            elif msgctxt is not None:
                msgctxt += line[1:-1]
    return msgs


TESTCUBE_DIR = osp.join(DATADIR, 'cubes', 'i18ntestcube')


class cubePotGeneratorTC(TestCase):
    """test case for i18n pot file generator"""

    def test_i18ncube(self):
        env = os.environ.copy()
        if 'PYTHONPATH' in env:
            env['PYTHONPATH'] += os.pathsep
        else:
            env['PYTHONPATH'] = ''
        env['PYTHONPATH'] += osp.join(DATADIR, 'libpython')
        cubedir = osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')
        self._check(cubedir, env)

    def test_i18ncube_legacy_layout(self):
        env = os.environ.copy()
        env['CW_CUBES_PATH'] = osp.join(DATADIR, 'cubes')
        if 'PYTHONPATH' in env:
            env['PYTHONPATH'] += os.pathsep
        else:
            env['PYTHONPATH'] = ''
        env['PYTHONPATH'] += DATADIR
        cubedir = osp.join(DATADIR, 'cubes', 'i18ntestcube')
        self._check(cubedir, env)

    def _check(self, cubedir, env):
        cmd = [sys.executable, '-m', 'cubicweb', 'i18ncube', 'i18ntestcube']
        proc = Popen(cmd, env=env, stdout=PIPE, stderr=STDOUT)
        stdout, _ = proc.communicate()
        msg = stdout.decode(sys.getdefaultencoding(), errors='replace')
        self.assertEqual(proc.returncode, 0, msg=msg)
        msgs = load_po(osp.join(cubedir, 'i18n', 'en.po.ref'))
        newmsgs = load_po(osp.join(cubedir, 'i18n', 'en.po'))
        self.assertEqual(msgs, newmsgs)


class CustomMessageExtractor(devctl.I18nCubeMessageExtractor):
    blacklist = devctl.I18nCubeMessageExtractor.blacklist | set(['excludeme'])


@contextmanager
def capture_stdout():
    stream = BytesIO() if PY2 else StringIO()
    sys.stdout = stream
    yield stream
    stream.seek(0)
    sys.stdout = sys.__stdout__


class I18nCollectorTest(BaseTestCase):

    def test_i18ncube_py_collection(self):
        extractor = CustomMessageExtractor(DATADIR, TESTCUBE_DIR)
        collected = extractor.collect_py()
        expected = [osp.join(TESTCUBE_DIR, path)
                    for path in ('__init__.py', '__pkginfo__.py',
                                 'views.py', 'schema.py')]
        self.assertCountEqual(expected, collected)

    def test_i18ncube_js_collection(self):
        extractor = CustomMessageExtractor(DATADIR, TESTCUBE_DIR)
        collected = extractor.collect_js()
        self.assertCountEqual([], collected, [])
        extractor.blacklist = ()  # don't ignore anything
        collected = extractor.collect_js()
        expected = [osp.join(TESTCUBE_DIR, 'node_modules/cubes.somefile.js')]
        self.assertCountEqual(expected, collected)

    class FakeMessageExtractor(devctl.I18nCubeMessageExtractor):
        """Fake message extractor that generates no pot file."""

        def generate_pot_file(self):
            return None

    @patch('pkg_resources.load_entry_point', return_value=FakeMessageExtractor)
    def test_cube_custom_extractor(self, mock_load_entry_point):
        distname = 'cubicweb_i18ntestcube'  # same for new and legacy layout
        for cubedir in [
            osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube'),
            # Legacy cubes.
            osp.join(DATADIR, 'cubes', 'i18ntestcube'),
        ]:
            with self.subTest(cubedir=cubedir):
                with capture_stdout() as stream:
                    devctl.update_cube_catalogs(cubedir)
                self.assertIn(u'no message catalog for cube i18ntestcube',
                              stream.read())
                mock_load_entry_point.assert_called_once_with(
                    distname, 'cubicweb.i18ncube', 'i18ntestcube')
                mock_load_entry_point.reset_mock()


if __name__ == '__main__':
    main()