diff --git a/.gitignore b/.gitignore index b4e9c1d..f04c045 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ pip-log.txt .coverage .tox nosetests.xml +.noseids +.hypothesis # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 3c10227..e96222f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,27 @@ +sudo: false language: python -python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" +python: "3.5" +env: + - TOX_ENV=py27 + - TOX_ENV=py34 + - TOX_ENV=py35 +matrix: + include: + - python: 3.6 + env: TOX_ENV=py36 + include: + - python: 3.7 + env: TOX_ENV=py37 + include: + - python: 3.7 + env: TOX_ENV=lint + include: + - python: 2.7 + env: TOX_ENV=lint addons: apt: packages: - ed -install: - - pip install . -script: nosetests +install: pip install tox +script: tox -e $TOX_ENV +cache: pip diff --git a/README.rst b/README.rst index 3565fbc..5e3d91e 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,7 @@ What The Patch!? .. image:: https://travis-ci.org/cscorley/whatthepatch.svg?style=flat :target: https://travis-ci.org/cscorley/whatthepatch -What The Patch!? is a library for parsing patch files. Its only purpose is to -read a patch file and get it into some usable form by other programs. +What The Patch!? is a library for both parsing and applying patch files. Features --------- @@ -71,47 +70,41 @@ each diff in the patch: .. code-block:: python >>> import whatthepatch - >>> with open('somechanges.patch') as f: + >>> import pprint + >>> with open('tests/casefiles/diff-unified.diff') as f: ... text = f.read() ... >>> for diff in whatthepatch.parse_patch(text): - ... print(diff) + ... print(diff) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE ... - diff(header=header( - index_path=None, - old_path='lao', - old_version='2012-12-26 23:16:54.000000000 -0600', - new_path='tzu', - new_version='2012-12-26 23:16:50.000000000 -0600' - ), - changes=[ - (1, None, 'The Way that can be told of is not the eternal Way;'), - (2, None, 'The name that can be named is not the eternal name.'), - (3, 1, 'The Nameless is the origin of Heaven and Earth;'), - (4, None, 'The Named is the mother of all things.'), - (None, 2, 'The named is the mother of all things.'), - (None, 3, ''), - (5, 4, 'Therefore let there always be non-being,'), - (6, 5, ' so we may see their subtlety,'), - (7, 6, 'And let there always be being,'), - (9, 8, 'The two are the same,'), - (10, 9, 'But after they are produced,'), - (11, 10, ' they have different names.'), - (None, 11, 'They both may be called deep and profound.'), - (None, 12, 'Deeper and more profound,'), - (None, 13, 'The door of all subtleties!') - ] - ) - -*Edited to show structure of the results* + diff(header=header(index_path=None, + old_path='lao', + old_version='2013-01-05 16:56:19.000000000 -0600', + new_path='tzu', + new_version='2013-01-05 16:56:35.000000000 -0600'), + changes=[Change(old=1, new=None, hunk=1, line='The Way that can be told of is not the eternal Way;'), + Change(old=2, new=None, hunk=1, line='The name that can be named is not the eternal name.'), + Change(old=3, new=1, hunk=1, line='The Nameless is the origin of Heaven and Earth;'), + Change(old=4, new=None, hunk=1, line='The Named is the mother of all things.'), + Change(old=None, new=2, hunk=1, line='The named is the mother of all things.'), + Change(old=None, new=3, hunk=1, line=''), Change(old=5, new=4, hunk=1, line='Therefore let there always be non-being,'), + Change(old=6, new=5, hunk=1, line=' so we may see their subtlety,'), + Change(old=7, new=6, hunk=1, line='And let there always be being,'), + Change(old=9, new=8, hunk=2, line='The two are the same,'), + Change(old=10, new=9, hunk=2, line='But after they are produced,'), + Change(old=11, new=10, hunk=2, line=' they have different names.'), + Change(old=None, new=11, hunk=2, line='They both may be called deep and profound.'), + Change(old=None, new=12, hunk=2, line='Deeper and more profound,'), + Change(old=None, new=13, hunk=2, line='The door of all subtleties!')], + text='...') The changes are listed as they are in the patch, but instead of the +/- syntax of the patch, we get a tuple of two numbers and the text of the line. What these numbers indicate are as follows: -#. ``( 1, None, ... )`` indicates line 1 of the file lao was **removed**. -#. ``( None, 2, ... )`` indicates line 2 of the file tzu was **inserted**. -#. ``( 5, 4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**. +#. ``( old=1, new=None, ... )`` indicates line 1 of the file lao was **removed**. +#. ``( old=None, new=2, ... )`` indicates line 2 of the file tzu was **inserted**. +#. ``( old=5, new=4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**. Please note that not all patch formats provide the actual lines modified, so some results will have the text portion of the tuple set to ``None``. @@ -124,15 +117,29 @@ To apply a diff to some lines of text, first read the patch and parse it. .. code-block:: python >>> import whatthepatch - >>> with open('somechanges.patch') as f: + >>> with open('tests/casefiles/diff-default.diff') as f: ... text = f.read() ... - >>> with open('lao') as f: + >>> with open('tests/casefiles/lao') as f: ... lao = f.read() ... >>> diff = [x for x in whatthepatch.parse_patch(text)] >>> diff = diff[0] >>> tzu = whatthepatch.apply_diff(diff, lao) + >>> tzu # doctest: +NORMALIZE_WHITESPACE + ['The Nameless is the origin of Heaven and Earth;', + 'The named is the mother of all things.', + '', + 'Therefore let there always be non-being,', + ' so we may see their subtlety,', + 'And let there always be being,', + ' so we may see their outcome.', + 'The two are the same,', + 'But after they are produced,', + ' they have different names.', + 'They both may be called deep and profound.', + 'Deeper and more profound,', + 'The door of all subtleties!'] Contribute diff --git a/setup.py b/setup.py index 97696d3..3db4ae6 100644 --- a/setup.py +++ b/setup.py @@ -37,12 +37,11 @@ "Topic :: Text Processing", "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ], ) - diff --git a/tests/casefiles/diff-unified-bad.diff b/tests/casefiles/diff-unified-bad.diff new file mode 100644 index 0000000..376d751 --- /dev/null +++ b/tests/casefiles/diff-unified-bad.diff @@ -0,0 +1,19 @@ +--- lao 2013-01-05 16:56:19.000000000 -0600 ++++ tzu 2013-01-05 16:56:35.000000000 -0600 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all tings. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are the same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, ++The door of all subtleties! diff --git a/tests/casefiles/diff-unified-bad2.diff b/tests/casefiles/diff-unified-bad2.diff new file mode 100644 index 0000000..dc6a97c --- /dev/null +++ b/tests/casefiles/diff-unified-bad2.diff @@ -0,0 +1,19 @@ +--- lao 2013-01-05 16:56:19.000000000 -0600 ++++ tzu 2013-01-05 16:56:35.000000000 -0600 +@@ -1,7 +1,6 @@ +-The Way that can be told of is not the eternal Way; +-The name that can be named is not the eternal name. + The Nameless is the origin of Heaven and Earth; +-The Named is the mother of all things. ++The named is the mother of all things. ++ + Therefore let there always be non-being, + so we may see their subtlety, + And let there always be being, +@@ -9,3 +8,6 @@ + The two are te same, + But after they are produced, + they have different names. ++They both may be called deep and profound. ++Deeper and more profound, ++The door of all subtleties! diff --git a/tests/test_apply.py b/tests/test_apply.py index 2a7dfed..465040b 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -1,10 +1,25 @@ # -*- coding: utf-8 -*- +import unittest +import difflib + +from hypothesis import given, assume +from hypothesis import strategies as st +from nose.tools import assert_raises + +from whatthepatch import exceptions import whatthepatch as wtp -import unittest -from io import StringIO +def _apply(src, diff_text, reverse=False, use_patch=False): + diff = list(wtp.parse_patch(diff_text)) + assert len(diff) == 1 + return wtp.apply.apply_diff(diff[0], src, reverse, use_patch) + + +def _apply_r(src, diff_text, reverse=True, use_patch=False): + return _apply(src, diff_text, reverse, use_patch) + class ApplyTestSuite(unittest.TestCase): """Basic test cases.""" @@ -26,62 +41,141 @@ def test_diff_default(self): with open('tests/casefiles/diff-default.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) - - new_text = wtp.apply.apply_diff(diff, self.lao) - self.assertEqual(new_text, self.tzu) + self.assertEqual(_apply(self.lao, diff_text), self.tzu) + self.assertEqual(_apply_r(self.tzu, diff_text), self.lao) def test_diff_context(self): with open('tests/casefiles/diff-context.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) - - new_text = wtp.apply.apply_diff(diff, self.lao) - self.assertEqual(new_text, self.tzu) + self.assertEqual(_apply(self.lao, diff_text), self.tzu) + self.assertEqual(_apply_r(self.tzu, diff_text), self.lao) def test_diff_unified(self): with open('tests/casefiles/diff-unified.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) + self.assertEqual(_apply(self.lao, diff_text), self.tzu) + self.assertEqual(_apply_r(self.tzu, diff_text), self.lao) - new_text = wtp.apply.apply_diff(diff, self.lao) + def test_diff_unified_bad(self): + with open('tests/casefiles/diff-unified-bad.diff') as f: + diff_text = f.read() - self.assertEqual(new_text, self.tzu) + with assert_raises(exceptions.ApplyException) as ec: + _apply(self.lao, diff_text) + + e = ec.exception + e_str = str(e) + assert 'line 4' in e_str + assert 'The Named is the mother of all tings.' in e_str + assert 'The Named is the mother of all things.' in e_str + assert e.hunk == 1 + + def test_diff_unified_bad2(self): + with open('tests/casefiles/diff-unified-bad2.diff') as f: + diff_text = f.read() + + with assert_raises(exceptions.ApplyException) as ec: + _apply(self.lao, diff_text) + + e = ec.exception + e_str = str(e) + assert 'line 9' in e_str + assert 'The two are te same,' in e_str + assert 'The two are the same,' in e_str + assert e.hunk == 2 + + def test_diff_unified_bad_backward(self): + with open('tests/casefiles/diff-unified-bad2.diff') as f: + diff_text = f.read() + + with assert_raises(exceptions.ApplyException) as ec: + _apply(self.tzu, diff_text) + + e = ec.exception + e_str = str(e) + assert 'line 1' in e_str + assert 'The Way that can be told of is not the eternal Way;' in e_str + assert 'The Nameless is the origin of Heaven and Earth;' in e_str + assert e.hunk == 1 + + def test_diff_unified_bad_empty_source(self): + with open('tests/casefiles/diff-unified-bad2.diff') as f: + diff_text = f.read() + + with assert_raises(exceptions.ApplyException) as ec: + _apply('', diff_text) + + e = ec.exception + e_str = str(e) + assert 'line 1' in e_str + assert 'The Way that can be told of is not the eternal Way;' in e_str + assert 'does not exist in source' + assert e.hunk == 1 def test_diff_unified_patchutil(self): with open('tests/casefiles/diff-unified.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) + self.assertEqual( + _apply(self.lao, diff_text, use_patch=True), + (self.tzu, None), + ) + self.assertEqual( + _apply_r(self.tzu, diff_text, use_patch=True), + (self.lao, None), + ) - new_text = wtp.apply.apply_diff(diff, self.lao, use_patch=True) + new_text = _apply(self.lao, diff_text, use_patch=True) self.assertEqual(new_text, (self.tzu, None)) - self.assertRaises(AssertionError, wtp.apply.apply_diff, diff, [''] + self.lao, use_patch=True) + with assert_raises(exceptions.ApplyException): + _apply([''] + self.lao, diff_text, use_patch=True) def test_diff_rcs(self): with open('tests/casefiles/diff-rcs.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) + new_text = _apply(self.lao, diff_text) - new_text = wtp.apply.apply_diff(diff, self.lao) self.assertEqual(new_text, self.tzu) def test_diff_ed(self): - self.maxDiff = None with open('tests/casefiles/diff-ed.diff') as f: diff_text = f.read() - diff = next(wtp.parse_patch(diff_text)) + new_text = _apply(self.lao, diff_text) + self.assertEqual(self.tzu, new_text) - new_text = wtp.apply.apply_diff(diff, self.lao) - self.assertEqual(self.tzu,new_text) - - new_text = wtp.apply.apply_diff(diff, self.lao, use_patch=True) + new_text = _apply(self.lao, diff_text, use_patch=True) self.assertEqual(new_text, (self.tzu, None)) + @given( + st.text(), st.text(), st.text(), st.text(), st.datetimes(), + st.datetimes(), st.integers(), + ) + def test_apply_reverse(self, a, b, fromfile, tofile, fromdate, todate, n): + diff = list(difflib.unified_diff( + a.split('\n'), + b.split('\n'), + fromfile=fromfile, + tofile=tofile, + fromfiledate=fromdate.isoformat(), + tofiledate=todate.isoformat(), + n=n, + lineterm='', + )) + assume(diff) + a_str = a + b_str = b + + self.assertEqual(_apply(a_str.split('\n'), diff[:]), b_str.split('\n')) + self.assertEqual( + _apply_r(b_str.split('\n'), diff[:]), + a_str.split('\n'), + ) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_patch.py b/tests/test_patch.py index e1e0744..8728249 100644 --- a/tests/test_patch.py +++ b/tests/test_patch.py @@ -1,18 +1,115 @@ # -*- coding: utf-8 -*- import whatthepatch as wtp +from whatthepatch.patch import Change, diffobj, header as headerobj import unittest import os -from io import StringIO module_path = os.path.dirname(__file__) -datapath = lambda fname: os.path.join(module_path, 'casefiles', fname) + + +def datapath(fname): + return os.path.join(module_path, 'casefiles', fname) + + +def indent(amount, changes): + indent_str = ' ' * amount + return [(l, r, indent_str + t if t else t) for (l, r, t) in changes] + + +CSC_CHANGES = [ + ( + None, + 1, + '# This is a basic script I wrote to run Bugxplore over the dataset', + ), + (None, 2, ''), + (None, 3, ''), + (1, 4, 'import os'), + (2, 5, 'import sys'), + (3, 6, 'import pickle'), + (5, 8, 'import copy'), + (6, 9, ''), + (7, 10, 'from datetime import date'), + (8, None, 'from Main import main'), + (9, None, 'from Main import _make_dir'), + (None, 11, 'from Bugxplore import main'), + (None, 12, 'from Bugxplore import _make_dir'), + (10, 13, ''), + (11, 14, 'storageDir = \'/tmp/csc/bugs/\''), + (12, 15, 'argv = []'), +] + +DIFFXPLORE_CHANGES = indent(4, [ + (46, 46, ''), + (47, 47, '# Configure option parser'), + (48, 48, "optparser = OptionParser(usage='%prog [options] DIFF_FILE', " + "version='0.1')"), + (49, None, "optparser.set_defaults(output_dir='/tmp/sctdiffs'," + "project_name='default_project')"), + (None, 49, "optparser.set_defaults(output_dir='/tmp/diffs')"), + (50, 50, "optparser.add_option('-o', '--output-dir', dest='output_dir', " + "help='Output directory')"), + (51, 51, "optparser.add_option('-p', '--project_name', " + "dest='project_name', help='Project name')"), + (52, 52, "optparser.add_option('-d', '--delete_cvs_folder', " + "dest='cvs_delete', help='Deletable CVS checkout folder')"), + (53, None, "optparser.add_option('-a', '--append', action='store_true', " + "dest='app', default=False, " + "help='Append to existing MethTerms2 document')"), + (None, 53, ''), + (54, 54, '# Invoke option parser'), + (55, 55, '(options,args) = optparser.parse_args(argv)'), + (56, 56, ''), +]) + +BUGXPLORE_CHANGES = indent(4, [ + (83, 83, ''), + (84, 84, '# Configure option parser'), + (85, 85, "optparser = OptionParser(usage='%prog [options] BUG_IDS', " + "version='0.1')"), + (86, None, "optparser.set_defaults(output_dir='/tmp/bugs'," + "project_name='default_project')"), + (None, 86, "optparser.set_defaults(output_dir='/tmp/bugs')"), + (87, 87, "optparser.add_option('-u', '--bugzilla-url', " + "dest='bugzilla_url', help='URL of Bugzilla installation root')"), + (88, 88, "optparser.add_option('-o', '--output-dir', dest='output_dir', " + "help='Output directory')"), + (89, 89, "optparser.add_option('-p', '--project_name', " + "dest='project_name', help='Project name')"), + (90, 90, "optparser.add_option('-d', '--delete_cvs_folder', " + "dest='cvs_delete', help='Deletable CVS checkout folder')"), + (91, None, "optparser.add_option('-a', '--append', action='store_true', " + "dest='app', default=False, " + "help='Append to existing MethTerms2 document')"), + (None, 91, ''), + (92, 92, '# Invoke option parser'), + (93, 93, '(options,args) = optparser.parse_args(argv)'), +]) + [(94, 94, ' ')] class PatchTestSuite(unittest.TestCase): + def assert_diffs_equal(self, a, b): + def _process_change(c): + return (c.old, c.new, c.line) + + def _process_diffobj(d): + return d._replace(changes=[_process_change(c) for c in d.changes]) + + def _process(d_or_c): + if isinstance(d_or_c, list): + return [_process(o) for o in d_or_c] + if isinstance(d_or_c, diffobj): + return _process_diffobj(d_or_c) + if isinstance(d_or_c, Change): + return _process_change(d_or_c) + return d_or_c + + return self.assertEqual(_process(a), b) + def test_default_diff(self): with open(datapath('diff-default.diff')) as f: text = f.read() @@ -29,11 +126,11 @@ def test_default_diff(self): ] results = list(wtp.patch.parse_default_diff(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) - expected_main = [wtp.patch.diffobj(header=None, changes=expected, text=text)] + expected_main = [diffobj(header=None, changes=expected, text=text)] results_main = list(wtp.patch.parse_patch(text)) - self.assertEqual(results_main, expected_main) + self.assert_diffs_equal(results_main, expected_main) def test_svn_unified_patch(self): with open('tests/casefiles/svn-unified.patch') as f: @@ -42,90 +139,44 @@ def test_svn_unified_patch(self): lines = text.splitlines() expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/csc.py', - old_path='bugtrace/trunk/src/bugtrace/csc.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/csc.py', - new_version=12784, - ), - changes=[ - (None, 1, '# This is a basic script I wrote to run Bugxplore over the dataset'), - (None, 2, ''), - (None, 3, ''), - (1, 4, 'import os'), - (2, 5, 'import sys'), - (3, 6, 'import pickle'), - (5, 8, 'import copy'), - (6, 9, ''), - (7, 10, 'from datetime import date'), - (8, None, 'from Main import main'), - (9, None, 'from Main import _make_dir'), - (None, 11, 'from Bugxplore import main'), - (None, 12, 'from Bugxplore import _make_dir'), - (10, 13, ''), - (11, 14, 'storageDir = \'/tmp/csc/bugs/\''), - (12, 15, 'argv = []'), - ], - text = '\n'.join(lines[:22]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - new_version=12784, - ), - changes=[ - (46, 46, ''), - (47, 47, ' # Configure option parser'), - (48, 48, " optparser = OptionParser(usage='%prog [options] DIFF_FILE', version='0.1')"), - (49, None, " optparser.set_defaults(output_dir='/tmp/sctdiffs',project_name='default_project')"), - (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), - (50, 50, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (51, 51, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (52, 52, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (53, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 53, ''), - (54, 54, ' # Invoke option parser'), - (55, 55, ' (options,args) = optparser.parse_args(argv)'), - (56, 56, ''), - ], - text = '\n'.join(lines[22:40]) + '\n' + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='bugtrace/trunk/src/bugtrace/csc.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/csc.py', + new_version=12784, ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - new_version=12784, - ), - changes=[ - (83, 83, ''), - (84, 84, ' # Configure option parser'), - (85, 85, " optparser = OptionParser(usage='%prog [options] BUG_IDS', version='0.1')"), - (86, None, " optparser.set_defaults(output_dir='/tmp/bugs',project_name='default_project')"), - (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), - (87, 87, " optparser.add_option('-u', '--bugzilla-url', dest='bugzilla_url', help='URL of Bugzilla installation root')"), - (88, 88, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (89, 89, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (90, 90, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (91, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 91, ''), - (92, 92, ' # Invoke option parser'), - (93, 93, ' (options,args) = optparser.parse_args(argv)'), - (94, 94, ' '), - ], - text = '\n'.join(lines[40:]) + '\n' - ) - ] + changes=CSC_CHANGES, + text='\n'.join(lines[:22]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + new_version=12784, + ), + changes=DIFFXPLORE_CHANGES, + text='\n'.join(lines[22:40]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + new_version=12784, + ), + changes=BUGXPLORE_CHANGES, + text='\n'.join(lines[40:]) + '\n' + ) + ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_svn_context_patch(self): with open('tests/casefiles/svn-context.patch') as f: @@ -134,90 +185,44 @@ def test_svn_context_patch(self): lines = text.splitlines() expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/csc.py', - old_path='bugtrace/trunk/src/bugtrace/csc.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/csc.py', - new_version=12784, - ), - changes=[ - (None, 1, '# This is a basic script I wrote to run Bugxplore over the dataset'), - (None, 2, ''), - (None, 3, ''), - (1, 4, 'import os'), - (2, 5, 'import sys'), - (3, 6, 'import pickle'), - (5, 8, 'import copy'), - (6, 9, ''), - (7, 10, 'from datetime import date'), - (8, None, 'from Main import main'), - (9, None, 'from Main import _make_dir'), - (None, 11, 'from Bugxplore import main'), - (None, 12, 'from Bugxplore import _make_dir'), - (10, 13, ''), - (11, 14, 'storageDir = \'/tmp/csc/bugs/\''), - (12, 15, 'argv = []'), - ], - text = '\n'.join(lines[:32]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - new_version=12784, - ), - changes=[ - (46, 46, ''), - (47, 47, ' # Configure option parser'), - (48, 48, " optparser = OptionParser(usage='%prog [options] DIFF_FILE', version='0.1')"), - (49, None, " optparser.set_defaults(output_dir='/tmp/sctdiffs',project_name='default_project')"), - (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), - (50, 50, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (51, 51, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (52, 52, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (53, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 53, ''), - (54, 54, ' # Invoke option parser'), - (55, 55, ' (options,args) = optparser.parse_args(argv)'), - (56, 56, ''), - ], - text = '\n'.join(lines[32:61]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_version=12783, - new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - new_version=12784, - ), - changes=[ - (83, 83, ''), - (84, 84, ' # Configure option parser'), - (85, 85, " optparser = OptionParser(usage='%prog [options] BUG_IDS', version='0.1')"), - (86, None, " optparser.set_defaults(output_dir='/tmp/bugs',project_name='default_project')"), - (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), - (87, 87, " optparser.add_option('-u', '--bugzilla-url', dest='bugzilla_url', help='URL of Bugzilla installation root')"), - (88, 88, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (89, 89, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (90, 90, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (91, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 91, ''), - (92, 92, ' # Invoke option parser'), - (93, 93, ' (options,args) = optparser.parse_args(argv)'), - (94, 94, ' '), - ], - text = '\n'.join(lines[61:]) + '\n' - ) - ] + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='bugtrace/trunk/src/bugtrace/csc.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/csc.py', + new_version=12784, + ), + changes=CSC_CHANGES, + text='\n'.join(lines[:32]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + new_version=12784, + ), + changes=DIFFXPLORE_CHANGES, + text='\n'.join(lines[32:61]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + new_version=12784, + ), + changes=BUGXPLORE_CHANGES, + text='\n'.join(lines[61:]) + '\n' + ), + ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_svn_git_patch(self): with open('tests/casefiles/svn-git.patch') as f: @@ -225,155 +230,119 @@ def test_svn_git_patch(self): lines = text.splitlines() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/csc.py', - old_path='projects/bugs/bugtrace/trunk/src/bugtrace/csc.py', - old_version=12783, - new_path='projects/bugs/bugtrace/trunk/src/bugtrace/csc.py', - new_version=12784, - ), - changes=[ - (None, 1, '# This is a basic script I wrote to run Bugxplore over the dataset'), - (None, 2, ''), - (None, 3, ''), - (1, 4, 'import os'), - (2, 5, 'import sys'), - (3, 6, 'import pickle'), - (5, 8, 'import copy'), - (6, 9, ''), - (7, 10, 'from datetime import date'), - (8, None, 'from Main import main'), - (9, None, 'from Main import _make_dir'), - (None, 11, 'from Bugxplore import main'), - (None, 12, 'from Bugxplore import _make_dir'), - (10, 13, ''), - (11, 14, 'storageDir = \'/tmp/csc/bugs/\''), - (12, 15, 'argv = []'), - ], - text = '\n'.join(lines[:23]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_path='projects/bugs/bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_version=12783, - new_path='projects/bugs/bugtrace/trunk/src/bugtrace/Diffxplore.py', - new_version=12784, - ), - changes=[ - (46, 46, ''), - (47, 47, ' # Configure option parser'), - (48, 48, " optparser = OptionParser(usage='%prog [options] DIFF_FILE', version='0.1')"), - (49, None, " optparser.set_defaults(output_dir='/tmp/sctdiffs',project_name='default_project')"), - (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), - (50, 50, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (51, 51, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (52, 52, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (53, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 53, ''), - (54, 54, ' # Invoke option parser'), - (55, 55, ' (options,args) = optparser.parse_args(argv)'), - (56, 56, ''), - ], - text = '\n'.join(lines[23:42]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_path='projects/bugs/bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_version=12783, - new_path='projects/bugs/bugtrace/trunk/src/bugtrace/Bugxplore.py', - new_version=12784, - ), - changes=[ - (83, 83, ''), - (84, 84, ' # Configure option parser'), - (85, 85, " optparser = OptionParser(usage='%prog [options] BUG_IDS', version='0.1')"), - (86, None, " optparser.set_defaults(output_dir='/tmp/bugs',project_name='default_project')"), - (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), - (87, 87, " optparser.add_option('-u', '--bugzilla-url', dest='bugzilla_url', help='URL of Bugzilla installation root')"), - (88, 88, " optparser.add_option('-o', '--output-dir', dest='output_dir', help='Output directory')"), - (89, 89, " optparser.add_option('-p', '--project_name', dest='project_name', help='Project name')"), - (90, 90, " optparser.add_option('-d', '--delete_cvs_folder', dest='cvs_delete', help='Deletable CVS checkout folder')"), - (91, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 91, ''), - (92, 92, ' # Invoke option parser'), - (93, 93, ' (options,args) = optparser.parse_args(argv)'), - (94, 94, ' '), - ], - text = '\n'.join(lines[42:]) + '\n' - ) - ] - + csc_diff = diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='projects/bugs/bugtrace/trunk/src/bugtrace/csc.py', + old_version=12783, + new_path='projects/bugs/bugtrace/trunk/src/bugtrace/csc.py', + new_version=12784, + ), + changes=CSC_CHANGES, + text='\n'.join(lines[:23]) + '\n' + ) + + diffxplore_path = 'bugtrace/trunk/src/bugtrace/Diffxplore.py' + diffxplore_diff = diffobj( + header=headerobj( + index_path=diffxplore_path, + old_path='projects/bugs/' + diffxplore_path, + old_version=12783, + new_path='projects/bugs/' + diffxplore_path, + new_version=12784, + ), + changes=DIFFXPLORE_CHANGES, + text='\n'.join(lines[23:42]) + '\n' + ) + + bugexplore_path = 'bugtrace/trunk/src/bugtrace/Bugxplore.py' + bugxplore_diff = diffobj( + header=headerobj( + index_path=bugexplore_path, + old_path='projects/bugs/' + bugexplore_path, + old_version=12783, + new_path='projects/bugs/' + bugexplore_path, + new_version=12784, + ), + changes=BUGXPLORE_CHANGES, + text='\n'.join(lines[42:]) + '\n' + ) + + expected = [csc_diff, diffxplore_diff, bugxplore_diff] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_svn_rcs_patch(self): with open('tests/casefiles/svn-rcs.patch') as f: text = f.read() lines = text.splitlines() + + csc_changes = [ + (None, 1, '# This is a basic script I wrote to run ' + 'Bugxplore over the dataset'), + (None, 2, ''), + (None, 3, ''), + (8, None, None), + (9, None, None), + (None, 11, 'from Bugxplore import main'), + (None, 12, 'from Bugxplore import _make_dir'), + ] + + diffxplore_changes = [ + (49, None, None), + (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), + (53, None, None), + (None, 53, ''), + ] + + bugxplore_changes = [ + (86, None, None), + (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), + (91, None, None), + (None, 91, ''), + ] + expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/csc.py', - old_path='bugtrace/trunk/src/bugtrace/csc.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/csc.py', - new_version=None, - ), - changes=[ - (None, 1, '# This is a basic script I wrote to run Bugxplore over the dataset'), - (None, 2, ''), - (None, 3, ''), - (8, None, None), - (9, None, None), - (None, 11, 'from Bugxplore import main'), - (None, 12, 'from Bugxplore import _make_dir'), - ], - text = '\n'.join(lines[:10]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - new_version=None, - ), - changes=[ - (49, None, None), - (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), - (53, None, None), - (None, 53, ''), - ], - text = '\n'.join(lines[10:18]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - new_version=None, - ), - changes=[ - (86, None, None), - (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), - (91, None, None), - (None, 91, ''), - ], - text = '\n'.join(lines[18:]) + '\n' - ) - ] + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='bugtrace/trunk/src/bugtrace/csc.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/csc.py', + new_version=None, + ), + changes=csc_changes, + text='\n'.join(lines[:10]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + new_version=None, + ), + changes=diffxplore_changes, + text='\n'.join(lines[10:18]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + new_version=None, + ), + changes=bugxplore_changes, + text='\n'.join(lines[18:]) + '\n' + ), + ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) - + self.assert_diffs_equal(results, expected) def test_svn_default_patch(self): with open('tests/casefiles/svn-default.patch') as f: @@ -381,62 +350,74 @@ def test_svn_default_patch(self): lines = text.splitlines() + csc_changes = [ + (None, 1, '# This is a basic script I wrote to run ' + 'Bugxplore over the dataset'), + (None, 2, ''), + (None, 3, ''), + (8, None, 'from Main import main'), + (9, None, 'from Main import _make_dir'), + (None, 11, 'from Bugxplore import main'), + (None, 12, 'from Bugxplore import _make_dir'), + ] + + diffxplore_changes = indent(4, [ + (49, None, "optparser.set_defaults(output_dir='/tmp/sctdiffs'," + "project_name='default_project')"), + (None, 49, "optparser.set_defaults(output_dir='/tmp/diffs')"), + (53, None, "optparser.add_option('-a', '--append', " + "action='store_true', dest='app', default=False, " + "help='Append to existing MethTerms2 document')"), + (None, 53, ''), + ]) + + bugxplore_changes = indent(4, [ + (86, None, "optparser.set_defaults(output_dir='/tmp/bugs'," + "project_name='default_project')"), + (None, 86, "optparser.set_defaults(output_dir='/tmp/bugs')"), + (91, None, "optparser.add_option('-a', '--append', " + "action='store_true', dest='app', default=False, " + "help='Append to existing MethTerms2 document')"), + (None, 91, ''), + ]) + expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/csc.py', - old_path='bugtrace/trunk/src/bugtrace/csc.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/csc.py', - new_version=None, - ), - changes=[ - (None, 1, '# This is a basic script I wrote to run Bugxplore over the dataset'), - (None, 2, ''), - (None, 3, ''), - (8, None, 'from Main import main'), - (9, None, 'from Main import _make_dir'), - (None, 11, 'from Bugxplore import main'), - (None, 12, 'from Bugxplore import _make_dir'), - ], - text = '\n'.join(lines[:12]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', - new_version=None, - ), - changes=[ - (49, None, " optparser.set_defaults(output_dir='/tmp/sctdiffs',project_name='default_project')"), - (None, 49, " optparser.set_defaults(output_dir='/tmp/diffs')"), - (53, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 53, ''), - ], - text = '\n'.join(lines[12:22]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - old_version=None, - new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', - new_version=None, - ), - changes=[ - (86, None, " optparser.set_defaults(output_dir='/tmp/bugs',project_name='default_project')"), - (None, 86, " optparser.set_defaults(output_dir='/tmp/bugs')"), - (91, None, " optparser.add_option('-a', '--append', action='store_true', dest='app', default=False, help='Append to existing MethTerms2 document')"), - (None, 91, ''), - ], - text = '\n'.join(lines[22:]) + '\n' - ) - ] + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='bugtrace/trunk/src/bugtrace/csc.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/csc.py', + new_version=None, + ), + changes=csc_changes, + text='\n'.join(lines[:12]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/Diffxplore.py', + new_version=None, + ), + changes=diffxplore_changes, + text='\n'.join(lines[12:22]) + '\n' + ), + diffobj( + header=headerobj( + index_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + old_version=None, + new_path='bugtrace/trunk/src/bugtrace/Bugxplore.py', + new_version=None, + ), + changes=bugxplore_changes, + text='\n'.join(lines[22:]) + '\n' + ) + ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) - + self.assert_diffs_equal(results, expected) def test_git_patch(self): with open('tests/casefiles/git.patch') as f: @@ -444,70 +425,82 @@ def test_git_patch(self): lines = text.splitlines() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='novel/src/java/edu/ua/eng/software/novel/NovelFrame.java', - old_version='aae63fe', - new_path='novel/src/java/edu/ua/eng/software/novel/NovelFrame.java', - new_version='5abbc99' - ), - changes=[ - (135, 135, ' public void actionPerformed(ActionEvent e) {'), - (136, 136, ''), - (137, 137, ' if (e.getActionCommand().equals("OPEN")) {'), - (138, None, ' prefsDialog(prefs.getImportPane());'), - (None, 138, ' prefs.selectImportPane();'), - (None, 139, ' prefsDialog();'), - (139, 140, ' } else if (e.getActionCommand().equals("SET")) {'), - (140, None, ' prefsDialog(prefs.getRepoPane());'), - (None, 141, ' prefs.selectRepoPane();'), - (None, 142, ' prefsDialog();'), - (141, 143, ' } else if (e.getActionCommand().equals("PREFS"))'), - (142, 144, ' prefsDialog();'), - (143, 145, ' else if (e.getActionCommand().equals("EXIT"))'), - (158, 160, ' * Create dialog to handle user preferences'), - (159, 161, ' */'), - (160, 162, ' public void prefsDialog() {'), - (161, None, ''), - (162, 163, ' prefs.setVisible(true);'), - (163, 164, ' }'), - (164, 165, ''), - (165, None, ' public void prefsDialog(Component c) {'), - (166, None, ' prefs.setSelectedComponent(c);'), - (167, None, ' prefsDialog();'), - (168, None, ' }'), - (169, None, ''), - (170, 166, ' /**'), - (171, 167, ' * Open software tutorials, most likely to be hosted online'), - (172, 168, ' * ')], - text = '\n'.join(lines[:34]) + '\n' - ), - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='novel/src/java/edu/ua/eng/software/novel/NovelPrefPane.java', - old_version='a63b57e', - new_path='novel/src/java/edu/ua/eng/software/novel/NovelPrefPane.java', - new_version='919f413' - ), - changes=[ - (18, 18, ''), - (19, 19, ' public abstract void apply();'), - (20, 20, ''), - (None, 21, ' public abstract void applyPrefs();'), - (None, 22, ''), - (21, 23, ' public abstract boolean isChanged();'), - (22, 24, ''), - (23, 25, ' protected Preferences prefs;')], - text = '\n'.join(lines[34:]) + '\n' - ) - ] + novel_frame_changes = indent(4, [ + (135, 135, 'public void actionPerformed(ActionEvent e) {'), + (136, 136, ''), + (137, 137, ' if (e.getActionCommand().equals("OPEN")) {'), + (138, None, ' prefsDialog(prefs.getImportPane());'), + (None, 138, ' prefs.selectImportPane();'), + (None, 139, ' prefsDialog();'), + (139, 140, ' } else if (e.getActionCommand().equals("SET")) {'), + (140, None, ' prefsDialog(prefs.getRepoPane());'), + (None, 141, ' prefs.selectRepoPane();'), + (None, 142, ' prefsDialog();'), + (141, 143, ' } else if (e.getActionCommand().equals("PREFS"))'), + (142, 144, ' prefsDialog();'), + (143, 145, ' else if (e.getActionCommand().equals("EXIT"))'), + (158, 160, ' * Create dialog to handle user preferences'), + (159, 161, ' */'), + (160, 162, 'public void prefsDialog() {'), + (161, None, ''), + (162, 163, ' prefs.setVisible(true);'), + (163, 164, '}'), + (164, 165, ''), + (165, None, 'public void prefsDialog(Component c) {'), + (166, None, ' prefs.setSelectedComponent(c);'), + (167, None, ' prefsDialog();'), + (168, None, '}'), + (169, None, ''), + (170, 166, '/**'), + (171, 167, ' * Open software tutorials, ' + 'most likely to be hosted online'), + (172, 168, ' * ') + ]) + + novel_frame_path = ( + 'novel/src/java/edu/ua/eng/software/novel/NovelFrame.java' + ) + novel_frame = diffobj( + header=headerobj( + index_path=None, + old_path=novel_frame_path, + old_version='aae63fe', + new_path=novel_frame_path, + new_version='5abbc99' + ), + changes=novel_frame_changes, + text='\n'.join(lines[:34]) + '\n' + ) + + novel_pref_frame_path = ( + 'novel/src/java/edu/ua/eng/software/novel/NovelPrefPane.java' + ) + + novel_pref_frame = diffobj( + header=headerobj( + index_path=None, + old_path=novel_pref_frame_path, + old_version='a63b57e', + new_path=novel_pref_frame_path, + new_version='919f413' + ), + changes=[ + (18, 18, ''), + (19, 19, ' public abstract void apply();'), + (20, 20, ''), + (None, 21, ' public abstract void applyPrefs();'), + (None, 22, ''), + (21, 23, ' public abstract boolean isChanged();'), + (22, 24, ''), + (23, 25, ' protected Preferences prefs;')], + text='\n'.join(lines[34:]) + '\n', + ) + + expected = [novel_frame, novel_pref_frame] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_git_oneline_add(self): with open('tests/casefiles/git-oneline-add.diff') as f: @@ -516,8 +509,8 @@ def test_git_oneline_add(self): lines = text.splitlines() expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( + diffobj( + header=headerobj( index_path=None, old_path='/dev/null', old_version='0000000', @@ -533,7 +526,7 @@ def test_git_oneline_add(self): results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_git_oneline_change(self): with open('tests/casefiles/git-oneline-change.diff') as f: @@ -542,8 +535,8 @@ def test_git_oneline_change(self): lines = text.splitlines() expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( + diffobj( + header=headerobj( index_path=None, old_path='oneline.txt', old_version='f56f98d', @@ -559,7 +552,7 @@ def test_git_oneline_change(self): ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_git_oneline_rm(self): with open('tests/casefiles/git-oneline-rm.diff') as f: @@ -568,8 +561,8 @@ def test_git_oneline_rm(self): lines = text.splitlines() expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( + diffobj( + header=headerobj( index_path=None, old_path='oneline.txt', old_version='169ceeb', @@ -584,18 +577,19 @@ def test_git_oneline_rm(self): ] results = list(wtp.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_git_header(self): with open('tests/casefiles/git-header.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = None, - old_path = 'bugtrace/patch.py', - old_version = '8910dfd', - new_path = 'bugtrace/patch.py', - new_version = '456e34f') + expected = headerobj( + index_path=None, + old_path='bugtrace/patch.py', + old_version='8910dfd', + new_path='bugtrace/patch.py', + new_version='456e34f', + ) results = wtp.patch.parse_git_header(text) self.assertEqual(results, expected) @@ -607,12 +601,13 @@ def test_git_header_long(self): with open('tests/casefiles/git-header-long.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = None, - old_path = 'bugtrace/patch.py', - old_version = '18910dfd', - new_path = 'bugtrace/patch.py', - new_version = '2456e34f') + expected = headerobj( + index_path=None, + old_path='bugtrace/patch.py', + old_version='18910dfd', + new_path='bugtrace/patch.py', + new_version='2456e34f', + ) results = wtp.patch.parse_git_header(text) self.assertEqual(results, expected) @@ -624,12 +619,13 @@ def test_git_binary_files(self): with open('tests/casefiles/git-binary-files.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = None, - old_path = '/dev/null', - old_version = '0000000', - new_path = 'project/media/i/asc.gif', - new_version = '71e31ac') + expected = headerobj( + index_path=None, + old_path='/dev/null', + old_version='0000000', + new_path='project/media/i/asc.gif', + new_version='71e31ac', + ) results = wtp.patch.parse_git_header(text) self.assertEqual(results, expected) @@ -641,12 +637,13 @@ def test_svn_header(self): with open('tests/casefiles/svn-header.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = 'bugtrace/trunk/src/bugtrace/csc.py', - old_path = 'bugtrace/trunk/src/bugtrace/csc.py', - old_version = 12783, - new_path = 'bugtrace/trunk/src/bugtrace/csc.py', - new_version = 12784) + expected = headerobj( + index_path='bugtrace/trunk/src/bugtrace/csc.py', + old_path='bugtrace/trunk/src/bugtrace/csc.py', + old_version=12783, + new_path='bugtrace/trunk/src/bugtrace/csc.py', + new_version=12784, + ) results = wtp.patch.parse_svn_header(text) self.assertEqual(results, expected) @@ -657,12 +654,19 @@ def test_cvs_header(self): with open('tests/casefiles/cvs-header.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = 'org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/SafeChunkyInputStream.java', - old_path = 'org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/SafeChunkyInputStream.java', - old_version = '1.6.4.1', - new_path = 'org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/SafeChunkyInputStream.java', - new_version = '1.8') + path = ( + 'org.eclipse.core.resources' + '/src/org/eclipse/core/internal/localstore/' + 'SafeChunkyInputStream.java' + ) + + expected = headerobj( + index_path=path, + old_path=path, + old_version='1.6.4.1', + new_path=path, + new_version='1.8', + ) results = wtp.patch.parse_cvs_header(text) self.assertEqual(results, expected) @@ -673,12 +677,13 @@ def test_unified_header(self): with open('tests/casefiles/unified-header.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = None, - old_path = '/tmp/o', - old_version = '2012-12-22 06:43:35.000000000 -0600', - new_path = '/tmp/n', - new_version = '2012-12-23 20:40:50.000000000 -0600') + expected = headerobj( + index_path=None, + old_path='/tmp/o', + old_version='2012-12-22 06:43:35.000000000 -0600', + new_path='/tmp/n', + new_version='2012-12-23 20:40:50.000000000 -0600', + ) results = wtp.patch.parse_unified_header(text) self.assertEqual(results, expected) @@ -690,12 +695,13 @@ def test_unified_header_notab(self): with open('tests/casefiles/unified-header-notab.diff') as f: text = f.read() - expected = wtp.patch.header( - index_path = None, - old_path = '/tmp/some file', - old_version = '2012-12-22 06:43:35.000000000 -0600', - new_path = '/tmp/n', - new_version = '2012-12-23 20:40:50.000000000 -0600') + expected = headerobj( + index_path=None, + old_path='/tmp/some file', + old_version='2012-12-22 06:43:35.000000000 -0600', + new_path='/tmp/n', + new_version='2012-12-23 20:40:50.000000000 -0600', + ) results = wtp.patch.parse_unified_header(text) self.assertEqual(results, expected) @@ -703,7 +709,6 @@ def test_unified_header_notab(self): results_main = wtp.patch.parse_header(text) self.assertEqual(results_main, expected) - def test_unified_diff(self): with open(datapath('diff-unified.diff')) as f: text = f.read() @@ -730,129 +735,127 @@ def test_unified_diff(self): ] results = list(wtp.patch.parse_unified_diff(text_diff)) - self.assertEqual(results, expected) - - expected_main = wtp.patch.diffobj(header= - wtp.patch.header(index_path=None, - old_path='lao', - old_version='2013-01-05 16:56:19.000000000 -0600', - new_path='tzu', - new_version='2013-01-05 16:56:35.000000000 -0600' - ), - changes=expected, - text=text) + self.assert_diffs_equal(results, expected) + + expected_main = diffobj( + header=headerobj( + index_path=None, + old_path='lao', + old_version='2013-01-05 16:56:19.000000000 -0600', + new_path='tzu', + new_version='2013-01-05 16:56:35.000000000 -0600' + ), + changes=expected, + text=text, + ) results_main = next(wtp.patch.parse_patch(text)) - self.assertEqual(results_main, expected_main) + self.assert_diffs_equal(results_main, expected_main) def test_diff_unified_with_does_not_include_extra_lines(self): with open('tests/casefiles/diff-unified-blah.diff') as f: text = f.read() + changes = [ + (1, None, 'The Way that can be told of is not the eternal Way;'), + (2, None, 'The name that can be named is not the eternal name.'), + (3, 1, 'The Nameless is the origin of Heaven and Earth;'), + (4, None, 'The Named is the mother of all things.'), + (None, 2, 'The named is the mother of all things.'), + (None, 3, ''), + (5, 4, 'Therefore let there always be non-being,'), + (6, 5, ' so we may see their subtlety,'), + (7, 6, 'And let there always be being,'), + (9, 8, 'The two are the same,'), + (10, 9, 'But after they are produced,'), + (11, 10, ' they have different names.'), + (None, 11, 'They both may be called deep and profound.'), + (None, 12, 'Deeper and more profound,'), + (None, 13, 'The door of all subtleties!'), + ] - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='lao', - old_version='2013-01-05 16:56:19.000000000 -0600', - new_path='tzu', - new_version='2013-01-05 16:56:35.000000000 -0600' - ), - changes=[ - (1, None, 'The Way that can be told of is not the eternal Way;'), - (2, None, 'The name that can be named is not the eternal name.'), - (3, 1, 'The Nameless is the origin of Heaven and Earth;'), - (4, None, 'The Named is the mother of all things.'), - (None, 2, 'The named is the mother of all things.'), - (None, 3, ''), - (5, 4, 'Therefore let there always be non-being,'), - (6, 5, ' so we may see their subtlety,'), - (7, 6, 'And let there always be being,'), - (9, 8, 'The two are the same,'), - (10, 9, 'But after they are produced,'), - (11, 10, ' they have different names.'), - (None, 11, 'They both may be called deep and profound.'), - (None, 12, 'Deeper and more profound,'), - (None, 13, 'The door of all subtleties!')], - text=text) - ] - + expected = [diffobj( + header=headerobj( + index_path=None, + old_path='lao', + old_version='2013-01-05 16:56:19.000000000 -0600', + new_path='tzu', + new_version='2013-01-05 16:56:35.000000000 -0600', + ), + changes=changes, + text=text, + )] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_diff_context_with_does_not_include_extra_lines(self): with open('tests/casefiles/diff-context-blah.diff') as f: text = f.read() + changes = [ + (1, None, 'The Way that can be told of is not the eternal Way;'), + (2, None, 'The name that can be named is not the eternal name.'), + (3, 1, 'The Nameless is the origin of Heaven and Earth;'), + (4, None, 'The Named is the mother of all things.'), + (None, 2, 'The named is the mother of all things.'), + (None, 3, ''), + (5, 4, 'Therefore let there always be non-being,'), + (6, 5, ' so we may see their subtlety,'), + (7, 6, 'And let there always be being,'), + (9, 8, 'The two are the same,'), + (10, 9, 'But after they are produced,'), + (11, 10, ' they have different names.'), + (None, 11, 'They both may be called deep and profound.'), + (None, 12, 'Deeper and more profound,'), + (None, 13, 'The door of all subtleties!'), + ] - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='lao', - old_version='2013-01-05 16:56:19.000000000 -0600', - new_path='tzu', - new_version='2013-01-05 16:56:35.000000000 -0600' - ), - changes=[ - (1, None, 'The Way that can be told of is not the eternal Way;'), - (2, None, 'The name that can be named is not the eternal name.'), - (3, 1, 'The Nameless is the origin of Heaven and Earth;'), - (4, None, 'The Named is the mother of all things.'), - (None, 2, 'The named is the mother of all things.'), - (None, 3, ''), - (5, 4, 'Therefore let there always be non-being,'), - (6, 5, ' so we may see their subtlety,'), - (7, 6, 'And let there always be being,'), - (9, 8, 'The two are the same,'), - (10, 9, 'But after they are produced,'), - (11, 10, ' they have different names.'), - (None, 11, 'They both may be called deep and profound.'), - (None, 12, 'Deeper and more profound,'), - (None, 13, 'The door of all subtleties!')], - text=text) - ] - + expected = [diffobj( + header=headerobj( + index_path=None, + old_path='lao', + old_version='2013-01-05 16:56:19.000000000 -0600', + new_path='tzu', + new_version='2013-01-05 16:56:35.000000000 -0600' + ), + changes=changes, + text=text, + )] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_diff_default_with_does_not_include_extra_lines(self): with open('tests/casefiles/diff-default-blah.diff') as f: text = f.read() - expected = [ - wtp.patch.diffobj( - header=None, - changes=[ - (1, None, 'The Way that can be told of is not the eternal Way;'), - (2, None, 'The name that can be named is not the eternal name.'), - (4, None, 'The Named is the mother of all things.'), - (None, 2, 'The named is the mother of all things.'), - (None, 3, ''), - (None, 11, 'They both may be called deep and profound.'), - (None, 12, 'Deeper and more profound,'), - (None, 13, 'The door of all subtleties!')], - text=text) - ] + changes = [ + (1, None, 'The Way that can be told of is not the eternal Way;'), + (2, None, 'The name that can be named is not the eternal name.'), + (4, None, 'The Named is the mother of all things.'), + (None, 2, 'The named is the mother of all things.'), + (None, 3, ''), + (None, 11, 'They both may be called deep and profound.'), + (None, 12, 'Deeper and more profound,'), + (None, 13, 'The door of all subtleties!'), + ] + expected = [diffobj(header=None, changes=changes, text=text)] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) - + self.assert_diffs_equal(results, expected) def test_context_header(self): with open('tests/casefiles/context-header.diff') as f: text = f.read() - - expected = wtp.patch.header( - index_path = None, - old_path = '/tmp/o', - old_version = '2012-12-22 06:43:35.000000000 -0600', - new_path = '/tmp/n', - new_version = '2012-12-23 20:40:50.000000000 -0600') + expected = headerobj( + index_path=None, + old_path='/tmp/o', + old_version='2012-12-22 06:43:35.000000000 -0600', + new_path='/tmp/n', + new_version='2012-12-23 20:40:50.000000000 -0600', + ) results = wtp.patch.parse_context_header(text) self.assertEqual(results, expected) @@ -860,7 +863,6 @@ def test_context_header(self): results_main = wtp.patch.parse_header(text) self.assertEqual(results_main, expected) - def test_context_diff(self): with open(datapath('diff-context.diff')) as f: text = f.read() @@ -869,38 +871,39 @@ def test_context_diff(self): text_diff = '\n'.join(text.splitlines()[2:]) + '\n' expected = [ - (1, None, 'The Way that can be told of is not the eternal Way;'), - (2, None, 'The name that can be named is not the eternal name.'), - (3, 1, 'The Nameless is the origin of Heaven and Earth;'), - (4, None, 'The Named is the mother of all things.'), - (None, 2, 'The named is the mother of all things.'), - (None, 3, ''), - (5, 4, 'Therefore let there always be non-being,'), - (6, 5, ' so we may see their subtlety,'), - (7, 6, 'And let there always be being,'), - (9, 8, 'The two are the same,'), - (10, 9, 'But after they are produced,'), - (11, 10, ' they have different names.'), - (None, 11, 'They both may be called deep and profound.'), - (None, 12, 'Deeper and more profound,'), - (None, 13, 'The door of all subtleties!'), - ] + (1, None, 'The Way that can be told of is not the eternal Way;'), + (2, None, 'The name that can be named is not the eternal name.'), + (3, 1, 'The Nameless is the origin of Heaven and Earth;'), + (4, None, 'The Named is the mother of all things.'), + (None, 2, 'The named is the mother of all things.'), + (None, 3, ''), + (5, 4, 'Therefore let there always be non-being,'), + (6, 5, ' so we may see their subtlety,'), + (7, 6, 'And let there always be being,'), + (9, 8, 'The two are the same,'), + (10, 9, 'But after they are produced,'), + (11, 10, ' they have different names.'), + (None, 11, 'They both may be called deep and profound.'), + (None, 12, 'Deeper and more profound,'), + (None, 13, 'The door of all subtleties!'), + ] results = list(wtp.patch.parse_context_diff(text_diff)) - self.assertEqual(results, expected) - - - expected_main = wtp.patch.diffobj(header= - wtp.patch.header(index_path=None, - old_path='lao', - old_version='2013-01-05 16:56:19.000000000 -0600', - new_path='tzu', - new_version='2013-01-05 16:56:35.000000000 -0600' - ), - changes=expected, - text=text) + self.assert_diffs_equal(results, expected) + + expected_main = diffobj( + header=headerobj( + index_path=None, + old_path='lao', + old_version='2013-01-05 16:56:19.000000000 -0600', + new_path='tzu', + new_version='2013-01-05 16:56:35.000000000 -0600', + ), + changes=expected, + text=text, + ) results_main = next(wtp.patch.parse_patch(text)) - self.assertEqual(results_main, expected_main) + self.assert_diffs_equal(results_main, expected_main) def test_ed_diff(self): with open(datapath('diff-ed.diff')) as f: @@ -917,13 +920,12 @@ def test_ed_diff(self): (None, 13, 'The door of all subtleties!') ] - results = list(wtp.patch.parse_ed_diff(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) - expected_main = [wtp.patch.diffobj(header=None, changes=expected, text=text)] + expected_main = [diffobj(header=None, changes=expected, text=text)] results_main = list(wtp.patch.parse_patch(text)) - self.assertEqual(results_main, expected_main) + self.assert_diffs_equal(results_main, expected_main) def test_rcs_diff(self): with open(datapath('diff-rcs.diff')) as f: @@ -940,93 +942,98 @@ def test_rcs_diff(self): (None, 13, 'The door of all subtleties!') ] - results = list(wtp.patch.parse_rcs_ed_diff(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) - expected_main = [wtp.patch.diffobj(header=None, changes=expected, text=text)] + expected_main = [diffobj(header=None, changes=expected, text=text)] results_main = list(wtp.patch.parse_patch(text)) - self.assertEqual(results_main, expected_main) + self.assert_diffs_equal(results_main, expected_main) def test_embedded_diff_in_comment(self): with open('tests/casefiles/embedded-diff.comment') as f: text = f.read() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='src/org/mozilla/javascript/IRFactory.java', - old_version=None, - new_path='src/org/mozilla/javascript/IRFactory.java', - new_version=None, - ), - changes=[ - (2182, 2182, ' case Token.GETELEM:'), - (2183, 2183, ' decompileElementGet((ElementGet) node);'), - (2184, 2184, ' break;'), - (None, 2185, ' case Token.THIS:'), - (None, 2186, ' decompiler.addToken(node.getType());'), - (None, 2187, ' break;'), - (2185, 2188, ' default:'), - (2186, 2189, ' Kit.codeBug("unexpected token: "'), - (2187, 2190, ' + Token.typeToName(node.getType()));'), - ], - text=text - ), - ] + changes = indent(10, [ + (2182, 2182, 'case Token.GETELEM:'), + (2183, 2183, ' decompileElementGet((ElementGet) node);'), + (2184, 2184, ' break;'), + (None, 2185, 'case Token.THIS:'), + (None, 2186, ' decompiler.addToken(node.getType());'), + (None, 2187, ' break;'), + (2185, 2188, 'default:'), + (2186, 2189, ' Kit.codeBug("unexpected token: "'), + (2187, 2190, ' ' + '+ Token.typeToName(node.getType()));'), + ]) + + expected = [diffobj( + header=headerobj( + index_path=None, + old_path='src/org/mozilla/javascript/IRFactory.java', + old_version=None, + new_path='src/org/mozilla/javascript/IRFactory.java', + new_version=None, + ), + changes=changes, + text=text, + )] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_mozilla_527452_5_comment(self): with open('tests/casefiles/mozilla-527452-5.comment') as f: text = f.read() lines = text.splitlines() - - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='js_instrumentation_proxy/src/org/mozilla/javascript/ast/StringLiteral.java', - old_path='js_instrumentation_proxy/src/org/mozilla/javascript/ast/StringLiteral.java', - old_version=5547, - new_path='js_instrumentation_proxy/src/org/mozilla/javascript/ast/StringLiteral.java', - new_version=None, - ), - changes=[ - (112, 112, ' // TODO(stevey): make sure this unescapes everything properly'), - (113, 113, ' String q = String.valueOf(getQuoteCharacter());'), - (114, 114, ' String rep = "\\\\\\\\" + q;'), # escape the escape that's escaping an escape. wut - (115, None, ' String s = value.replaceAll(q, rep);'), - (None, 115, ' String s = value.replace("\\\\", "\\\\\\\\");'), - (None, 116, ' s = s.replaceAll(q, rep);'), - (116, 117, ' s = s.replaceAll("\\n", "\\\\\\\\n");'), - (117, 118, ' s = s.replaceAll("\\r", "\\\\\\\\r");'), - (118, 119, ' s = s.replaceAll("\\t", "\\\\\\\\t");') - ], - text = '\n'.join(lines[2:]) + '\n' - ), - ] + path = ( + 'js_instrumentation_proxy/src/org/mozilla/' + 'javascript/ast/StringLiteral.java' + ) + header = headerobj( + index_path=path, + old_path=path, + old_version=5547, + new_path=path, + new_version=None, + ) + + changes = indent(8, [ + (112, 112, '// TODO(stevey): make sure this unescapes ' + 'everything properly'), + (113, 113, 'String q = String.valueOf(getQuoteCharacter());'), + (114, 114, r'String rep = "\\\\" + q;'), + (115, None, 'String s = value.replaceAll(q, rep);'), + (None, 115, r'String s = value.replace("\\", "\\\\");'), + (None, 116, 's = s.replaceAll(q, rep);'), + (116, 117, r's = s.replaceAll("\n", "\\\\n");'), + (117, 118, r's = s.replaceAll("\r", "\\\\r");'), + (118, 119, r's = s.replaceAll("\t", "\\\\t");'), + ]) + text = '\n'.join(lines[2:]) + '\n' + + expected = [diffobj(header=header, changes=changes, text=text)] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_dos_unified_cvs(self): with open('tests/casefiles/mozilla-560291.diff') as f: text = f.read() + path = 'src/org/mozilla/javascript/ast/ArrayComprehensionLoop.java' lines = text.splitlines() + header = headerobj( + index_path=path, + old_path=path, + old_version='1.1', + new_path=path, + new_version='15 Sep 2011 02:26:05 -0000' + ) expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='src/org/mozilla/javascript/ast/ArrayComprehensionLoop.java', - old_path='src/org/mozilla/javascript/ast/ArrayComprehensionLoop.java', - old_version='1.1', - new_path='src/org/mozilla/javascript/ast/ArrayComprehensionLoop.java', - new_version='15 Sep 2011 02:26:05 -0000' - ), + diffobj( + header=header, changes=[ (79, 79, ' @Override'), (80, 80, ' public String toSource(int depth) {'), @@ -1039,37 +1046,38 @@ def test_dos_unified_cvs(self): (84, 86, ' + " in "'), (85, 87, ' + iteratedObject.toSource(0)') ], - text = '\n'.join(lines[2:]) + '\n' + text='\n'.join(lines[2:]) + '\n' ) ] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) - + self.assert_diffs_equal(results, expected) def test_old_style_cvs(self): with open('tests/casefiles/mozilla-252983.diff') as f: text = f.read() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='mozilla/js/rhino/CHANGELOG', - old_path='mozilla/js/rhino/CHANGELOG', - old_version='1.1.1.1', - new_path='mozilla/js/rhino/CHANGELOG', - new_version='1.1', # or 'Thu Jan 25 10:59:02 2007' - ), - changes=[ - (1, None, 'This file version: $Id: CHANGELOG,v 1.1.1.1 2007/01/25 15:59:02 inonit Exp $'), - (None, 1, 'This file version: $Id: CHANGELOG,v 1.1 2007/01/25 15:59:02 inonit Exp $'), - (2, 2, ''), - (3, 3, 'Changes since Rhino 1.6R5'), - (4, 4, '========================='), - ], - text=text - ), - ] + changes = [ + (1, None, 'This file version: $Id: CHANGELOG,v 1.1.1.1 ' + '2007/01/25 15:59:02 inonit Exp $'), + (None, 1, 'This file version: $Id: CHANGELOG,v 1.1 ' + '2007/01/25 15:59:02 inonit Exp $'), + (2, 2, ''), + (3, 3, 'Changes since Rhino 1.6R5'), + (4, 4, '========================='), + ] + + expected = [diffobj( + header=headerobj( + index_path='mozilla/js/rhino/CHANGELOG', + old_path='mozilla/js/rhino/CHANGELOG', + old_version='1.1.1.1', + new_path='mozilla/js/rhino/CHANGELOG', + new_version='1.1', # or 'Thu Jan 25 10:59:02 2007' + ), + changes=changes, + text=text, + )] results = wtp.patch.parse_cvs_header(text) self.assertEqual(results, expected[0].header) @@ -1078,37 +1086,39 @@ def test_old_style_cvs(self): self.assertEqual(results, expected[0].header) results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_mozilla_252983_versionless(self): with open('tests/casefiles/mozilla-252983-versionless.diff') as f: text = f.read() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path='mozilla/js/rhino/CHANGELOG', - old_path='mozilla/js/rhino/CHANGELOG', - old_version=None, - new_path='mozilla/js/rhino/CHANGELOG', - new_version=None, - ), - changes=[ - (1, None, 'This file version: $Id: CHANGELOG,v 1.1.1.1 2007/01/25 15:59:02 inonit Exp $'), - (None, 1, 'This file version: $Id: CHANGELOG,v 1.1 2007/01/25 15:59:02 inonit Exp $'), - (2, 2, ''), - (3, 3, 'Changes since Rhino 1.6R5'), - (4, 4, '========================='), - ], - text=text - ), - ] + changes = [ + (1, None, 'This file version: $Id: CHANGELOG,v 1.1.1.1 ' + '2007/01/25 15:59:02 inonit Exp $'), + (None, 1, 'This file version: $Id: CHANGELOG,v 1.1 ' + '2007/01/25 15:59:02 inonit Exp $'), + (2, 2, ''), + (3, 3, 'Changes since Rhino 1.6R5'), + (4, 4, '========================='), + ] + + expected = [diffobj( + header=headerobj( + index_path='mozilla/js/rhino/CHANGELOG', + old_path='mozilla/js/rhino/CHANGELOG', + old_version=None, + new_path='mozilla/js/rhino/CHANGELOG', + new_version=None, + ), + changes=changes, + text=text, + )] results = wtp.patch.parse_header(text) self.assertEqual(results, expected[0].header) results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_apache_attachment_2241(self): with open('tests/casefiles/apache-attachment-2241.diff') as f: @@ -1116,43 +1126,59 @@ def test_apache_attachment_2241(self): lines = text.splitlines() - expected = [ - wtp.patch.diffobj( - header=wtp.patch.header( - index_path=None, - old_path='src\\main\\org\\apache\\tools\\ant\\taskdefs\\optional\\pvcs\\Pvcs.orig', - old_version='Sat Jun 22 16:11:58 2002', - new_path='src\\main\\org\\apache\\tools\\ant\\taskdefs\\optional\\pvcs\\Pvcs.java', - new_version='Fri Jun 28 10:55:50 2002' - ), - changes=[ - (91, 91, ' *'), - (92, 92, ' * @author Thomas Christensen'), - (93, 93, ' * @author Don Jeffery'), - (94, None, ' * @author Steven E. Newton'), - (None, 94, ' * @author Steven E. Newton'), - (95, 95, ' */'), - (96, 96, 'public class Pvcs extends org.apache.tools.ant.Task {'), - (97, 97, ' private String pvcsbin;') - ], - text= '\n'.join(lines) + '\n' - ), - ] + header = headerobj( + index_path=None, + old_path=( + r'src\main\org\apache\tools\ant' + r'\taskdefs\optional\pvcs\Pvcs.orig' + ), + old_version='Sat Jun 22 16:11:58 2002', + new_path=( + r'src\main\org\apache\tools\ant' + r'\taskdefs\optional\pvcs\Pvcs.java' + ), + new_version='Fri Jun 28 10:55:50 2002' + ) + + changes = [ + (91, 91, ' *'), + (92, 92, ' * @author ' + 'Thomas Christensen'), + (93, 93, ' * @author ' + 'Don Jeffery'), + (94, None, ' * @author ' + 'Steven E. Newton'), + (None, 94, ' * @author ' + 'Steven E. Newton'), + (95, 95, ' */'), + (96, 96, 'public class Pvcs extends org.apache.tools.ant.Task {'), + (97, 97, ' private String pvcsbin;') + ] + + text = '\n'.join(lines) + '\n' + + expected = [diffobj(header=header, changes=changes, text=text)] results = list(wtp.patch.parse_patch(text)) - self.assertEqual(results, expected) + self.assert_diffs_equal(results, expected) def test_space_in_path_header(self): with open('tests/casefiles/eclipse-attachment-126343.header') as f: text = f.read() - expected = wtp.patch.header( - index_path = 'test plugin/org/eclipse/jdt/debug/testplugin/ResumeBreakpointListener.java', - old_path = '/dev/null', - old_version = '1 Jan 1970 00:00:00 -0000', - new_path = 'test plugin/org/eclipse/jdt/debug/testplugin/ResumeBreakpointListener.java', - new_version = '1 Jan 1970 00:00:00 -0000' - ) + expected = headerobj( + index_path=( + 'test plugin/org/eclipse/jdt/debug/testplugin/' + 'ResumeBreakpointListener.java' + ), + old_path='/dev/null', + old_version='1 Jan 1970 00:00:00 -0000', + new_path=( + 'test plugin/org/eclipse/jdt/debug/testplugin/' + 'ResumeBreakpointListener.java' + ), + new_version='1 Jan 1970 00:00:00 -0000' + ) results = wtp.patch.parse_header(text) self.assertEqual(results, expected) @@ -1161,12 +1187,15 @@ def test_svn_mixed_line_ends(self): with open('tests/casefiles/svn-mixed_line_ends.patch') as f: text = f.read() - expected_header = wtp.patch.header( - index_path='java/org/apache/catalina/loader/WebappClassLoader.java', - old_path='java/org/apache/catalina/loader/WebappClassLoader.java', - old_version=1346371, - new_path='java/org/apache/catalina/loader/WebappClassLoader.java', - new_version=None) + expected_header = headerobj( + index_path=( + 'java/org/apache/catalina/loader/WebappClassLoader.java' + ), + old_path='java/org/apache/catalina/loader/WebappClassLoader.java', + old_version=1346371, + new_path='java/org/apache/catalina/loader/WebappClassLoader.java', + new_version=None, + ) results = list(wtp.patch.parse_patch(text)) self.assertEqual(results[0].header, expected_header) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2638048 --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py{27,34,35,36,37}, lint + +[testenv] +commands = nosetests --with-doctest --doctest-extension=rst {posargs} +deps = + pytz + nose==1.3.7 + hypothesis==4.14.0 + +[testenv:lint] +deps = + flake8==3.7.7 +commands=flake8 whatthepatch tests setup.py diff --git a/whatthepatch/__init__.py b/whatthepatch/__init__.py index d9e8088..9fb5787 100644 --- a/whatthepatch/__init__.py +++ b/whatthepatch/__init__.py @@ -2,3 +2,5 @@ from .patch import parse_patch from .apply import apply_diff + +__all__ = ['parse_patch', 'apply_diff'] diff --git a/whatthepatch/apply.py b/whatthepatch/apply.py index ed74467..e5d302f 100644 --- a/whatthepatch/apply.py +++ b/whatthepatch/apply.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -import re import subprocess -from . import patch +from . import patch, exceptions from .snippets import which, remove + def apply_patch(diffs): """ Not ready for use yet """ pass @@ -24,69 +24,103 @@ def apply_patch(diffs): with open(diff.header.new_path, 'w') as f: f.write(new_text) -def apply_diff(diff, text, use_patch=False): + +def _apply_diff_with_subprocess(diff, lines, reverse=False): + # call out to patch program + patchexec = which('patch') + if not patchexec: + raise exceptions.SubprocessException('patch program does not exist') + + filepath = '/tmp/wtp-' + str(hash(diff.header)) + oldfilepath = filepath + '.old' + newfilepath = filepath + '.new' + rejfilepath = filepath + '.rej' + patchfilepath = filepath + '.patch' + with open(oldfilepath, 'w') as f: + f.write('\n'.join(lines) + '\n') + + with open(patchfilepath, 'w') as f: + f.write(diff.text) + + args = [patchexec, + '--reverse' if reverse else '--forward', + '--quiet', + '-o', newfilepath, + '-i', patchfilepath, + '-r', rejfilepath, + oldfilepath + ] + ret = subprocess.call(args) + + with open(newfilepath) as f: + lines = f.read().splitlines() + + try: + with open(rejfilepath) as f: + rejlines = f.read().splitlines() + except IOError: + rejlines = None + + remove(oldfilepath) + remove(newfilepath) + remove(rejfilepath) + remove(patchfilepath) + + # do this last to ensure files get cleaned up + if ret != 0: + raise exceptions.SubprocessException('patch program failed', code=ret) + + return lines, rejlines + + +def _reverse(changes): + def _reverse_change(c): + return c._replace( + old=c.new, + new=c.old, + ) + + return [_reverse_change(c) for c in changes] + + +def apply_diff(diff, text, reverse=False, use_patch=False): try: lines = text.splitlines() except AttributeError: lines = list(text) if use_patch: - # call out to patch program - patchexec = which('patch') - assert patchexec # patch program does not exist - - filepath = '/tmp/wtp-' + str(hash(diff.header)) - oldfilepath = filepath + '.old' - newfilepath = filepath + '.new' - rejfilepath = filepath + '.rej' - patchfilepath = filepath + '.patch' - with open(oldfilepath, 'w') as f: - f.write('\n'.join(lines) + '\n') - - with open(patchfilepath, 'w') as f: - f.write(diff.text) - - args = [patchexec, - '--quiet', - '-o', newfilepath, - '-i', patchfilepath, - '-r', rejfilepath, - oldfilepath - ] - ret = subprocess.call(args) - - - with open(newfilepath) as f: - lines = f.read().splitlines() - - try: - with open(rejfilepath) as f: - rejlines = f.read().splitlines() - except IOError: - rejlines = None - - remove(oldfilepath) - remove(newfilepath) - remove(rejfilepath) - remove(patchfilepath) - - # do this last to ensure files get cleaned up - assert ret == 0 # patch return code is success - - return lines, rejlines + return _apply_diff_with_subprocess(diff, lines, reverse) + n_lines = len(lines) + + changes = diff.changes or [] + changes = _reverse(changes) if reverse else changes # check that the source text matches the context of the diff - for old, new, line in diff.changes: + for old, new, hunk, line in changes: # might have to check for line is None here for ed scripts if old is not None and line is not None: - assert len(lines) >= old - assert lines[old-1] == line + if old > n_lines: + raise exceptions.HunkApplyException( + 'context line {n}, "{line}" does not exist in source' + .format(n=old, line=line), + hunk=hunk, + ) + if lines[old-1] != line: + raise exceptions.HunkApplyException( + 'context line {n}, "{line}" does not match "{sl}"'.format( + n=old, + line=line, + sl=lines[old-1] + ), + hunk=hunk, + ) # for calculating the old line r = 0 i = 0 - for old, new, line in diff.changes: + for old, new, hunk, line in changes: if old is not None and new is None: del lines[old-1-r+i] r += 1 @@ -95,13 +129,10 @@ def apply_diff(diff, text, use_patch=False): i += 1 elif old is not None and new is not None: # are we crazy? - #assert new == old - r + i + # assert new == old - r + i # Sometimes, people remove hunks from patches, making these # numbers completely unreliable. Because they're jerks. pass return lines - - - diff --git a/whatthepatch/exceptions.py b/whatthepatch/exceptions.py new file mode 100644 index 0000000..00c53e4 --- /dev/null +++ b/whatthepatch/exceptions.py @@ -0,0 +1,32 @@ +class WhatThePatchException(Exception): + pass + + +class HunkException(WhatThePatchException): + def __init__(self, msg, hunk=None): + self.hunk = hunk + if hunk is not None: + super(HunkException, self).__init__('{msg}, in hunk #{n}'.format( + msg=msg, + n=hunk, + )) + else: + super(HunkException, self).__init__(msg) + + +class ApplyException(WhatThePatchException): + pass + + +class SubprocessException(ApplyException): + def __init__(self, msg, code): + super(SubprocessException, self).__init__(msg) + self.code = code + + +class HunkApplyException(HunkException, ApplyException, ValueError): + pass + + +class ParseException(HunkException, ValueError): + pass diff --git a/whatthepatch/patch.py b/whatthepatch/patch.py index 1a06836..5f09b6a 100644 --- a/whatthepatch/patch.py +++ b/whatthepatch/patch.py @@ -4,11 +4,15 @@ from collections import namedtuple from .snippets import split_by_regex, findall_regex +from . import exceptions -header = namedtuple('header', - 'index_path old_path old_version new_path new_version') +header = namedtuple( + 'header', + 'index_path old_path old_version new_path new_version', +) diffobj = namedtuple('diff', 'header changes text') +Change = namedtuple('Change', 'old new hunk line') file_timestamp_str = '(.+?)(?:\t|:| +)(.*)' # .+? was previously [^:\t\n\r\f\v]+ @@ -16,24 +20,24 @@ # general diff regex diffcmd_header = re.compile('^diff.* (.+) (.+)$') unified_header_index = re.compile('^Index: (.+)$') -unified_header_old_line = re.compile('^--- ' + file_timestamp_str + '$') -unified_header_new_line = re.compile('^\+\+\+ ' + file_timestamp_str + '$') -unified_hunk_start = re.compile('^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$') +unified_header_old_line = re.compile(r'^--- ' + file_timestamp_str + '$') +unified_header_new_line = re.compile(r'^\+\+\+ ' + file_timestamp_str + '$') +unified_hunk_start = re.compile(r'^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$') unified_change = re.compile('^([-+ ])(.*)$') -context_header_old_line = re.compile('^\*\*\* ' + file_timestamp_str + '$') +context_header_old_line = re.compile(r'^\*\*\* ' + file_timestamp_str + '$') context_header_new_line = re.compile('^--- ' + file_timestamp_str + '$') -context_hunk_start = re.compile('^\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*$') -context_hunk_old = re.compile('^\*\*\* (\d+),?(\d*) \*\*\*\*$') -context_hunk_new = re.compile('^--- (\d+),?(\d*) ----$') +context_hunk_start = re.compile(r'^\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*$') +context_hunk_old = re.compile(r'^\*\*\* (\d+),?(\d*) \*\*\*\*$') +context_hunk_new = re.compile(r'^--- (\d+),?(\d*) ----$') context_change = re.compile('^([-+ !]) (.*)$') -ed_hunk_start = re.compile('^(\d+),?(\d*)([acd])$') +ed_hunk_start = re.compile(r'^(\d+),?(\d*)([acd])$') ed_hunk_end = re.compile('^.$') # much like forward ed, but no 'c' type -rcs_ed_hunk_start = re.compile('^([ad])(\d+) ?(\d*)$') +rcs_ed_hunk_start = re.compile(r'^([ad])(\d+) ?(\d*)$') -default_hunk_start = re.compile('^(\d+),?(\d*)([acd])(\d+),?(\d*)$') +default_hunk_start = re.compile(r'^(\d+),?(\d*)([acd])(\d+),?(\d*)$') default_hunk_mid = re.compile('^---$') default_change = re.compile('^([><]) (.*)$') @@ -41,10 +45,10 @@ # git has a special index header and no end part git_diffcmd_header = re.compile('^diff --git a/(.+) b/(.+)$') -git_header_index = re.compile('^index ([a-f0-9]+)..([a-f0-9]+) ?(\d*)$') +git_header_index = re.compile(r'^index ([a-f0-9]+)..([a-f0-9]+) ?(\d*)$') git_header_old_line = re.compile('^--- (.+)$') -git_header_new_line = re.compile('^\+\+\+ (.+)$') -git_header_file_mode = re.compile('^(new|deleted) file mode \d{6}$') +git_header_new_line = re.compile(r'^\+\+\+ (.+)$') +git_header_file_mode = re.compile(r'^(new|deleted) file mode \d{6}$') git_header_binary_file = re.compile('^Binary files (.+) and (.+) differ') bzr_header_index = re.compile("=== (.+)") @@ -52,13 +56,15 @@ bzr_header_new_line = unified_header_new_line svn_header_index = unified_header_index -svn_header_timestamp_version = re.compile('\((?:working copy|revision (\d+))\)') -svn_header_timestamp = re.compile('.*(\(.*\))$') +svn_header_timestamp_version = re.compile( + r'\((?:working copy|revision (\d+))\)' +) +svn_header_timestamp = re.compile(r'.*(\(.*\))$') cvs_header_index = unified_header_index -cvs_header_rcs = re.compile('^RCS file: (.+)(?:,\w{1}$|$)') -cvs_header_timestamp = re.compile('(.+)\t([\d.]+)') -cvs_header_timestamp_colon = re.compile(':([\d.]+)\t(.+)') +cvs_header_rcs = re.compile(r'^RCS file: (.+)(?:,\w{1}$|$)') +cvs_header_timestamp = re.compile(r'(.+)\t([\d.]+)') +cvs_header_timestamp_colon = re.compile(r':([\d.]+)\t(.+)') old_cvs_diffcmd_header = re.compile('^diff.* (.+):(.*) (.+):(.*)$') @@ -69,17 +75,17 @@ def parse_patch(text): lines = text # maybe use this to nuke all of those line endings? - #lines = [x.splitlines()[0] for x in lines] + # lines = [x.splitlines()[0] for x in lines] lines = [x if len(x) == 0 else x.splitlines()[0] for x in lines] check = [ - unified_header_index, - diffcmd_header, - cvs_header_rcs, - git_header_index, - context_header_old_line, - unified_header_old_line, - ] + unified_header_index, + diffcmd_header, + cvs_header_rcs, + git_header_index, + context_header_old_line, + unified_header_old_line, + ] for c in check: diffs = split_by_regex(lines, c) @@ -93,12 +99,14 @@ def parse_patch(text): if h or d: yield diffobj(header=h, changes=d, text=difftext) + def parse_header(text): h = parse_scm_header(text) if h is None: h = parse_diff_header(text) return h + def parse_scm_header(text): try: lines = text.splitlines() @@ -128,12 +136,12 @@ def parse_scm_header(text): new_path = new_path[2:] return header( - index_path=res.index_path, - old_path = old_path, - old_version = res.old_version, - new_path = new_path, - new_version = res.new_version - ) + index_path=res.index_path, + old_path=old_path, + old_version=res.old_version, + new_path=new_path, + new_version=res.new_version + ) else: res = parser(lines) @@ -141,6 +149,7 @@ def parse_scm_header(text): return None + def parse_diff_header(text): try: lines = text.splitlines() @@ -148,21 +157,21 @@ def parse_diff_header(text): lines = text check = [ - (unified_header_new_line, parse_unified_header), - (context_header_old_line, parse_context_header), - (diffcmd_header, parse_diffcmd_header), - # TODO: - # git_header can handle version-less unified headers, but - # will trim a/ and b/ in the paths if they exist... - (git_header_new_line, parse_git_header), - ] + (unified_header_new_line, parse_unified_header), + (context_header_old_line, parse_context_header), + (diffcmd_header, parse_diffcmd_header), + # TODO: + # git_header can handle version-less unified headers, but + # will trim a/ and b/ in the paths if they exist... + (git_header_new_line, parse_git_header), + ] for regex, parser in check: diffs = findall_regex(lines, regex) if len(diffs) > 0: return parser(lines) - return None # no header? + return None # no header? def parse_diff(text): @@ -172,18 +181,19 @@ def parse_diff(text): lines = text check = [ - (unified_hunk_start, parse_unified_diff), - (context_hunk_start, parse_context_diff), - (default_hunk_start, parse_default_diff), - (ed_hunk_start, parse_ed_diff), - (rcs_ed_hunk_start, parse_rcs_ed_diff), - ] + (unified_hunk_start, parse_unified_diff), + (context_hunk_start, parse_context_diff), + (default_hunk_start, parse_default_diff), + (ed_hunk_start, parse_ed_diff), + (rcs_ed_hunk_start, parse_rcs_ed_diff), + ] for hunk, parser in check: diffs = findall_regex(lines, hunk) if len(diffs) > 0: return parser(lines) + def parse_git_header(text): try: lines = text.splitlines() @@ -221,14 +231,16 @@ def parse_git_header(text): if new_path.startswith('b/'): new_path = new_path[2:] return header( - index_path = None, - old_path = old_path, - old_version = over, - new_path = new_path, - new_version = nver) + index_path=None, + old_path=old_path, + old_version=over, + new_path=new_path, + new_version=nver + ) return None + def parse_svn_header(text): try: lines = text.splitlines() @@ -242,60 +254,64 @@ def parse_svn_header(text): while len(lines) > 0: i = svn_header_index.match(lines[0]) del lines[0] - if i: - diff_header = parse_diff_header(lines) - if diff_header: - opath = diff_header.old_path - over = diff_header.old_version - if over: - oend = svn_header_timestamp_version.match(over) - if oend and oend.group(1): - over = int(oend.group(1)) - elif opath: - ts = svn_header_timestamp.match(opath) - if ts: - opath = opath[:-len(ts.group(1))] - oend = svn_header_timestamp_version.match(ts.group(1)) - if oend and oend.group(1): - over = int(oend.group(1)) - - npath = diff_header.new_path - nver = diff_header.new_version - if nver: - nend = svn_header_timestamp_version.match(diff_header.new_version) - if nend and nend.group(1): - nver = int(nend.group(1)) - elif npath: - ts = svn_header_timestamp.match(npath) - if ts: - npath = npath[:-len(ts.group(1))] - nend = svn_header_timestamp_version.match(ts.group(1)) - if nend and nend.group(1): - nver = int(nend.group(1)) - - if type(over) != int: - over = None - - if type(nver) != int: - nver = None + if not i: + continue - return header( - index_path = i.group(1), - old_path = opath, - old_version = over, - new_path = npath, - new_version = nver, - ) + diff_header = parse_diff_header(lines) + if not diff_header: return header( - index_path = i.group(1), - old_path = i.group(1), - old_version = None, - new_path = i.group(1), - new_version = None, - ) + index_path=i.group(1), + old_path=i.group(1), + old_version=None, + new_path=i.group(1), + new_version=None, + ) + + opath = diff_header.old_path + over = diff_header.old_version + if over: + oend = svn_header_timestamp_version.match(over) + if oend and oend.group(1): + over = int(oend.group(1)) + elif opath: + ts = svn_header_timestamp.match(opath) + if ts: + opath = opath[:-len(ts.group(1))] + oend = svn_header_timestamp_version.match(ts.group(1)) + if oend and oend.group(1): + over = int(oend.group(1)) + + npath = diff_header.new_path + nver = diff_header.new_version + if nver: + nend = svn_header_timestamp_version.match(diff_header.new_version) + if nend and nend.group(1): + nver = int(nend.group(1)) + elif npath: + ts = svn_header_timestamp.match(npath) + if ts: + npath = npath[:-len(ts.group(1))] + nend = svn_header_timestamp_version.match(ts.group(1)) + if nend and nend.group(1): + nver = int(nend.group(1)) + + if type(over) != int: + over = None + + if type(nver) != int: + nver = None + + return header( + index_path=i.group(1), + old_path=opath, + old_version=over, + new_path=npath, + new_version=nver, + ) return None + def parse_cvs_header(text): try: lines = text.splitlines() @@ -310,75 +326,76 @@ def parse_cvs_header(text): while len(lines) > 0: i = cvs_header_index.match(lines[0]) del lines[0] - if i: - diff_header = parse_diff_header(lines) - if diff_header: - over = diff_header.old_version - if over: - oend = cvs_header_timestamp.match(over) - oend_c = cvs_header_timestamp_colon.match(over) - if oend: - over = oend.group(2) - elif oend_c: - over = oend_c.group(1) - - nver = diff_header.new_version - if nver: - nend = cvs_header_timestamp.match(nver) - nend_c = cvs_header_timestamp_colon.match(nver) - if nend: - nver = nend.group(2) - elif nend_c: - nver = nend_c.group(1) + if not i: + continue + + diff_header = parse_diff_header(lines) + if diff_header: + over = diff_header.old_version + if over: + oend = cvs_header_timestamp.match(over) + oend_c = cvs_header_timestamp_colon.match(over) + if oend: + over = oend.group(2) + elif oend_c: + over = oend_c.group(1) + + nver = diff_header.new_version + if nver: + nend = cvs_header_timestamp.match(nver) + nend_c = cvs_header_timestamp_colon.match(nver) + if nend: + nver = nend.group(2) + elif nend_c: + nver = nend_c.group(1) - return header( - index_path = i.group(1), - old_path = diff_header.old_path, - old_version = over, - new_path = diff_header.new_path, - new_version = nver, - ) return header( - index_path = i.group(1), - old_path = i.group(1), - old_version = None, - new_path = i.group(1), - new_version = None, - ) + index_path=i.group(1), + old_path=diff_header.old_path, + old_version=over, + new_path=diff_header.new_path, + new_version=nver, + ) + return header( + index_path=i.group(1), + old_path=i.group(1), + old_version=None, + new_path=i.group(1), + new_version=None, + ) elif headers_old: # parse old style headers while len(lines) > 0: i = cvs_header_index.match(lines[0]) del lines[0] - if i: - d = old_cvs_diffcmd_header.match(lines[0]) - if d: - _ = parse_diff_header(lines) # will get rid of the useless stuff for us - over = d.group(2) - if not over: - over = None - - nver = d.group(4) - if not nver: - nver = None - return header( - index_path = i.group(1), - old_path = d.group(1), - old_version = over, - new_path = d.group(3), - new_version = nver, - ) + if not i: + continue + d = old_cvs_diffcmd_header.match(lines[0]) + if not d: return header( - index_path = i.group(1), - old_path = i.group(1), - old_version = None, - new_path = i.group(1), - new_version = None, - ) + index_path=i.group(1), + old_path=i.group(1), + old_version=None, + new_path=i.group(1), + new_version=None, + ) + + # will get rid of the useless stuff for us + parse_diff_header(lines) + over = d.group(2) if d.group(2) else None + nver = d.group(4) if d.group(4) else None + return header( + index_path=i.group(1), + old_path=d.group(1), + old_version=over, + new_path=d.group(3), + new_version=nver, + ) return None + def parse_diffcmd_header(text): try: lines = text.splitlines() @@ -394,15 +411,15 @@ def parse_diffcmd_header(text): del lines[0] if d: return header( - index_path = None, - old_path = d.group(1), - old_version = None, - new_path = d.group(2), - new_version = None, - ) - + index_path=None, + old_path=d.group(1), + old_version=None, + new_path=d.group(2), + new_version=None, + ) return None + def parse_unified_header(text): try: lines = text.splitlines() @@ -426,18 +443,19 @@ def parse_unified_header(text): nver = n.group(2) if len(nver) == 0: - never = None + nver = None return header( - index_path = None, - old_path = o.group(1), - old_version = over, - new_path = n.group(1), - new_version = nver, - ) + index_path=None, + old_path=o.group(1), + old_version=over, + new_path=n.group(1), + new_version=nver, + ) return None + def parse_context_header(text): try: lines = text.splitlines() @@ -461,15 +479,15 @@ def parse_context_header(text): nver = n.group(2) if len(nver) == 0: - never = None + nver = None return header( - index_path = None, - old_path = o.group(1), - old_version = over, - new_path = n.group(1), - new_version = nver, - ) + index_path=None, + old_path=o.group(1), + old_version=over, + new_path=n.group(1), + new_version=nver, + ) return None @@ -490,44 +508,46 @@ def parse_default_diff(text): changes = list() hunks = split_by_regex(lines, default_hunk_start) - for hunk in hunks: - if len(hunk): - r = 0 - i = 0 - while len(hunk) > 0: - h = default_hunk_start.match(hunk[0]) - c = default_change.match(hunk[0]) - del hunk[0] - if h: - old = int(h.group(1)) - if len(h.group(2)) > 0: - old_len = int(h.group(2)) - old + 1 - else: - old_len = 0 - - new = int(h.group(4)) - if len(h.group(5)) > 0: - new_len = int(h.group(5)) - new + 1 - else: - new_len = 0 - - hunk_kind = h.group(3) - elif c: - kind = c.group(1) - line = c.group(2) - - if kind == '<' and (r != old_len or r == 0): - changes.append((old + r, None, line)) - r += 1 - elif kind == '>' and (i != new_len or i == 0): - changes.append((None, new + i, line)) - i += 1 + for hunk_n, hunk in enumerate(hunks): + if not len(hunk): + continue + + r = 0 + i = 0 + while len(hunk) > 0: + h = default_hunk_start.match(hunk[0]) + c = default_change.match(hunk[0]) + del hunk[0] + if h: + old = int(h.group(1)) + if len(h.group(2)) > 0: + old_len = int(h.group(2)) - old + 1 + else: + old_len = 0 + + new = int(h.group(4)) + if len(h.group(5)) > 0: + new_len = int(h.group(5)) - new + 1 + else: + new_len = 0 + + elif c: + kind = c.group(1) + line = c.group(2) + + if kind == '<' and (r != old_len or r == 0): + changes.append(Change(old + r, None, hunk_n, line)) + r += 1 + elif kind == '>' and (i != new_len or i == 0): + changes.append(Change(None, new + i, hunk_n, line)) + i += 1 if len(changes) > 0: return changes return None + def parse_unified_diff(text): try: lines = text.splitlines() @@ -542,7 +562,7 @@ def parse_unified_diff(text): changes = list() hunks = split_by_regex(lines, unified_hunk_start) - for hunk in hunks: + for hunk_n, hunk in enumerate(hunks): # reset counters r = 0 i = 0 @@ -573,13 +593,13 @@ def parse_unified_diff(text): c = None if kind == '-' and (r != old_len or r == 0): - changes.append((old + r, None, line)) + changes.append(Change(old + r, None, hunk_n, line)) r += 1 elif kind == '+' and (i != new_len or i == 0): - changes.append((None, new + i, line)) + changes.append(Change(None, new + i, hunk_n, line)) i += 1 elif kind == ' ' and r != old_len and i != new_len: - changes.append((old + r, new + i, line)) + changes.append(Change(old + r, new + i, hunk_n, line)) r += 1 i += 1 @@ -591,7 +611,6 @@ def parse_unified_diff(text): return None - def parse_context_diff(text): try: lines = text.splitlines() @@ -604,111 +623,128 @@ def parse_context_diff(text): k = 0 changes = list() - old_lines = list() - new_lines = list() hunks = split_by_regex(lines, context_hunk_start) - for hunk in hunks: - if len(hunk): - j = 0 - k = 0 - parts = split_by_regex(hunk, context_hunk_new) - if len(parts) != 2: - raise ValueError("Context diff invalid") + for hunk_n, hunk in enumerate(hunks): + if not len(hunk): + continue - old_hunk = parts[0] - new_hunk = parts[1] + j = 0 + k = 0 + parts = split_by_regex(hunk, context_hunk_new) + if len(parts) != 2: + raise exceptions.ParseException("Context diff invalid", hunk_n) + + old_hunk = parts[0] + new_hunk = parts[1] + + while len(old_hunk) > 0: + o = context_hunk_old.match(old_hunk[0]) + del old_hunk[0] + + if not o: + continue + + old = int(o.group(1)) + old_len = int(o.group(2)) + 1 - old + while len(new_hunk) > 0: + n = context_hunk_new.match(new_hunk[0]) + del new_hunk[0] + + if not n: + continue + + new = int(n.group(1)) + new_len = int(n.group(2)) + 1 - new + break + break + # now have old and new set, can start processing? + if len(old_hunk) > 0 and len(new_hunk) == 0: + msg = "Got unexpected change in removal hunk: " + # only removes left? while len(old_hunk) > 0: - o = context_hunk_old.match(old_hunk[0]) + c = context_change.match(old_hunk[0]) del old_hunk[0] - if o: - old = int(o.group(1)) - old_len = int(o.group(2)) + 1 - old - while len(new_hunk) > 0: - n = context_hunk_new.match(new_hunk[0]) - del new_hunk[0] - if n: - new = int(n.group(1)) - new_len = int(n.group(2)) + 1 - new - break - break - - # now have old and new set, can start processing? - if len(old_hunk) > 0 and len(new_hunk) == 0: - # only removes left? - while len(old_hunk) > 0: - c = context_change.match(old_hunk[0]) - del old_hunk[0] - if c: - kind = c.group(1) - line = c.group(2) - - if kind == '-' and (j != old_len or j == 0): - changes.append((old + j, None, line)) - j += 1 - elif kind == ' ' and ((j != old_len and k != new_len) - or (j == 0 or k == 0)): - changes.append((old + j, new + k, line)) - j += 1 - k += 1 - elif kind == '+' or kind == '!': - raise ValueError("Got unexpected change in removal hunk: " + kind) - - elif len(old_hunk) == 0 and len(new_hunk) > 0: - # only insertions left? - while len(new_hunk) > 0: - c = context_change.match(new_hunk[0]) - del new_hunk[0] - if c: - kind = c.group(1) - line = c.group(2) - - if kind == '+' and (k != new_len or k == 0): - changes.append((None, new + k, line)) - k += 1 - elif kind == ' ' and ((j != old_len and k != new_len) - or (j == 0 or k == 0)): - changes.append((old + j, new + k, line)) - j += 1 - k += 1 - elif kind == '-' or kind == '!': - raise ValueError("Got unexpected change in insertion hunk: " + kind) + + if not c: + continue + + kind = c.group(1) + line = c.group(2) + + if kind == '-' and (j != old_len or j == 0): + changes.append(Change(old + j, None, hunk_n, line)) + j += 1 + elif kind == ' ' and ((j != old_len and k != new_len) + or (j == 0 or k == 0)): + changes.append(Change(old + j, new + k, hunk_n, line)) + j += 1 + k += 1 + elif kind == '+' or kind == '!': + raise exceptions.ParseException(msg + kind, hunk_n) + + continue + + if len(old_hunk) == 0 and len(new_hunk) > 0: + msg = "Got unexpected change in removal hunk: " + # only insertions left? + while len(new_hunk) > 0: + c = context_change.match(new_hunk[0]) + del new_hunk[0] + + if not c: + continue + + kind = c.group(1) + line = c.group(2) + + if kind == '+' and (k != new_len or k == 0): + changes.append(Change(None, new + k, hunk_n, line)) + k += 1 + elif kind == ' ' and ((j != old_len and k != new_len) + or (j == 0 or k == 0)): + changes.append(Change(old + j, new + k, hunk_n, line)) + j += 1 + k += 1 + elif kind == '-' or kind == '!': + raise exceptions.ParseException(msg + kind, hunk_n) + continue + + # both + while len(old_hunk) > 0 and len(new_hunk) > 0: + oc = context_change.match(old_hunk[0]) + nc = context_change.match(new_hunk[0]) + okind = None + nkind = None + + if oc: + okind = oc.group(1) + oline = oc.group(2) + + if nc: + nkind = nc.group(1) + nline = nc.group(2) + + if not (oc or nc): + del old_hunk[0] + del new_hunk[0] + elif okind == ' ' and nkind == ' ' and oline == nline: + changes.append(Change(old + j, new + k, hunk_n, oline)) + j += 1 + k += 1 + del old_hunk[0] + del new_hunk[0] + elif okind == '-' or okind == '!' and (j != old_len or j == 0): + changes.append(Change(old + j, None, hunk_n, oline)) + j += 1 + del old_hunk[0] + elif nkind == '+' or nkind == '!' and (k != old_len or k == 0): + changes.append(Change(None, new + k, hunk_n, nline)) + k += 1 + del new_hunk[0] else: - # both - while len(old_hunk) > 0 and len(new_hunk) > 0: - oc = context_change.match(old_hunk[0]) - nc = context_change.match(new_hunk[0]) - okind = None - nkind = None - - if oc: - okind = oc.group(1) - oline = oc.group(2) - - if nc: - nkind = nc.group(1) - nline = nc.group(2) - - if not (oc or nc): - del old_hunk[0] - del new_hunk[0] - elif okind == ' ' and nkind == ' ' and oline == nline: - changes.append((old + j, new + k, oline)) - j += 1 - k += 1 - del old_hunk[0] - del new_hunk[0] - elif okind == '-' or okind == '!' and (j != old_len or j == 0): - changes.append((old + j, None, oline)) - j += 1 - del old_hunk[0] - elif nkind == '+' or nkind == '!' and (k != old_len or k == 0): - changes.append((None, new + k, nline)) - k += 1 - del new_hunk[0] - else: - return None + return None if len(changes) > 0: return changes @@ -723,7 +759,6 @@ def parse_ed_diff(text): lines = text old = 0 - new = 0 j = 0 k = 0 @@ -734,59 +769,68 @@ def parse_ed_diff(text): hunks = split_by_regex(lines, ed_hunk_start) hunks.reverse() - for hunk in hunks: - if len(hunk): - j = 0 - k = 0 - while len(hunk) > 0: - o = ed_hunk_start.match(hunk[0]) - del hunk[0] - if o: - old = int(o.group(1)) - if len(o.group(2)): - old_end = int(o.group(2)) - else: - old_end = old - - hunk_kind = o.group(3) - if hunk_kind == 'd': - k = 0 - while old_end >= old: - changes.append((old + k, None, None)) - r += 1 - k += 1 - old_end -= 1 - else: - while len(hunk) > 0: - e = ed_hunk_end.match(hunk[0]) - if e: - pass - elif hunk_kind == 'c': - k = 0 - while old_end >= old: - changes.append((old + k, None, None)) - r += 1 - k += 1 - old_end -= 1 - - # I basically have no idea why this works - # for these tests. - changes.append((None, old - r + i + k + j, hunk[0])) - i += 1 - j += 1 - elif hunk_kind == 'a': - changes.append((None, old - r + i + 1, hunk[0])) - i += 1 - - del hunk[0] + for hunk_n, hunk in enumerate(hunks): + if not len(hunk): + continue + j = 0 + k = 0 + while len(hunk) > 0: + o = ed_hunk_start.match(hunk[0]) + del hunk[0] + + if not o: + continue + old = int(o.group(1)) + old_end = int(o.group(2)) if len(o.group(2)) else old + hunk_kind = o.group(3) + if hunk_kind == 'd': + k = 0 + while old_end >= old: + changes.append(Change(old + k, None, hunk_n, None)) + r += 1 + k += 1 + old_end -= 1 + continue + + while len(hunk) > 0: + e = ed_hunk_end.match(hunk[0]) + if not e and hunk_kind == 'c': + k = 0 + while old_end >= old: + changes.append(Change(old + k, None, hunk_n, None)) + r += 1 + k += 1 + old_end -= 1 + + # I basically have no idea why this works + # for these tests. + changes.append(Change( + None, + old - r + i + k + j, + hunk_n, + hunk[0], + )) + i += 1 + j += 1 + if not e and hunk_kind == 'a': + changes.append(Change( + None, + old - r + i + 1, + hunk_n, + hunk[0], + )) + i += 1 + + del hunk[0] if len(changes) > 0: return changes return None + def parse_rcs_ed_diff(text): # much like forward ed, but no 'c' type try: @@ -795,43 +839,43 @@ def parse_rcs_ed_diff(text): lines = text old = 0 - new = 0 j = 0 size = 0 total_change_size = 0 changes = list() - hunks = split_by_regex(lines, rcs_ed_hunk_start) - for hunk in hunks: + for hunk_n, hunk in enumerate(hunks): if len(hunk): j = 0 while len(hunk) > 0: o = rcs_ed_hunk_start.match(hunk[0]) del hunk[0] - if o: - hunk_kind = o.group(1) - old = int(o.group(2)) - size = int(o.group(3)) - - - if hunk_kind == 'a': - old += total_change_size + 1 - total_change_size += size - while size > 0 and len(hunk) > 0: - changes.append((None, old + j, hunk[0])) - j += 1 - size -= 1 - - del hunk[0] - - elif hunk_kind == 'd': - total_change_size -= size - while size > 0: - changes.append((old + j, None, None)) - j += 1 - size -= 1 + + if not o: + continue + + hunk_kind = o.group(1) + old = int(o.group(2)) + size = int(o.group(3)) + + if hunk_kind == 'a': + old += total_change_size + 1 + total_change_size += size + while size > 0 and len(hunk) > 0: + changes.append(Change(None, old + j, hunk_n, hunk[0])) + j += 1 + size -= 1 + + del hunk[0] + + elif hunk_kind == 'd': + total_change_size -= size + while size > 0: + changes.append(Change(old + j, None, hunk_n, None)) + j += 1 + size -= 1 if len(changes) > 0: return changes diff --git a/whatthepatch/snippets.py b/whatthepatch/snippets.py index 18f86f0..9edf99d 100644 --- a/whatthepatch/snippets.py +++ b/whatthepatch/snippets.py @@ -27,6 +27,7 @@ def make_dir(dir): else: raise e + def remove(path): if os.path.exists(path): if os.path.isdir(path): @@ -34,6 +35,7 @@ def remove(path): else: os.remove(path) + # file line length def file_len(fname): with open(fname) as f: @@ -41,6 +43,7 @@ def file_len(fname): pass return i + 1 + # find all indices of a list of strings that match a regex def findall_regex(l, r): found = list() @@ -52,6 +55,7 @@ def findall_regex(l, r): return found + def split_by_regex(l, r): splits = list() indices = findall_regex(l, r) @@ -68,6 +72,7 @@ def split_by_regex(l, r): return splits + # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def which(program): def is_exe(fpath):