1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2004-2021 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at https://trac.edgewall.org/wiki/TracLicense.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at https://trac.edgewall.org/log/.
13
14import difflib
15import io
16import os
17import re
18import unittest
19
20# Python 2.7 `assertMultiLineEqual` calls `safe_repr(..., short=True)`
21# which breaks our custom failure display in WikiTestCase.
22
23try:
24    from unittest.util import safe_repr
25except ImportError:
26    pass
27else:
28    unittest.case.safe_repr = lambda obj, short=False: safe_repr(obj, False)
29
30from trac.test import EnvironmentStub, MockRequest
31from trac.util.datefmt import datetime_now, to_utimestamp, utc
32from trac.util.text import strip_line_ws, to_unicode
33from trac.web.chrome import web_context
34from trac.wiki.formatter import (HtmlFormatter, InlineHtmlFormatter,
35                                 OutlineFormatter)
36
37
38class WikiTestCase(unittest.TestCase):
39
40    generate_opts = {}
41
42    def __init__(self, title, input, expected, file, line,
43                 setup=None, teardown=None, context=None, default_data=False,
44                 enable_components=None, disable_components=None,
45                 env_path='', destroying=False):
46        unittest.TestCase.__init__(self, 'test')
47        self.title = title
48        self.input = input
49        self.expected = expected
50        if file.endswith('.pyc'):
51            file = file.replace('.pyc', '.py')
52        self.file = file
53        self.line = line
54        self._setup = setup
55        self._teardown = teardown
56        self._context = context
57        self.context = None
58        self._env_kwargs = {'default_data': default_data,
59                            'enable': enable_components,
60                            'disable': disable_components,
61                            'path': env_path, 'destroying': destroying}
62
63    def _create_env(self):
64        env = EnvironmentStub(**self._env_kwargs)
65        # -- intertrac support
66        env.config.set('intertrac', 'genshi.title', "Genshi's Trac")
67        env.config.set('intertrac', 'genshi.url', "https://genshi.edgewall.org")
68        env.config.set('intertrac', 't', 'trac')
69        env.config.set('intertrac', 'th.title', "Trac Hacks")
70        env.config.set('intertrac', 'th.url', "http://trac-hacks.org")
71        # -- safe schemes
72        env.config.set('wiki', 'safe_schemes',
73                       'data,file,ftp,http,https,svn,svn+ssh,'
74                       'rfc-2396.compatible,rfc-2396+under_score')
75        return env
76
77    def setUp(self):
78        self.env = self._create_env()
79        self.req = MockRequest(self.env, script_name='/')
80        context = self._context
81        if context:
82            if isinstance(self._context, tuple):
83                context = web_context(self.req, *self._context)
84        else:
85            context = web_context(self.req, 'wiki', 'WikiStart')
86        self.context = context
87        # Remove the following lines in order to discover
88        # all the places were we should use the req.href
89        # instead of env.href
90        self.env.href = self.req.href
91        self.env.abs_href = self.req.abs_href
92        self.env.db_transaction(
93            "INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s)",
94            ('WikiStart', 1, to_utimestamp(datetime_now(utc)), 'joe',
95             '--', 'Entry page', 0))
96        if self._setup:
97            self._setup(self)
98
99    def tearDown(self):
100        self.env.reset_db()
101        if self._teardown:
102            self._teardown(self)
103
104    def test(self):
105        """Testing WikiFormatter"""
106        formatter = self.formatter()
107        v = str(formatter.generate(**self.generate_opts))
108        v = v.replace('\r', '').replace('\u200b', '')  # FIXME: keep ZWSP
109        v = strip_line_ws(v, leading=False)
110        try:
111            self.assertEqual(self.expected, v)
112        except AssertionError as e:
113            msg = to_unicode(e)
114            match = re.match(r"u?'(.*)' != u?'(.*)'", msg)
115            if match:
116                g1 = ["%s\n" % x for x in match.group(1).split(r'\n')]
117                g2 = ["%s\n" % x for x in match.group(2).split(r'\n')]
118                expected = ''.join(g1)
119                actual = ''.join(g2)
120                wiki = repr(self.input).replace(r'\n', '\n')
121                diff = ''.join(list(difflib.unified_diff(g1, g2, 'expected',
122                                                         'actual')))
123                # Tip: sometimes, 'expected' and 'actual' differ only by
124                #      whitespace, so it can be useful to visualize them, e.g.
125                # expected = expected.replace(' ', '.')
126                # actual = actual.replace(' ', '.')
127                def info(*args):
128                    return '\n========== %s: ==========\n%s' % args
129                msg = info('expected', expected)
130                msg += info('actual', actual)
131                msg += info('wiki', ''.join(wiki))
132                msg += info('diff', diff)
133            raise AssertionError( # See below for details
134                '%s\n\n%s:%s: "%s" (%s flavor)' \
135                % (msg, self.file, self.line, self.title, formatter.flavor))
136
137    def formatter(self):
138        return HtmlFormatter(self.env, self.context, self.input)
139
140    def shortDescription(self):
141        return 'Test ' + self.title
142
143
144class OneLinerTestCase(WikiTestCase):
145    def formatter(self):
146        return InlineHtmlFormatter(self.env, self.context, self.input)
147
148
149class EscapeNewLinesTestCase(WikiTestCase):
150    generate_opts = {'escape_newlines': True}
151
152    def formatter(self):
153        return HtmlFormatter(self.env, self.context, self.input)
154
155
156class OutlineTestCase(WikiTestCase):
157    def formatter(self):
158        class Outliner(object):
159            flavor = 'outliner'
160            def __init__(self, env, context, input):
161                self.outliner = OutlineFormatter(env, context)
162                self.input = input
163            def generate(self):
164                out = io.StringIO()
165                self.outliner.format(self.input, out)
166                return out.getvalue()
167        return Outliner(self.env, self.context, self.input)
168
169
170def wikisyntax_test_suite(data=None, setup=None, file=None, teardown=None,
171                          context=None, default_data=False,
172                          enable_components=None, disable_components=None,
173                          env_path=None, destroying=False):
174    suite = unittest.TestSuite()
175
176    def add_test_cases(data, filename):
177        tests = re.compile('^(%s.*)$' % ('=' * 30), re.MULTILINE).split(data)
178        next_line = 1
179        line = 0
180        for title, test in zip(tests[1::2], tests[2::2]):
181            title = title.lstrip('=').strip()
182            if line != next_line:
183                line = next_line
184            if not test or test == '\n':
185                continue
186            next_line += len(test.split('\n')) - 1
187            if 'SKIP' in title or 'WONTFIX' in title:
188                continue
189            blocks = test.split('-' * 30 + '\n')
190            if len(blocks) < 5:
191                blocks.extend([None] * (5 - len(blocks)))
192            input, page, oneliner, page_escape_nl, outline = blocks[:5]
193            for cls, expected in [
194                    (WikiTestCase, page),
195                    (OneLinerTestCase, oneliner and oneliner[:-1]),
196                    (EscapeNewLinesTestCase, page_escape_nl),
197                    (OutlineTestCase, outline)]:
198                if expected:
199                    tc = cls(title, input, expected, filename, line,
200                             setup, teardown, context, default_data,
201                             enable_components, disable_components,
202                             env_path, destroying)
203                    suite.addTest(tc)
204
205    if data:
206        add_test_cases(data, file)
207    else:
208        if os.path.exists(file):
209            with open(file, 'r', encoding='utf-8') as fobj:
210                data = fobj.read()
211            add_test_cases(data, file)
212        else:
213            print('no ' + file)
214
215    return suite
216