test/pylit_test.py: new test for modification time settings
[pylit.git] / test / pylit_test.py
blob1bcf0839ce6e63dec797b01121da7c251081e067
1 #!/usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
4 ## Test the pylit.py literal python module
5 ## =======================================
6 ##
7 ## :Version: 0.2
8 ## :Date: 2005-09-02
9 ## :Copyright: 2006 Guenter Milde.
10 ## Released under the terms of the GNU General Public License
11 ## (v. 2 or later)
13 ## .. contents::
15 ## ::
17 """pylit_test.py: test the "literal python" module"""
19 from pprint import pprint
20 from pylit import *
22 ## Text <-> Code conversion
23 ## ========================
25 ## Test strings
26 ## ------------
28 ## Example of text, code and stripped code with typical features"::
30 text = """.. #!/usr/bin/env python
31 # -*- coding: iso-8859-1 -*-
33 Leading text
35 in several paragraphs followed by a literal block::
37 block1 = 'first block'
39 Some more text and the next block. ::
41 block2 = 'second block'
42 print block1, block2
44 Trailing text.
45 """
46 # print text
48 ## The converter expects the data in separate lines (iterator or list)
49 ## with trailing newlines. We use the `splitlines` string method with
50 ## `keepends=True`::
52 textdata = text.splitlines(True)
53 # print textdata
55 ## If a "code source" is converted with the `strip` option, only text blocks
56 ## are extracted, which leads to::
58 stripped_text = """Leading text
60 in several paragraphs followed by a literal block:
62 Some more text and the next block.
64 Trailing text.
65 """
67 ## The code corresponding to the text test string.
69 ## Using a triple-quoted string for the code (and stripped_code) can create
70 ## problems with the conversion of this test by pylit (as the text parts
71 ## would be converted to text). This is catered for by using a different
72 ## comment string for the text blocks in this file: convert to text with
73 ## ``pylit --comment-string='## ' pylit_test.py``::
75 code = """#!/usr/bin/env python
76 # -*- coding: iso-8859-1 -*-
78 # Leading text
80 # in several paragraphs followed by a literal block::
82 block1 = 'first block'
84 # Some more text and the next block. ::
86 block2 = 'second block'
87 print block1, block2
89 # Trailing text.
90 """
91 # print code
93 codedata = code.splitlines(True)
95 ## Converting the text teststring with the `strip` option leads to::
97 stripped_code = """#!/usr/bin/env python
98 # -*- coding: iso-8859-1 -*-
100 block1 = 'first block'
102 block2 = 'second block'
103 print block1, block2
107 ## pprint(textdata)
108 ## pprint(stripped_code.splitlines(True))
110 ## Containers for special case examples:
112 ## 1. Text2Code samples
113 ## ``textsamples["what"] = (<text data>, <output>, <output (with `strip`)``
114 ## ::
116 textsamples = {}
118 ## 2. Code2Txt samples
119 ## ``codesamples["what"] = (<code data>, <output>, <output (with `strip`)``
120 ## ::
122 codesamples = {}
124 ## Auxiliary function to test the textsamples and codesamples::
126 def check_converter(key, converter, output):
127 print "E:", key
128 extract = converter()
129 pprint(extract)
130 outstr = "".join(["".join(block) for block in extract])
131 print "ist: ", repr(outstr)
132 print "soll:", repr(output)
133 assert output == outstr
135 ## Test generator for textsample tests::
137 def test_Text2Code_samples():
138 for key, sample in textsamples.iteritems():
139 yield (check_converter, key,
140 Text2Code(sample[0].splitlines(True)), sample[1])
141 if len(sample) == 3:
142 yield (check_converter, key,
143 Text2Code(sample[0].splitlines(True), strip=True),
144 sample[2])
146 ## Test generator for codesample tests::
148 def test_Code2Text_samples():
149 for key, sample in codesamples.iteritems():
150 yield (check_converter, key,
151 Code2Text(sample[0].splitlines(True)), sample[1])
152 if len(sample) == 3:
153 yield (check_converter, key,
154 Code2Text(sample[0].splitlines(True), strip=True),
155 sample[2])
157 ## Text2Code
158 ## ---------
160 ## base tests on the "long" test data ::
162 def test_Text2Code():
163 """Test the Text2Code class converting rst->code"""
164 outstr = str(Text2Code(textdata))
165 print code,
166 print outstr
167 assert code == outstr
169 def test_Text2Code_strip():
170 """strip=True should strip text parts"""
171 outstr = str(Text2Code(textdata, strip=True))
172 print "soll", repr(stripped_code)
173 print "ist ", repr(outstr)
174 # pprint(outstr)
175 assert stripped_code == outstr
177 def test_Text2Code_malindented_code_line():
178 """raise error if code line is less indented than code-indent"""
179 data1 = [".. #!/usr/bin/env python\n", # indent == 4 * " "
180 "\n",
181 " print 'hello world'"] # indent == 2 * " "
182 data2 = ["..\t#!/usr/bin/env python\n", # indent == 4 * " "
183 "\n",
184 " print 'hello world'"] # indent == 2 * " "
185 for data in (data1, data2):
186 try:
187 blocks = Text2Code(data)()
188 assert False, "wrong indent did not raise ValueError"
189 except ValueError:
190 pass
192 ## Special Cases
193 ## ~~~~~~~~~~~~~
195 ## Code follows text block without blank line
196 ## ''''''''''''''''''''''''''''''''''''''''''
198 ## End of text block detected ('::') but no paragraph separator (blank line)
199 ## follows
201 ## It is an reStructuredText syntax error, if a "literal block
202 ## marker" is not followed by a blank line.
204 ## Assuming that no double colon at end of line occures accidentially,
205 ## pylit will fix this and issue a warning::
207 textsamples["ensure blank line after text"] = (
208 """text followed by a literal block::
209 block1 = 'first block'
210 """,
211 """# text followed by a literal block::
213 block1 = 'first block'
214 """)
216 ## Text follows code block without blank line
217 ## ''''''''''''''''''''''''''''''''''''''''''
219 ## End of code block detected (a line not more indented than the preceding text
220 ## block)
222 ## reStructuredText syntax demands a paragraph separator (blank line) before
223 ## it.
225 ## Assuming that the unindent is not accidential, pylit fixes this and issues a
226 ## warning::
228 textsamples["ensure blank line after code"] = (
229 """::
231 block1 = 'first block'
232 more text
233 """,
234 """# ::
236 block1 = 'first block'
238 # more text
239 """)
241 ## A double colon on a line on its own
242 ## '''''''''''''''''''''''''''''''''''
244 ## As a double colon is added by the Code2Text conversion after a text block
245 ## (if not already present), it could be removed by the Text2Code conversion
246 ## to keep the source small and pretty.
248 ## However, this would put the text and code source line numbers out of sync,
249 ## which is bad for error reporting, failing doctests, and the `pylit_buffer()`
250 ## function in http://jedmodes.sf.net/mode/pylit.sl ::
252 ## textsamples["should remove single double colon"] = (
253 ## ["text followed by a literal block\n",
254 ## "\n",
255 ## "::\n",
256 ## "\n",
257 ## " foo = 'first'\n"]
258 ## ["", # empty header
259 ## "# text followed by a literal block\n\n",
260 ## "foo = 'first'\n"]
262 ## header samples
263 ## ''''''''''''''
265 ## Convert a leading reStructured text comment (variant: only if there is
266 ## content on the first line) to a leading code block. Return an empty list,
267 ## if there is no header. ::
269 textsamples["simple header"] = (".. print 'hello world'",
270 "print 'hello world'")
272 textsamples["no header (start with text)"] = (
273 """a classical example without header::
275 print 'hello world'
276 """,
277 """# a classical example without header::
279 print 'hello world'
280 """)
282 textsamples["standard header, followed by text"] = (
283 """.. #!/usr/bin/env python
284 # -*- coding: iso-8859-1 -*-
286 a classical example with header::
288 print 'hello world'
289 """,
290 """#!/usr/bin/env python
291 # -*- coding: iso-8859-1 -*-
293 # a classical example with header::
295 print 'hello world'
296 """)
298 textsamples["standard header, followed by code"] = (
299 """.. #!/usr/bin/env python
301 print 'hello world'
302 """,
303 """#!/usr/bin/env python
305 print 'hello world'
306 """)
308 ## Code2Text
309 ## ---------
311 ## Code2Text.strip_literal_marker
313 ## * strip `::`-line as well as preceding blank line if on a line on its own
314 ## * strip `::` if it is preceded by whitespace.
315 ## * convert `::` to a single colon if preceded by text
317 def test_Code2Text_strip_literal_marker():
318 c2t_instance = Code2Text(codedata)
319 samples = ((["text\n", "\n", "::\n", "\n"], ["text\n", "\n"]),
320 (["text ::\n", "\n"], ["text\n", "\n"]),
321 (["text::\n", "\n"], ["text:\n", "\n"]))
322 for (ist, soll) in samples:
323 c2t_instance.strip_literal_marker(ist)
324 print ist, soll
325 assert ist == soll
327 ## base tests on the "long" test strings ::
329 def test_Code2Text():
330 """Test Code2Text class converting code->text"""
331 outstr = str(Code2Text(codedata))
332 # print text
333 print repr(text)
334 print repr(outstr)
335 assert text == outstr
337 def test_Code2Text_strip():
338 """Test Code2Text class converting code->rst with strip=True
340 Should strip code blocks
342 pprint(Code2Text(codedata, strip=True)())
343 outstr = str(Code2Text(codedata, strip=True))
344 print repr(stripped_text)
345 print repr(outstr)
346 assert stripped_text == outstr
348 def test_Code2Text_different_comment_string():
349 """Convert only comments with the specified comment string to text
351 outstr = str(Code2Text(codedata, comment_string="##", strip=True))
352 print outstr
353 assert outstr == ""
354 data = ["# ::\n",
355 "\n",
356 "block1 = 'first block'\n",
357 "\n",
358 "## more text"]
359 soll = [['..'],
360 [' # ::\n',
361 '\n',
362 " block1 = 'first block'\n",
363 '\n'], # leading code block as header
364 [' more text'] # keep space (not part of comment string)
366 output = Code2Text(data, comment_string="##")()
367 print "ist ", output
368 print "soll", soll
369 assert output == soll
371 ## Special cases
372 ## ~~~~~~~~~~~~~
374 ## blank comment line
375 ## ''''''''''''''''''''
377 ## Normally, whitespace in the comment string is significant, i.e. with
378 ## `comment_string = "# "`, a line "#something\n" will count as code.
380 ## However, if a comment line is blank, trailing whitespace in the comment
381 ## string should be ignored, i.e. "#\n" is recognized as a blank text line::
383 codesamples["ignore trailing whitespace in comment string for blank line"] = (
384 """# ::
386 block1 = 'first block'
389 # more text
390 """,
391 """::
393 block1 = 'first block'
396 more text
397 """)
399 ## No blank line after text
400 ## ''''''''''''''''''''''''
402 ## If a matching comment precedes oder follows a code line (i.e. any line
403 ## without matching comment) without a blank line inbetween, it counts as code
404 ## line.
406 ## This will keep small inline comments close to the code they comment on. It
407 ## will also keep blocks together where one commented line doesnot match the
408 ## comment string (the whole block will be kept as commented code)
409 ## ::
411 codesamples["comment before code (without blank line)"] = (
412 """# this is text::
414 # this is a comment
415 foo = 'first'
416 """,
417 """this is text::
419 # this is a comment
420 foo = 'first'
421 """,
422 """this is text:
424 """)
426 codesamples["comment block before code (without blank line)"] = (
427 """# no text (watch the comment sign in the next line)::
429 # this is a comment
430 foo = 'first'
431 """,
432 """.. # no text (watch the comment sign in the next line)::
434 # this is a comment
435 foo = 'first'
436 """,
439 codesamples["comment after code (without blank line)"] = (
440 """# ::
442 block1 = 'first block'
443 # commented code
445 # text again
446 """,
447 """::
449 block1 = 'first block'
450 # commented code
452 text again
453 """,
455 text again
456 """)
458 codesamples["comment block after code (without blank line)"] = (
459 """# ::
461 block1 = 'first block'
462 # commented code
464 # still comment
465 """,
466 """::
468 block1 = 'first block'
469 # commented code
471 # still comment
472 """,
474 """)
476 ## missing literal block marker
477 ## ''''''''''''''''''''''''''''
479 ## If text (with matching comment string) is followed by code (line(s) without
480 ## matching comment string), but there is no double colon at the end, back
481 ## conversion would not recognize the end of text!
483 ## Therefore, pylit adds a paragraph containing only "::" -- the literal block
484 ## marker in expanded form. (While it would in many cases be nicer to add the
485 ## double colon to the last text line, this is not always valid rst syntax,
486 ## e.g. after a section header or a list. Therefore the automatic insertion
487 ## will use the save form, feel free to correct this by hand.)::
489 codesamples["insert missing double colon after text block"] = (
490 """# text followed by code without double colon
492 foo = 'first'
493 """,
494 """text followed by code without double colon
498 foo = 'first'
499 """,
500 """text followed by code without double colon
502 """)
504 ## header samples
505 ## ''''''''''''''
507 ## Convert a header (leading code block) to a reStructured text comment. ::
509 codesamples["no matching comment, just code"] = ("print 'hello world'",
510 ".. print 'hello world'")
512 codesamples["empty header (start with matching comment)"] = (
513 """# a classical example without header::
515 print 'hello world'
516 """,
517 """a classical example without header::
519 print 'hello world'
520 """,
521 """a classical example without header:
523 """)
525 codesamples["standard header, followed by text"] = (
526 """#!/usr/bin/env python
527 # -*- coding: iso-8859-1 -*-
529 # a classical example with header::
531 print 'hello world'
532 """,
533 """.. #!/usr/bin/env python
534 # -*- coding: iso-8859-1 -*-
536 a classical example with header::
538 print 'hello world'
539 """,
540 """a classical example with header:
542 """)
544 codesamples["standard header, followed by code"] = (
545 """#!/usr/bin/env python
547 print 'hello world'
548 """,
549 """.. #!/usr/bin/env python
551 print 'hello world'
552 """,
555 ## Command line use
556 ## ================
558 ## Test the option parsing::
560 def test_Values():
561 values = OptionValues()
562 print values
563 defaults = {"a1": 1, "a2": False}
564 values = OptionValues(defaults)
565 print values, values.as_dict()
566 assert values.a1 == 1
567 assert values.a2 == False
568 assert values.as_dict() == defaults
570 class test_PylitOptions:
571 """Test the PylitOption class"""
572 def setUp(self):
573 self.options = PylitOptions()
575 def test_languages_and_extensions(self):
576 """dictionary of programming languages and extensions"""
577 for ext in [".py", ".sl", ".c"]:
578 assert ext in self.options.code_extensions
579 assert self.options.code_languages[".py"] == "python"
580 assert self.options.code_languages[".sl"] == "slang"
581 assert self.options.code_languages[".c"] == "c++"
583 def test_parse_args(self):
584 """parse cmd line args"""
585 # default should appear in options
586 values = self.options.parse_args(txt2code=False)
587 print values, type(values), dir(values)
588 assert values.txt2code == False
589 # "cmd line arg should appear as option overwriting default"
590 values = self.options.parse_args(["--txt2code"], txt2code=False)
591 assert values.txt2code == True
592 # "1st non option arg is infile, 2nd is outfile"
593 values = self.options.parse_args(["--txt2code", "text.txt", "code.py"])
594 print values.infile
595 assert values.infile == "text.txt"
596 assert values.outfile == "code.py"
597 # set the output (option with argument)
598 values = self.options.parse_args(["--outfile", "code.py"])
599 assert values.outfile == "code.py"
601 def test_parse_args_comment_string(self):
602 # default should appear in options
603 values = self.options.parse_args(["--comment-string=% "])
604 pprint(values.as_dict())
605 assert values.comment_string == "% "
606 # "cmd line arg should appear as option overwriting default"
607 values = self.options.parse_args(["--comment-string=% "],
608 comment_string="##")
609 assert values.comment_string == '% '
611 def test_get_outfile_name(self):
612 """should return a sensible outfile name given an infile name"""
613 # return stdout for stdin
614 assert "-" == self.options.get_outfile_name("-")
615 # return with ".txt" stripped
616 assert "foo.py" == self.options.get_outfile_name("foo.py.txt")
617 # return with ".txt" added if extension marks code file
618 assert "foo.py.txt" == self.options.get_outfile_name("foo.py")
619 assert "foo.sl.txt" == self.options.get_outfile_name("foo.sl")
620 assert "foo.c.txt" == self.options.get_outfile_name("foo.c")
621 # return with ".txt" added if txt2code == False (not None!)
622 assert "foo.py.txt" == self.options.get_outfile_name("foo.py", txt2code=False)
623 # catchall: add ".out" if no other guess possible
624 assert "foo.out" == self.options.get_outfile_name("foo", txt2code=None)
626 def test_complete_values(self):
627 """Basic test of the option completion"""
628 values = optparse.Values()
629 values.infile = "foo"
630 values = self.options.complete_values(values)
631 # the following options should be set:
632 print values.infile # logo, as we give it...
633 print values.outfile
634 assert values.outfile == "foo.out" # fallback extension .out added
635 print values.txt2code
636 assert values.txt2code == True # the default
637 print values.language
638 assert values.language == "python" # the default
640 def test_complete_values_txt(self):
641 """Test the option completion with a text input file"""
642 values = optparse.Values()
643 values.infile = "foo.txt"
644 values = self.options.complete_values(values)
645 # should set outfile (see also `test_get_outfile_name`)
646 assert values.outfile == "foo"
647 # should set conversion direction according to extension
648 assert values.txt2code == True
650 def test_complete_values_code(self):
651 """Test the option completion with a code input file"""
652 values = optparse.Values()
653 values.infile = "foo.py"
654 values = self.options.complete_values(values)
655 # should set outfile name
656 assert values.outfile == "foo.py.txt"
657 # should set conversion directions according to extension
658 print values.txt2code
659 assert values.txt2code == False
661 def test_complete_values_dont_overwrite(self):
662 """The option completion must not overwrite existing option values"""
663 values = optparse.Values()
664 values.infile = "foo.py"
665 values.outfile = "bar.txt"
666 values.txt2code = True
667 values = self.options.complete_values(values)
668 assert values.outfile == "bar.txt"
669 assert values.txt2code == True
671 def test_init(self):
672 options = PylitOptions(["--txt2code", "foo"], txt2code=False)
673 pprint(options)
674 assert options.values.txt2code == True
675 assert options.values.infile == "foo"
677 ## Input and Output streams
678 ## ------------------------
680 ## ::
682 class IOTests:
683 """base class for IO tests, sets up and tears down example files in /tmp
685 txtpath = "/tmp/pylit_test.py.txt"
686 codepath = "/tmp/pylit_test.py"
687 outpath = "/tmp/pylit_test.out"
689 def setUp(self):
690 """Set up the test files"""
691 txtfile = file(self.txtpath, 'w')
692 txtfile.write(text)
693 # txtfile.flush() # is this needed if we close?
694 txtfile.close()
696 codefile = file(self.codepath, 'w')
697 codefile.write(code)
698 # codefile.flush() # is this needed if we close?
699 codefile.close()
701 def tearDown(self):
702 """clean up after all member tests are done"""
703 try:
704 os.unlink(self.txtpath)
705 os.unlink(self.codepath)
706 os.unlink(self.outpath)
707 except OSError:
708 pass
710 class test_Streams(IOTests):
711 def test_is_newer(self):
712 # this __file__ is older, than code file
713 print __file__, os.path.getmtime(__file__)
714 print self.codepath, os.path.getmtime(self.codepath)
716 assert is_newer(self.codepath, __file__) is True, "file1 is newer"
717 assert is_newer(__file__, self.codepath) is False, "file2 is newer"
718 assert is_newer(__file__, "fffo") is True, "file2 doesnot exist"
719 assert is_newer("fflo", __file__) is False, "file1 doesnot exist"
721 assert is_newer(__file__, __file__) is None, "equal is not newer"
722 assert is_newer("fflo", "fffo") is None, "no file exists -> equal"
724 def test_open_streams(self):
725 # default should return stdin and -out:
726 (instream, outstream) = open_streams()
727 assert instream is sys.stdin
728 assert outstream is sys.stdout
730 # open input and output file
731 (instream, outstream) = open_streams(self.txtpath, self.outpath)
732 assert type(instream) == file
733 assert type(outstream) == file
734 # read something from the input
735 assert instream.read() == text
736 # write something to the output
737 outstream.write(text)
738 # check the output, we have to flush first
739 outstream.flush()
740 outfile = file(self.outpath, 'r')
741 assert outfile.read() == text
743 def test_open_streams_no_infile(self):
744 """should exit with usage info if no infile given"""
745 try:
746 (instream, outstream) = open_streams("")
747 assert False, "should rise SystemExit"
748 except IOError:
749 pass
751 ## Another convenience function that returns a converter instance::
753 def test_get_converter():
754 # with default or txt2code
755 converter = get_converter(textdata)
756 print converter.__class__
757 assert converter.__class__ == Text2Code
758 converter = get_converter(textdata, txt2code=False)
759 assert converter.__class__ == Code2Text
761 # the run_doctest runs a doctest on the text version (as doc-string)
762 class test_Run_Doctest(IOTests):
763 """Doctest should run on the text source"""
764 def test_doctest_txt2code(self):
765 (failures, tests) = run_doctest(self.txtpath, txt2code=True)
766 assert (failures, tests) == (0, 0)
767 def test_doctest_code2txt(self):
768 (failures, tests) = run_doctest(self.codepath, txt2code=False)
769 assert (failures, tests) == (0, 0)
771 ## The main() function is called if the script is run from the command line
773 ## ::
775 class test_Main(IOTests):
776 """test default operation from command line
778 def get_output(self):
779 """read and return the content of the output file"""
780 outstream = file(self.outpath, 'r')
781 return outstream.read()
783 def test_text_to_code(self):
784 """test conversion of text file to code file"""
785 main(infile=self.txtpath, outfile=self.outpath)
786 output = self.get_output()
787 print repr(output)
788 assert output == code
790 def test_text_to_code_strip(self):
791 """test conversion of text file to stripped code file"""
792 main(infile=self.txtpath, outfile=self.outpath, strip=True)
793 output = self.get_output()
794 print repr(output)
795 assert output == stripped_code
797 def test_main_code_to_text(self):
798 """test conversion of code file to text file"""
799 main(infile=self.codepath, outfile=self.outpath)
800 output = self.get_output()
801 assert output == text
803 def test_main_code_to_text_strip(self):
804 """test conversion of code file to stripped text file"""
805 main(infile=self.codepath, outfile=self.outpath, strip=True)
806 output = self.get_output()
807 assert output == stripped_text
809 def test_main_diff(self):
810 result = main(infile=self.codepath, diff=True)
811 print "diff return value", result
812 assert result is False # no differences found
814 def test_main_diff_with_differences(self):
815 """diffing a file to itself should fail, as the input is converted"""
816 result = main(infile=self.codepath, outfile=self.codepath, diff=True)
817 print "diff return value", result
818 assert result is True # differences found
820 def test_main_execute(self):
821 result = main(infile=self.txtpath, execute=True)
822 print result
824 def test_main_execute_code(self):
825 result = main(infile=self.codepath, execute=True)
827 import nose
828 nose.runmodule() # requires nose 0.9.1
829 sys.exit()