3
Python-Markdown Regression Tests
4
================================
6
Tests of the various APIs with the python markdown lib.
10
from __future__ import unicode_literals
17
from markdown.__main__ import parse_options
18
from logging import DEBUG, WARNING, CRITICAL
22
PY3 = sys.version_info[0] == 3
25
class TestMarkdownBasics(unittest.TestCase):
26
""" Tests basics of the Markdown class. """
29
""" Create instance of Markdown. """
30
self.md = markdown.Markdown()
32
def testBlankInput(self):
33
""" Test blank input. """
34
self.assertEqual(self.md.convert(''), '')
36
def testWhitespaceOnly(self):
37
""" Test input of only whitespace. """
38
self.assertEqual(self.md.convert(' '), '')
40
def testSimpleInput(self):
41
""" Test simple input. """
42
self.assertEqual(self.md.convert('foo'), '<p>foo</p>')
44
def testInstanceExtension(self):
45
""" Test Extension loading with a class instance. """
46
from markdown.extensions.footnotes import FootnoteExtension
47
markdown.Markdown(extensions=[FootnoteExtension()])
49
def testNamedExtension(self):
50
""" Test Extension loading with Name (`path.to.module`). """
51
markdown.Markdown(extensions=['markdown.extensions.footnotes'])
53
def TestNamedExtensionWithClass(self):
54
""" Test Extension loading with class name (`path.to.module:Class`). """
55
markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension'])
58
class TestBlockParser(unittest.TestCase):
59
""" Tests of the BlockParser class. """
62
""" Create instance of BlockParser. """
63
self.parser = markdown.Markdown().parser
65
def testParseChunk(self):
66
""" Test BlockParser.parseChunk. """
67
root = markdown.util.etree.Element("div")
69
self.parser.parseChunk(root, text)
71
markdown.serializers.to_xhtml_string(root),
72
"<div><p>foo</p></div>"
75
def testParseDocument(self):
76
""" Test BlockParser.parseDocument. """
77
lines = ['#foo', '', 'bar', '', ' baz']
78
tree = self.parser.parseDocument(lines)
79
self.assertTrue(isinstance(tree, markdown.util.etree.ElementTree))
80
self.assertTrue(markdown.util.etree.iselement(tree.getroot()))
82
markdown.serializers.to_xhtml_string(tree.getroot()),
83
"<div><h1>foo</h1><p>bar</p><pre><code>baz\n</code></pre></div>"
87
class TestBlockParserState(unittest.TestCase):
88
""" Tests of the State class for BlockParser. """
91
self.state = markdown.blockparser.State()
93
def testBlankState(self):
94
""" Test State when empty. """
95
self.assertEqual(self.state, [])
97
def testSetSate(self):
98
""" Test State.set(). """
99
self.state.set('a_state')
100
self.assertEqual(self.state, ['a_state'])
101
self.state.set('state2')
102
self.assertEqual(self.state, ['a_state', 'state2'])
104
def testIsSate(self):
105
""" Test State.isstate(). """
106
self.assertEqual(self.state.isstate('anything'), False)
107
self.state.set('a_state')
108
self.assertEqual(self.state.isstate('a_state'), True)
109
self.state.set('state2')
110
self.assertEqual(self.state.isstate('state2'), True)
111
self.assertEqual(self.state.isstate('a_state'), False)
112
self.assertEqual(self.state.isstate('missing'), False)
115
""" Test State.reset(). """
116
self.state.set('a_state')
118
self.assertEqual(self.state, [])
119
self.state.set('state1')
120
self.state.set('state2')
122
self.assertEqual(self.state, ['state1'])
125
class TestHtmlStash(unittest.TestCase):
126
""" Test Markdown's HtmlStash. """
129
self.stash = markdown.util.HtmlStash()
130
self.placeholder = self.stash.store('foo')
132
def testSimpleStore(self):
133
""" Test HtmlStash.store. """
134
self.assertEqual(self.placeholder, self.stash.get_placeholder(0))
135
self.assertEqual(self.stash.html_counter, 1)
136
self.assertEqual(self.stash.rawHtmlBlocks, [('foo', False)])
138
def testStoreMore(self):
139
""" Test HtmlStash.store with additional blocks. """
140
placeholder = self.stash.store('bar')
141
self.assertEqual(placeholder, self.stash.get_placeholder(1))
142
self.assertEqual(self.stash.html_counter, 2)
144
self.stash.rawHtmlBlocks,
145
[('foo', False), ('bar', False)]
148
def testSafeStore(self):
149
""" Test HtmlStash.store with 'safe' html. """
150
self.stash.store('bar', True)
152
self.stash.rawHtmlBlocks,
153
[('foo', False), ('bar', True)]
157
""" Test HtmlStash.reset. """
159
self.assertEqual(self.stash.html_counter, 0)
160
self.assertEqual(self.stash.rawHtmlBlocks, [])
162
def testUnsafeHtmlInSafeMode(self):
163
""" Test that unsafe HTML gets escaped in safe_mode. """
164
output = markdown.markdown('foo', extensions=[self.build_extension()], safe_mode='escape')
165
self.assertEqual(output, '<p><script>print("evil")</script></p>')
167
def build_extension(self):
168
""" Build an extention that addes unsafe html to Stash in same_mode. """
169
class Unsafe(markdown.treeprocessors.Treeprocessor):
172
el.text = self.markdown.htmlStash.store('<script>print("evil")</script>', safe=False)
175
class StoreUnsafeHtml(markdown.extensions.Extension):
176
def extendMarkdown(self, md, md_globals):
177
md.treeprocessors.add('unsafe', Unsafe(md), '_end')
179
return StoreUnsafeHtml()
182
class TestOrderedDict(unittest.TestCase):
183
""" Test OrderedDict storage class. """
186
self.odict = markdown.odict.OrderedDict()
187
self.odict['first'] = 'This'
188
self.odict['third'] = 'a'
189
self.odict['fourth'] = 'self'
190
self.odict['fifth'] = 'test'
192
def testValues(self):
193
""" Test output of OrderedDict.values(). """
194
self.assertEqual(list(self.odict.values()), ['This', 'a', 'self', 'test'])
197
""" Test output of OrderedDict.keys(). """
199
list(self.odict.keys()),
200
['first', 'third', 'fourth', 'fifth']
204
""" Test output of OrderedDict.items(). """
206
list(self.odict.items()), [
214
def testAddBefore(self):
215
""" Test adding an OrderedDict item before a given key. """
216
self.odict.add('second', 'is', '<third')
218
list(self.odict.items()), [
227
def testAddAfter(self):
228
""" Test adding an OrderDict item after a given key. """
229
self.odict.add('second', 'is', '>first')
231
list(self.odict.items()), [
240
def testAddAfterEnd(self):
241
""" Test adding an OrderedDict item after the last key. """
242
self.odict.add('sixth', '.', '>fifth')
244
list(self.odict.items()), [
253
def testAdd_begin(self):
254
""" Test adding an OrderedDict item using "_begin". """
255
self.odict.add('zero', 'CRAZY', '_begin')
257
list(self.odict.items()), [
266
def testAdd_end(self):
267
""" Test adding an OrderedDict item using "_end". """
268
self.odict.add('sixth', '.', '_end')
270
list(self.odict.items()), [
279
def testAddBadLocation(self):
280
""" Test Error on bad location in OrderedDict.add(). """
281
self.assertRaises(ValueError, self.odict.add, 'sixth', '.', '<seventh')
282
self.assertRaises(ValueError, self.odict.add, 'second', 'is', 'third')
284
def testDeleteItem(self):
285
""" Test deletion of an OrderedDict item. """
286
del self.odict['fourth']
288
list(self.odict.items()),
289
[('first', 'This'), ('third', 'a'), ('fifth', 'test')]
292
def testChangeValue(self):
293
""" Test OrderedDict change value. """
294
self.odict['fourth'] = 'CRAZY'
296
list(self.odict.items()), [
304
def testChangeOrder(self):
305
""" Test OrderedDict change order. """
306
self.odict.link('fourth', '<third')
308
list(self.odict.items()), [
316
def textBadLink(self):
317
""" Test OrderedDict change order with bad location. """
318
self.assertRaises(ValueError, self.odict.link('fourth', '<bad'))
319
# Check for data integrity ("fourth" wasn't deleted).'
321
list(self.odict.items()), [
330
class TestErrors(unittest.TestCase):
331
""" Test Error Reporting. """
334
# Set warnings to be raised as errors
335
warnings.simplefilter('error')
338
# Reset warning behavior back to default
339
warnings.simplefilter('default')
341
def testNonUnicodeSource(self):
342
""" Test falure on non-unicode source text. """
343
if sys.version_info < (3, 0):
344
source = "foo".encode('utf-16')
345
self.assertRaises(UnicodeDecodeError, markdown.markdown, source)
347
def testBadOutputFormat(self):
348
""" Test failure on bad output_format. """
349
self.assertRaises(KeyError, markdown.Markdown, output_format='invalid')
351
def testLoadExtensionFailure(self):
352
""" Test failure of an extension to load. """
355
markdown.Markdown, extensions=['non_existant_ext']
358
def testLoadBadExtension(self):
359
""" Test loading of an Extension with no makeExtension function. """
360
self.assertRaises(AttributeError, markdown.Markdown, extensions=['markdown.util'])
362
def testNonExtension(self):
363
""" Test loading a non Extension object as an extension. """
364
self.assertRaises(TypeError, markdown.Markdown, extensions=[object])
366
def testBaseExtention(self):
367
""" Test that the base Extension class will raise NotImplemented. """
370
markdown.Markdown, extensions=[markdown.extensions.Extension()]
373
def testMdxExtention(self):
374
""" Test that prepending mdx_ raises a DeprecationWarning. """
375
_create_fake_extension(name='fake', use_old_style=True)
378
markdown.Markdown, extensions=['fake']
381
def testShortNameExtention(self):
382
""" Test that using a short name raises a DeprecationWarning. """
385
markdown.Markdown, extensions=['footnotes']
388
def testStringConfigExtention(self):
389
""" Test that passing configs to an Extension in the name raises a DeprecationWarning. """
392
markdown.Markdown, extensions=['markdown.extension.footnotes(PLACE_MARKER=FOO)']
396
def _create_fake_extension(name, has_factory_func=True, is_wrong_type=False, use_old_style=False):
397
""" Create a fake extension module for testing. """
399
mod_name = '_'.join(['mdx', name])
403
# mod_name must be bytes in Python 2.x
404
mod_name = bytes(mod_name)
405
ext_mod = types.ModuleType(mod_name)
407
def makeExtension(*args, **kwargs):
411
return markdown.extensions.Extension(*args, **kwargs)
414
ext_mod.makeExtension = makeExtension
415
# Warning: this brute forces the extenson module onto the system. Either
416
# this needs to be specificly overriden or a new python session needs to
417
# be started to get rid of this. This should be ok in a testing context.
418
sys.modules[mod_name] = ext_mod
421
class testETreeComments(unittest.TestCase):
423
Test that ElementTree Comments work.
425
These tests should only be a concern when using cElementTree with third
426
party serializers (including markdown's (x)html serializer). While markdown
427
doesn't use ElementTree.Comment itself, we should certainly support any
428
third party extensions which may. Therefore, these tests are included to
429
ensure such support is maintained.
433
# Create comment node
434
self.comment = markdown.util.etree.Comment('foo')
435
if hasattr(markdown.util.etree, 'test_comment'):
436
self.test_comment = markdown.util.etree.test_comment
438
self.test_comment = markdown.util.etree.Comment
440
def testCommentIsComment(self):
441
""" Test that an ElementTree Comment passes the `is Comment` test. """
442
self.assertTrue(self.comment.tag is markdown.util.etree.test_comment)
444
def testCommentIsBlockLevel(self):
445
""" Test that an ElementTree Comment is recognized as BlockLevel. """
446
self.assertFalse(markdown.util.isBlockLevel(self.comment.tag))
448
def testCommentSerialization(self):
449
""" Test that an ElementTree Comment serializes properly. """
451
markdown.serializers.to_html_string(self.comment),
455
def testCommentPrettify(self):
456
""" Test that an ElementTree Comment is prettified properly. """
457
pretty = markdown.treeprocessors.PrettifyTreeprocessor()
458
pretty.run(self.comment)
460
markdown.serializers.to_html_string(self.comment),
465
class testElementTailTests(unittest.TestCase):
466
""" Element Tail Tests """
468
self.pretty = markdown.treeprocessors.PrettifyTreeprocessor()
470
def testBrTailNoNewline(self):
471
""" Test that last <br> in tree has a new line tail """
472
root = markdown.util.etree.Element('root')
473
br = markdown.util.etree.SubElement(root, 'br')
474
self.assertEqual(br.tail, None)
475
self.pretty.run(root)
476
self.assertEqual(br.tail, "\n")
479
class testSerializers(unittest.TestCase):
480
""" Test the html and xhtml serializers. """
483
""" Test HTML serialization. """
484
el = markdown.util.etree.Element('div')
485
p = markdown.util.etree.SubElement(el, 'p')
487
markdown.util.etree.SubElement(el, 'hr')
489
markdown.serializers.to_html_string(el),
490
'<div><p>foo</p><hr></div>'
494
"""" Test XHTML serialization. """
495
el = markdown.util.etree.Element('div')
496
p = markdown.util.etree.SubElement(el, 'p')
498
markdown.util.etree.SubElement(el, 'hr')
500
markdown.serializers.to_xhtml_string(el),
501
'<div><p>foo</p><hr /></div>'
504
def testMixedCaseTags(self):
505
"""" Test preservation of tag case. """
506
el = markdown.util.etree.Element('MixedCase')
507
el.text = 'not valid '
508
em = markdown.util.etree.SubElement(el, 'EMPHASIS')
510
markdown.util.etree.SubElement(el, 'HR')
512
markdown.serializers.to_xhtml_string(el),
513
'<MixedCase>not valid <EMPHASIS>html</EMPHASIS><HR /></MixedCase>'
516
def buildExtension(self):
517
""" Build an extension which registers fakeSerializer. """
518
def fakeSerializer(elem):
519
# Ignore input and return hardcoded output
520
return '<div><p>foo</p></div>'
522
class registerFakeSerializer(markdown.extensions.Extension):
523
def extendMarkdown(self, md, md_globals):
524
md.output_formats['fake'] = fakeSerializer
526
return registerFakeSerializer()
528
def testRegisterSerializer(self):
531
'baz', extensions=[self.buildExtension()], output_format='fake'
537
class testAtomicString(unittest.TestCase):
538
""" Test that AtomicStrings are honored (not parsed). """
541
md = markdown.Markdown()
542
self.inlineprocessor = md.treeprocessors['inline']
544
def testString(self):
545
""" Test that a regular string is parsed. """
546
tree = markdown.util.etree.Element('div')
547
p = markdown.util.etree.SubElement(tree, 'p')
548
p.text = 'some *text*'
549
new = self.inlineprocessor.run(tree)
551
markdown.serializers.to_html_string(new),
552
'<div><p>some <em>text</em></p></div>'
555
def testSimpleAtomicString(self):
556
""" Test that a simple AtomicString is not parsed. """
557
tree = markdown.util.etree.Element('div')
558
p = markdown.util.etree.SubElement(tree, 'p')
559
p.text = markdown.util.AtomicString('some *text*')
560
new = self.inlineprocessor.run(tree)
562
markdown.serializers.to_html_string(new),
563
'<div><p>some *text*</p></div>'
566
def testNestedAtomicString(self):
567
""" Test that a nested AtomicString is not parsed. """
568
tree = markdown.util.etree.Element('div')
569
p = markdown.util.etree.SubElement(tree, 'p')
570
p.text = markdown.util.AtomicString('*some* ')
571
span1 = markdown.util.etree.SubElement(p, 'span')
572
span1.text = markdown.util.AtomicString('*more* ')
573
span2 = markdown.util.etree.SubElement(span1, 'span')
574
span2.text = markdown.util.AtomicString('*text* ')
575
span3 = markdown.util.etree.SubElement(span2, 'span')
576
span3.text = markdown.util.AtomicString('*here*')
577
span3.tail = markdown.util.AtomicString(' *to*')
578
span2.tail = markdown.util.AtomicString(' *test*')
579
span1.tail = markdown.util.AtomicString(' *with*')
580
new = self.inlineprocessor.run(tree)
582
markdown.serializers.to_html_string(new),
583
'<div><p>*some* <span>*more* <span>*text* <span>*here*</span> '
584
'*to*</span> *test*</span> *with*</p></div>'
588
class TestConfigParsing(unittest.TestCase):
589
def assertParses(self, value, result):
590
self.assertTrue(markdown.util.parseBoolValue(value, False) is result)
592
def testBooleansParsing(self):
593
self.assertParses(True, True)
594
self.assertParses('novalue', None)
595
self.assertParses('yES', True)
596
self.assertParses('FALSE', False)
597
self.assertParses(0., False)
598
self.assertParses('none', False)
600
def testPreserveNone(self):
601
self.assertTrue(markdown.util.parseBoolValue('None', preserve_none=True) is None)
602
self.assertTrue(markdown.util.parseBoolValue(None, preserve_none=True) is None)
604
def testInvalidBooleansParsing(self):
605
self.assertRaises(ValueError, markdown.util.parseBoolValue, 'novalue')
608
class TestCliOptionParsing(unittest.TestCase):
609
""" Test parsing of Command Line Interface Options. """
612
self.default_options = {
616
'output_format': 'xhtml1',
619
'extension_configs': {},
624
if os.path.isfile(self.tempfile):
625
os.remove(self.tempfile)
627
def testNoOptions(self):
628
options, logging_level = parse_options([])
629
self.assertEqual(options, self.default_options)
630
self.assertEqual(logging_level, CRITICAL)
632
def testQuietOption(self):
633
options, logging_level = parse_options(['-q'])
634
self.assertTrue(logging_level > CRITICAL)
636
def testVerboseOption(self):
637
options, logging_level = parse_options(['-v'])
638
self.assertEqual(logging_level, WARNING)
640
def testNoisyOption(self):
641
options, logging_level = parse_options(['--noisy'])
642
self.assertEqual(logging_level, DEBUG)
644
def testInputFileOption(self):
645
options, logging_level = parse_options(['foo.txt'])
646
self.default_options['input'] = 'foo.txt'
647
self.assertEqual(options, self.default_options)
649
def testOutputFileOption(self):
650
options, logging_level = parse_options(['-f', 'foo.html'])
651
self.default_options['output'] = 'foo.html'
652
self.assertEqual(options, self.default_options)
654
def testInputAndOutputFileOptions(self):
655
options, logging_level = parse_options(['-f', 'foo.html', 'foo.txt'])
656
self.default_options['output'] = 'foo.html'
657
self.default_options['input'] = 'foo.txt'
658
self.assertEqual(options, self.default_options)
660
def testEncodingOption(self):
661
options, logging_level = parse_options(['-e', 'utf-8'])
662
self.default_options['encoding'] = 'utf-8'
663
self.assertEqual(options, self.default_options)
665
def testSafeModeOption(self):
666
options, logging_level = parse_options(['-s', 'escape'])
667
self.default_options['safe_mode'] = 'escape'
668
self.assertEqual(options, self.default_options)
670
def testOutputFormatOption(self):
671
options, logging_level = parse_options(['-o', 'html5'])
672
self.default_options['output_format'] = 'html5'
673
self.assertEqual(options, self.default_options)
675
def testNoLazyOlOption(self):
676
options, logging_level = parse_options(['-n'])
677
self.default_options['lazy_ol'] = False
678
self.assertEqual(options, self.default_options)
680
def testExtensionOption(self):
681
options, logging_level = parse_options(['-x', 'markdown.extensions.footnotes'])
682
self.default_options['extensions'] = ['markdown.extensions.footnotes']
683
self.assertEqual(options, self.default_options)
685
def testMultipleExtensionOptions(self):
686
options, logging_level = parse_options([
687
'-x', 'markdown.extensions.footnotes',
688
'-x', 'markdown.extensions.smarty'
690
self.default_options['extensions'] = [
691
'markdown.extensions.footnotes',
692
'markdown.extensions.smarty'
694
self.assertEqual(options, self.default_options)
696
def create_config_file(self, config):
697
""" Helper to create temp config files. """
698
if not isinstance(config, markdown.util.string_type):
700
config = yaml.dump(config)
701
fd, self.tempfile = tempfile.mkstemp('.yml')
702
with os.fdopen(fd, 'w') as fp:
705
def testExtensionConfigOption(self):
707
'markdown.extensions.wikilinks': {
708
'base_url': 'http://example.com/',
710
'html_class': 'test',
712
'markdown.extensions.footnotes:FootnotesExtension': {
713
'PLACE_MARKER': '~~~footnotes~~~'
716
self.create_config_file(config)
717
options, logging_level = parse_options(['-c', self.tempfile])
718
self.default_options['extension_configs'] = config
719
self.assertEqual(options, self.default_options)
721
def textBoolExtensionConfigOption(self):
723
'markdown.extensions.toc': {
724
'title': 'Some Title',
729
self.create_config_file(config)
730
options, logging_level = parse_options(['-c', self.tempfile])
731
self.default_options['extension_configs'] = config
732
self.assertEqual(options, self.default_options)
734
def testExtensonConfigOptionAsJSON(self):
736
'markdown.extensions.wikilinks': {
737
'base_url': 'http://example.com/',
739
'html_class': 'test',
741
'markdown.extensions.footnotes:FootnotesExtension': {
742
'PLACE_MARKER': '~~~footnotes~~~'
746
self.create_config_file(json.dumps(config))
747
options, logging_level = parse_options(['-c', self.tempfile])
748
self.default_options['extension_configs'] = config
749
self.assertEqual(options, self.default_options)
751
def testExtensonConfigOptionMissingFile(self):
752
self.assertRaises(IOError, parse_options, ['-c', 'missing_file.yaml'])
754
def testExtensonConfigOptionBadFormat(self):
757
PLACE_MARKER= ~~~footnotes~~~
759
self.create_config_file(config)
760
self.assertRaises(yaml.YAMLError, parse_options, ['-c', self.tempfile])