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