1 # -*- coding: utf-8 -*-
2 # SPDX-License-Identifier: GPL-2.0+
4 # Copyright 2017 Google, Inc
7 """Functional tests for checking that patman behaves correctly"""
19 from patman.commit import Commit
20 from patman import control
21 from patman import gitutil
22 from patman import patchstream
23 from patman.patchstream import PatchStream
24 from patman.series import Series
25 from patman import settings
26 from u_boot_pylib import terminal
27 from u_boot_pylib import tools
28 from u_boot_pylib.test_util import capture_sys_output
31 from patman import status
33 PATMAN_DIR = pathlib.Path(__file__).parent
34 TEST_DATA_DIR = PATMAN_DIR / 'test/'
37 @contextlib.contextmanager
38 def directory_excursion(directory):
39 """Change directory to `directory` for a limited to the context block."""
48 class TestFunctional(unittest.TestCase):
49 """Functional tests for checking that patman behaves correctly"""
50 leb = (b'Lord Edmund Blackadd\xc3\xabr <weasel@blackadder.org>'.
52 fred = 'Fred Bloggs <f.bloggs@napier.net>'
53 joe = 'Joe Bloggs <joe@napierwallies.co.nz>'
54 mary = 'Mary Bloggs <mary@napierwallies.co.nz>'
59 self.tmpdir = tempfile.mkdtemp(prefix='patman.')
60 self.gitdir = os.path.join(self.tmpdir, 'git')
64 shutil.rmtree(self.tmpdir)
65 terminal.set_print_test_mode(False)
69 """Get the path to a test file
72 fname (str): Filename to obtain
75 str: Full path to file in the test directory
77 return TEST_DATA_DIR / fname
80 def _get_text(cls, fname):
81 """Read a file as text
84 fname (str): Filename to read
89 return open(cls._get_path(fname), encoding='utf-8').read()
92 def _get_patch_name(cls, subject):
93 """Get the filename of a patch given its subject
96 subject (str): Patch subject
99 str: Filename for that patch
101 fname = re.sub('[ :]', '-', subject)
102 return fname.replace('--', '-')
104 def _create_patches_for_test(self, series):
105 """Create patch files for use by tests
107 This copies patch files from the test directory as needed by the series
110 series (Series): Series containing commits to convert
114 str: Cover-letter filename, or None if none
115 fname_list: list of str, each a patch filename
119 for i, commit in enumerate(series.commits):
120 clean_subject = self._get_patch_name(commit.subject)
121 src_fname = '%04d-%s.patch' % (i + 1, clean_subject[:52])
122 fname = os.path.join(self.tmpdir, src_fname)
123 shutil.copy(self._get_path(src_fname), fname)
124 fname_list.append(fname)
125 if series.get('cover'):
126 src_fname = '0000-cover-letter.patch'
127 cover_fname = os.path.join(self.tmpdir, src_fname)
128 fname = os.path.join(self.tmpdir, src_fname)
129 shutil.copy(self._get_path(src_fname), fname)
131 return cover_fname, fname_list
133 def test_basic(self):
134 """Tests the basic flow of patman
136 This creates a series from some hard-coded patches build from a simple
137 tree with the following metadata in the top commit:
141 Series-postfix: some-branch
142 Series-cc: Stefan Brüns <stefan.bruens@rwth-aachen.de>
143 Cover-letter-cc: Lord Mëlchett <clergy@palace.gov>
146 Series-process-log: sort, uniq
154 - Changes only for this commit
157 - Some notes for the cover letter
160 test: A test patch series
161 This is a test of how the cover
166 and this in the first commit:
169 - second revision change
174 from the first commit
182 with the following commands:
184 git log -n2 --reverse >/path/to/tools/patman/test/test01.txt
185 git format-patch --subject-prefix RFC --cover-letter HEAD~2
186 mv 00* /path/to/tools/patman/test
188 It checks these aspects:
189 - git log can be processed by patchstream
190 - emailing patches uses the correct command
191 - CC file has information on each commit
192 - cover letter has the expected text and subject
193 - each patch has the correct subject
194 - dry-run information prints out correctly
195 - unicode is handled correctly
196 - Series-to, Series-cc, Series-prefix, Series-postfix, Cover-letter
197 - Cover-letter-cc, Series-version, Series-changes, Series-notes
201 ignore_bad_tags = False
202 stefan = b'Stefan Br\xc3\xbcns <stefan.bruens@rwth-aachen.de>'.decode('utf-8')
203 rick = 'Richard III <richard@palace.gov>'
204 mel = b'Lord M\xc3\xablchett <clergy@palace.gov>'.decode('utf-8')
205 add_maintainers = [stefan, rick]
211 'u-boot': ['u-boot@lists.denx.de'],
216 text = self._get_text('test01.txt')
217 series = patchstream.get_metadata_for_test(text)
218 cover_fname, args = self._create_patches_for_test(series)
219 get_maintainer_script = str(pathlib.Path(__file__).parent.parent.parent
220 / 'get_maintainer.pl') + ' --norolestats'
221 with capture_sys_output() as out:
222 patchstream.fix_patches(series, args)
223 if cover_fname and series.get('cover'):
224 patchstream.insert_cover_letter(cover_fname, series, count)
226 cc_file = series.MakeCcFile(process_tags, cover_fname,
227 not ignore_bad_tags, add_maintainers,
228 None, get_maintainer_script)
229 cmd = gitutil.email_patches(
230 series, cover_fname, args, dry_run, not ignore_bad_tags,
231 cc_file, in_reply_to=in_reply_to, thread=None)
232 series.ShowActions(args, cmd, process_tags)
233 cc_lines = open(cc_file, encoding='utf-8').read().splitlines()
236 lines = iter(out[0].getvalue().splitlines())
237 self.assertEqual('Cleaned %s patches' % len(series.commits),
239 self.assertEqual('Change log missing for v2', next(lines))
240 self.assertEqual('Change log missing for v3', next(lines))
241 self.assertEqual('Change log for unknown version v4', next(lines))
242 self.assertEqual("Alias 'pci' not found", next(lines))
243 while next(lines) != 'Cc processing complete':
245 self.assertIn('Dry run', next(lines))
246 self.assertEqual('', next(lines))
247 self.assertIn('Send a total of %d patches' % count, next(lines))
249 for i, commit in enumerate(series.commits):
250 self.assertEqual(' %s' % args[i], prev)
253 if 'Cc:' not in prev:
255 self.assertEqual('To: u-boot@lists.denx.de', prev)
256 self.assertEqual('Cc: %s' % stefan, next(lines))
257 self.assertEqual('Version: 3', next(lines))
258 self.assertEqual('Prefix:\t RFC', next(lines))
259 self.assertEqual('Postfix:\t some-branch', next(lines))
260 self.assertEqual('Cover: 4 lines', next(lines))
261 self.assertEqual(' Cc: %s' % self.fred, next(lines))
262 self.assertEqual(' Cc: %s' % self.leb,
264 self.assertEqual(' Cc: %s' % mel, next(lines))
265 self.assertEqual(' Cc: %s' % rick, next(lines))
266 expected = ('Git command: git send-email --annotate '
267 '--in-reply-to="%s" --to "u-boot@lists.denx.de" '
268 '--cc "%s" --cc-cmd "%s send --cc-cmd %s" %s %s'
269 % (in_reply_to, stefan, sys.argv[0], cc_file, cover_fname,
271 self.assertEqual(expected, next(lines))
273 self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
275 '%s %s\0%s\0%s\0%s' % (args[1], self.fred, self.leb, rick, stefan),
279 This is a test of how the cover
285 from the first commit
292 - Some notes for the cover letter
295 pci: Correct cast for sandbox
296 fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
300 lib/efi_loader/efi_memory.c | 1 +
302 4 files changed, 6 insertions(+), 2 deletions(-)
308 lines = open(cover_fname, encoding='utf-8').read().splitlines()
310 'Subject: [RFC PATCH some-branch v3 0/2] test: A test patch series',
312 self.assertEqual(expected.splitlines(), lines[7:])
314 for i, fname in enumerate(args):
315 lines = open(fname, encoding='utf-8').read().splitlines()
316 subject = [line for line in lines if line.startswith('Subject')]
317 self.assertEqual('Subject: [RFC %d/%d]' % (i + 1, count),
320 # Check that we got our commit notes
330 (no changes since v2)
333 - second revision change'''
345 - Changes only for this commit'''
348 expected = expected.splitlines()
349 self.assertEqual(expected, lines[start:(start+len(expected))])
351 def make_commit_with_file(self, subject, body, fname, text):
352 """Create a file and add it to the git repo with a new commit
355 subject (str): Subject for the commit
356 body (str): Body text of the commit
357 fname (str): Filename of file to create
358 text (str): Text to put into the file
360 path = os.path.join(self.gitdir, fname)
361 tools.write_file(path, text, binary=False)
362 index = self.repo.index
364 # pylint doesn't seem to find this
365 # pylint: disable=E1101
366 author = pygit2.Signature('Test user', 'test@email.com')
368 tree = index.write_tree()
369 message = subject + '\n' + body
370 self.repo.create_commit('HEAD', author, committer, message, tree,
371 [self.repo.head.target])
373 def make_git_tree(self):
374 """Make a simple git tree suitable for testing
376 It has three branches:
377 'base' has two commits: PCI, main
378 'first' has base as upstream and two more commits: I2C, SPI
379 'second' has base as upstream and three more: video, serial, bootm
382 pygit2.Repository: repository
384 repo = pygit2.init_repository(self.gitdir)
386 new_tree = repo.TreeBuilder().write()
388 # pylint doesn't seem to find this
389 # pylint: disable=E1101
390 author = pygit2.Signature('Test user', 'test@email.com')
392 _ = repo.create_commit('HEAD', author, committer, 'Created master',
395 self.make_commit_with_file('Initial commit', '''
398 ''', 'README', '''This is the README file
399 describing this project
400 in very little detail''')
402 self.make_commit_with_file('pci: PCI implementation', '''
403 Here is a basic PCI implementation
405 ''', 'pci.c', '''This is a file
407 and some more things''')
408 self.make_commit_with_file('main: Main program', '''
409 Hello here is the second commit.
410 ''', 'main.c', '''This is the main file
411 there is very little here
412 but we can always add more later
416 Series-cc: Barry Crump <bcrump@whataroa.nz>
418 base_target = repo.revparse_single('HEAD')
419 self.make_commit_with_file('i2c: I2C things', '''
420 This has some stuff to do with I2C
421 ''', 'i2c.c', '''And this is the file contents
422 with some I2C-related things in it''')
423 self.make_commit_with_file('spi: SPI fixes', '''
432 This is the cover letter for the series
435 ''' % self.leb, 'spi.c', '''Some fixes for SPI in this
436 file to make SPI work
437 better than before''')
438 first_target = repo.revparse_single('HEAD')
440 target = repo.revparse_single('HEAD~2')
441 # pylint doesn't seem to find this
442 # pylint: disable=E1101
443 repo.reset(target.oid, pygit2.GIT_CHECKOUT_FORCE)
444 self.make_commit_with_file('video: Some video improvements', '''
445 Fix up the video so that
446 it looks more purple. Purple is
448 ''', 'video.c', '''More purple here
451 Could not be any more purple''')
452 self.make_commit_with_file('serial: Add a serial driver', '''
453 Here is the serial driver
458 This series implements support
459 for my glorious board.
462 ''', 'serial.c', '''The code for the
463 serial driver is here''')
464 self.make_commit_with_file('bootm: Make it boot', '''
465 This makes my board boot
466 with a fix to the bootm
468 ''', 'bootm.c', '''Fix up the bootm
469 command to make the code as
470 complicated as possible''')
471 second_target = repo.revparse_single('HEAD')
473 repo.branches.local.create('first', first_target)
474 repo.config.set_multivar('branch.first.remote', '', '.')
475 repo.config.set_multivar('branch.first.merge', '', 'refs/heads/base')
477 repo.branches.local.create('second', second_target)
478 repo.config.set_multivar('branch.second.remote', '', '.')
479 repo.config.set_multivar('branch.second.merge', '', 'refs/heads/base')
481 repo.branches.local.create('base', base_target)
484 def test_branch(self):
485 """Test creating patches from a branch"""
486 repo = self.make_git_tree()
487 target = repo.lookup_reference('refs/heads/first')
488 # pylint doesn't seem to find this
489 # pylint: disable=E1101
490 self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
492 orig_dir = os.getcwd()
494 os.chdir(self.gitdir)
496 # Check that it can detect the current branch
497 self.assertEqual(2, gitutil.count_commits_to_branch(None))
498 col = terminal.Color()
499 with capture_sys_output() as _:
500 _, cover_fname, patch_files = control.prepare_patches(
501 col, branch=None, count=-1, start=0, end=0,
502 ignore_binary=False, signoff=True)
503 self.assertIsNone(cover_fname)
504 self.assertEqual(2, len(patch_files))
506 # Check that it can detect a different branch
507 self.assertEqual(3, gitutil.count_commits_to_branch('second'))
508 with capture_sys_output() as _:
509 _, cover_fname, patch_files = control.prepare_patches(
510 col, branch='second', count=-1, start=0, end=0,
511 ignore_binary=False, signoff=True)
512 self.assertIsNotNone(cover_fname)
513 self.assertEqual(3, len(patch_files))
515 # Check that it can skip patches at the end
516 with capture_sys_output() as _:
517 _, cover_fname, patch_files = control.prepare_patches(
518 col, branch='second', count=-1, start=0, end=1,
519 ignore_binary=False, signoff=True)
520 self.assertIsNotNone(cover_fname)
521 self.assertEqual(2, len(patch_files))
525 def test_custom_get_maintainer_script(self):
526 """Validate that a custom get_maintainer script gets used."""
528 with directory_excursion(self.gitdir):
530 os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null'
531 os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null'
532 tools.run('git', 'config', 'user.name', 'Dummy')
533 tools.run('git', 'config', 'user.email', 'dumdum@dummy.com')
534 tools.run('git', 'branch', 'upstream')
535 tools.run('git', 'branch', '--set-upstream-to=upstream')
536 tools.run('git', 'add', '.')
537 tools.run('git', 'commit', '-m', 'new commit')
539 # Setup patman configuration.
540 with open('.patman', 'w', buffering=1) as f:
541 f.write('[settings]\n'
542 'get_maintainer_script: dummy-script.sh\n'
543 'check_patch: False\n')
544 with open('dummy-script.sh', 'w', buffering=1) as f:
545 f.write('#!/usr/bin/env python\n'
546 'print("hello@there.com")\n')
547 os.chmod('dummy-script.sh', 0x555)
549 # Finally, do the test
550 with capture_sys_output():
551 output = tools.run(PATMAN_DIR / 'patman', '--dry-run')
552 # Assert the email address is part of the dry-run
554 self.assertIn('hello@there.com', output)
557 """Test collection of tags in a patchstream"""
558 text = '''This is a patch
560 Signed-off-by: Terminator
564 ''' % (self.joe, self.mary, self.leb)
565 pstrm = PatchStream.process_text(text)
566 self.assertEqual(pstrm.commit.rtags, {
567 'Reviewed-by': {self.joe, self.mary},
568 'Tested-by': {self.leb}})
570 def test_invalid_tag(self):
571 """Test invalid tag in a patchstream"""
572 text = '''This is a patch
576 with self.assertRaises(ValueError) as exc:
577 pstrm = PatchStream.process_text(text)
578 self.assertEqual("Line 3: Invalid tag = 'Serie-version: 2'",
581 def test_missing_end(self):
582 """Test a missing END tag"""
583 text = '''This is a patch
587 missing END after this line
590 pstrm = PatchStream.process_text(text)
591 self.assertEqual(["Missing 'END' in section 'cover'"],
594 def test_missing_blank_line(self):
595 """Test a missing blank line after a tag"""
596 text = '''This is a patch
599 - First line of changes
600 - Missing blank line after this line
603 pstrm = PatchStream.process_text(text)
604 self.assertEqual(["Missing 'blank line' in section 'Series-changes'"],
607 def test_invalid_commit_tag(self):
608 """Test an invalid Commit-xxx tag"""
609 text = '''This is a patch
613 pstrm = PatchStream.process_text(text)
614 self.assertEqual(["Line 3: Ignoring Commit-fred"], pstrm.commit.warn)
616 def test_self_test(self):
617 """Test a tested by tag by this user"""
618 test_line = 'Tested-by: %s@napier.com' % os.getenv('USER')
619 text = '''This is a patch
623 pstrm = PatchStream.process_text(text)
624 self.assertEqual(["Ignoring '%s'" % test_line], pstrm.commit.warn)
626 def test_space_before_tab(self):
627 """Test a space before a tab"""
628 text = '''This is a patch
632 pstrm = PatchStream.process_text(text)
633 self.assertEqual(["Line 3/0 has space before tab"], pstrm.commit.warn)
635 def test_lines_after_test(self):
636 """Test detecting lines after TEST= line"""
637 text = '''This is a patch
643 pstrm = PatchStream.process_text(text)
644 self.assertEqual(["Found 2 lines after TEST="], pstrm.commit.warn)
646 def test_blank_line_at_end(self):
647 """Test detecting a blank line at the end of a file"""
648 text = '''This is a patch
650 diff --git a/lib/fdtdec.c b/lib/fdtdec.c
651 index c072e54..942244f 100644
654 @@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void)
657 gd->ram_size = (phys_size_t)(res.end - res.start + 1);
658 - debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size);
659 + debug("%s: Initial DRAM size %llx\n", __func__,
660 + (unsigned long long)gd->ram_size);
662 diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
668 pstrm = PatchStream.process_text(text)
670 ["Found possible blank line(s) at end of file 'lib/fdtdec.c'"],
673 def test_no_upstream(self):
674 """Test CountCommitsToBranch when there is no upstream"""
675 repo = self.make_git_tree()
676 target = repo.lookup_reference('refs/heads/base')
677 # pylint doesn't seem to find this
678 # pylint: disable=E1101
679 self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
681 # Check that it can detect the current branch
682 orig_dir = os.getcwd()
684 os.chdir(self.gitdir)
685 with self.assertRaises(ValueError) as exc:
686 gitutil.count_commits_to_branch(None)
688 "Failed to determine upstream: fatal: no upstream configured for branch 'base'",
694 def _fake_patchwork(url, subpath):
695 """Fake Patchwork server for the function below
697 This handles accessing a series, providing a list consisting of a
701 url (str): URL of patchwork server
702 subpath (str): URL subpath to use
704 re_series = re.match(r'series/(\d*)/$', subpath)
706 series_num = re_series.group(1)
707 if series_num == '1234':
709 {'id': '1', 'name': 'Some patch'}]}
710 raise ValueError('Fake Patchwork does not understand: %s' % subpath)
712 def test_status_mismatch(self):
713 """Test Patchwork patches not matching the series"""
716 with capture_sys_output() as (_, err):
717 status.collect_patches(series, 1234, None, self._fake_patchwork)
718 self.assertIn('Warning: Patchwork reports 1 patches, series has 0',
721 def test_status_read_patch(self):
722 """Test handling a single patch in Patchwork"""
724 series.commits = [Commit('abcd')]
726 patches = status.collect_patches(series, 1234, None,
727 self._fake_patchwork)
728 self.assertEqual(1, len(patches))
730 self.assertEqual('1', patch.id)
731 self.assertEqual('Some patch', patch.raw_subject)
733 def test_parse_subject(self):
734 """Test parsing of the patch subject"""
735 patch = status.Patch('1')
737 # Simple patch not in a series
738 patch.parse_subject('Testing')
739 self.assertEqual('Testing', patch.raw_subject)
740 self.assertEqual('Testing', patch.subject)
741 self.assertEqual(1, patch.seq)
742 self.assertEqual(1, patch.count)
743 self.assertEqual(None, patch.prefix)
744 self.assertEqual(None, patch.version)
746 # First patch in a series
747 patch.parse_subject('[1/2] Testing')
748 self.assertEqual('[1/2] Testing', patch.raw_subject)
749 self.assertEqual('Testing', patch.subject)
750 self.assertEqual(1, patch.seq)
751 self.assertEqual(2, patch.count)
752 self.assertEqual(None, patch.prefix)
753 self.assertEqual(None, patch.version)
755 # Second patch in a series
756 patch.parse_subject('[2/2] Testing')
757 self.assertEqual('Testing', patch.subject)
758 self.assertEqual(2, patch.seq)
759 self.assertEqual(2, patch.count)
760 self.assertEqual(None, patch.prefix)
761 self.assertEqual(None, patch.version)
764 patch.parse_subject('[RFC,3/7] Testing')
765 self.assertEqual('Testing', patch.subject)
766 self.assertEqual(3, patch.seq)
767 self.assertEqual(7, patch.count)
768 self.assertEqual('RFC', patch.prefix)
769 self.assertEqual(None, patch.version)
772 patch.parse_subject('[v2,3/7] Testing')
773 self.assertEqual('Testing', patch.subject)
774 self.assertEqual(3, patch.seq)
775 self.assertEqual(7, patch.count)
776 self.assertEqual(None, patch.prefix)
777 self.assertEqual('v2', patch.version)
780 patch.parse_subject('[RESEND,v2,3/7] Testing')
781 self.assertEqual('Testing', patch.subject)
782 self.assertEqual(3, patch.seq)
783 self.assertEqual(7, patch.count)
784 self.assertEqual('RESEND', patch.prefix)
785 self.assertEqual('v2', patch.version)
788 patch.parse_subject('[RESEND] Testing')
789 self.assertEqual('Testing', patch.subject)
790 self.assertEqual(1, patch.seq)
791 self.assertEqual(1, patch.count)
792 self.assertEqual('RESEND', patch.prefix)
793 self.assertEqual(None, patch.version)
795 def test_compare_series(self):
796 """Test operation of compare_with_series()"""
797 commit1 = Commit('abcd')
798 commit1.subject = 'Subject 1'
799 commit2 = Commit('ef12')
800 commit2.subject = 'Subject 2'
801 commit3 = Commit('3456')
802 commit3.subject = 'Subject 2'
804 patch1 = status.Patch('1')
805 patch1.subject = 'Subject 1'
806 patch2 = status.Patch('2')
807 patch2.subject = 'Subject 2'
808 patch3 = status.Patch('3')
809 patch3.subject = 'Subject 2'
812 series.commits = [commit1]
814 patch_for_commit, commit_for_patch, warnings = (
815 status.compare_with_series(series, patches))
816 self.assertEqual(1, len(patch_for_commit))
817 self.assertEqual(patch1, patch_for_commit[0])
818 self.assertEqual(1, len(commit_for_patch))
819 self.assertEqual(commit1, commit_for_patch[0])
821 series.commits = [commit1]
822 patches = [patch1, patch2]
823 patch_for_commit, commit_for_patch, warnings = (
824 status.compare_with_series(series, patches))
825 self.assertEqual(1, len(patch_for_commit))
826 self.assertEqual(patch1, patch_for_commit[0])
827 self.assertEqual(1, len(commit_for_patch))
828 self.assertEqual(commit1, commit_for_patch[0])
829 self.assertEqual(["Cannot find commit for patch 2 ('Subject 2')"],
832 series.commits = [commit1, commit2]
834 patch_for_commit, commit_for_patch, warnings = (
835 status.compare_with_series(series, patches))
836 self.assertEqual(1, len(patch_for_commit))
837 self.assertEqual(patch1, patch_for_commit[0])
838 self.assertEqual(1, len(commit_for_patch))
839 self.assertEqual(commit1, commit_for_patch[0])
840 self.assertEqual(["Cannot find patch for commit 2 ('Subject 2')"],
843 series.commits = [commit1, commit2, commit3]
844 patches = [patch1, patch2]
845 patch_for_commit, commit_for_patch, warnings = (
846 status.compare_with_series(series, patches))
847 self.assertEqual(2, len(patch_for_commit))
848 self.assertEqual(patch1, patch_for_commit[0])
849 self.assertEqual(patch2, patch_for_commit[1])
850 self.assertEqual(1, len(commit_for_patch))
851 self.assertEqual(commit1, commit_for_patch[0])
852 self.assertEqual(["Cannot find patch for commit 3 ('Subject 2')",
853 "Multiple commits match patch 2 ('Subject 2'):\n"
854 ' Subject 2\n Subject 2'],
857 series.commits = [commit1, commit2]
858 patches = [patch1, patch2, patch3]
859 patch_for_commit, commit_for_patch, warnings = (
860 status.compare_with_series(series, patches))
861 self.assertEqual(1, len(patch_for_commit))
862 self.assertEqual(patch1, patch_for_commit[0])
863 self.assertEqual(2, len(commit_for_patch))
864 self.assertEqual(commit1, commit_for_patch[0])
865 self.assertEqual(["Multiple patches match commit 2 ('Subject 2'):\n"
866 ' Subject 2\n Subject 2',
867 "Cannot find commit for patch 3 ('Subject 2')"],
870 def _fake_patchwork2(self, url, subpath):
871 """Fake Patchwork server for the function below
873 This handles accessing series, patches and comments, providing the data
874 in self.patches to the caller
877 url (str): URL of patchwork server
878 subpath (str): URL subpath to use
880 re_series = re.match(r'series/(\d*)/$', subpath)
881 re_patch = re.match(r'patches/(\d*)/$', subpath)
882 re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
884 series_num = re_series.group(1)
885 if series_num == '1234':
886 return {'patches': self.patches}
888 patch_num = int(re_patch.group(1))
889 patch = self.patches[patch_num - 1]
892 patch_num = int(re_comments.group(1))
893 patch = self.patches[patch_num - 1]
894 return patch.comments
895 raise ValueError('Fake Patchwork does not understand: %s' % subpath)
897 def test_find_new_responses(self):
898 """Test operation of find_new_responses()"""
899 commit1 = Commit('abcd')
900 commit1.subject = 'Subject 1'
901 commit2 = Commit('ef12')
902 commit2.subject = 'Subject 2'
904 patch1 = status.Patch('1')
905 patch1.parse_subject('[1/2] Subject 1')
906 patch1.name = patch1.raw_subject
907 patch1.content = 'This is my patch content'
908 comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
910 patch1.comments = [comment1a]
912 patch2 = status.Patch('2')
913 patch2.parse_subject('[2/2] Subject 2')
914 patch2.name = patch2.raw_subject
915 patch2.content = 'Some other patch content'
917 'content': 'Reviewed-by: %s\nTested-by: %s\n' %
918 (self.mary, self.leb)}
919 comment2b = {'content': 'Reviewed-by: %s' % self.fred}
920 patch2.comments = [comment2a, comment2b]
922 # This test works by setting up commits and patch for use by the fake
923 # Rest API function _fake_patchwork2(). It calls various functions in
924 # the status module after setting up tags in the commits, checking that
925 # things behaves as expected
926 self.commits = [commit1, commit2]
927 self.patches = [patch1, patch2]
929 new_rtag_list = [None] * count
930 review_list = [None, None]
932 # Check that the tags are picked up on the first patch
933 status.find_new_responses(new_rtag_list, review_list, 0, commit1,
934 patch1, None, self._fake_patchwork2)
935 self.assertEqual(new_rtag_list[0], {'Reviewed-by': {self.joe}})
937 # Now the second patch
938 status.find_new_responses(new_rtag_list, review_list, 1, commit2,
939 patch2, None, self._fake_patchwork2)
940 self.assertEqual(new_rtag_list[1], {
941 'Reviewed-by': {self.mary, self.fred},
942 'Tested-by': {self.leb}})
944 # Now add some tags to the commit, which means they should not appear as
945 # 'new' tags when scanning comments
946 new_rtag_list = [None] * count
947 commit1.rtags = {'Reviewed-by': {self.joe}}
948 status.find_new_responses(new_rtag_list, review_list, 0, commit1,
949 patch1, None, self._fake_patchwork2)
950 self.assertEqual(new_rtag_list[0], {})
952 # For the second commit, add Ed and Fred, so only Mary should be left
954 'Tested-by': {self.leb},
955 'Reviewed-by': {self.fred}}
956 status.find_new_responses(new_rtag_list, review_list, 1, commit2,
957 patch2, None, self._fake_patchwork2)
958 self.assertEqual(new_rtag_list[1], {'Reviewed-by': {self.mary}})
960 # Check that the output patches expectations:
962 # Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
964 # Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
965 # Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
966 # + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
967 # 1 new response available in patchwork
970 series.commits = [commit1, commit2]
971 terminal.set_print_test_mode()
972 status.check_patchwork_status(series, '1234', None, None, False, False,
973 None, self._fake_patchwork2)
974 lines = iter(terminal.get_print_test_lines())
975 col = terminal.Color()
976 self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.BLUE),
979 terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False,
982 self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
985 self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.BLUE),
988 terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False,
991 self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
994 terminal.PrintLine(' Tested-by: ', col.GREEN, newline=False,
997 self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
1000 terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
1002 self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1004 self.assertEqual(terminal.PrintLine(
1005 '1 new response available in patchwork (use -d to write them to a new branch)',
1008 def _fake_patchwork3(self, url, subpath):
1009 """Fake Patchwork server for the function below
1011 This handles accessing series, patches and comments, providing the data
1012 in self.patches to the caller
1015 url (str): URL of patchwork server
1016 subpath (str): URL subpath to use
1018 re_series = re.match(r'series/(\d*)/$', subpath)
1019 re_patch = re.match(r'patches/(\d*)/$', subpath)
1020 re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
1022 series_num = re_series.group(1)
1023 if series_num == '1234':
1024 return {'patches': self.patches}
1026 patch_num = int(re_patch.group(1))
1027 patch = self.patches[patch_num - 1]
1030 patch_num = int(re_comments.group(1))
1031 patch = self.patches[patch_num - 1]
1032 return patch.comments
1033 raise ValueError('Fake Patchwork does not understand: %s' % subpath)
1035 def test_create_branch(self):
1036 """Test operation of create_branch()"""
1037 repo = self.make_git_tree()
1039 dest_branch = 'first2'
1041 gitdir = os.path.join(self.gitdir, '.git')
1043 # Set up the test git tree. We use branch 'first' which has two commits
1045 series = patchstream.get_metadata_for_list(branch, gitdir, count)
1046 self.assertEqual(2, len(series.commits))
1048 patch1 = status.Patch('1')
1049 patch1.parse_subject('[1/2] %s' % series.commits[0].subject)
1050 patch1.name = patch1.raw_subject
1051 patch1.content = 'This is my patch content'
1052 comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
1054 patch1.comments = [comment1a]
1056 patch2 = status.Patch('2')
1057 patch2.parse_subject('[2/2] %s' % series.commits[1].subject)
1058 patch2.name = patch2.raw_subject
1059 patch2.content = 'Some other patch content'
1061 'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1062 (self.mary, self.leb)}
1064 'content': 'Reviewed-by: %s' % self.fred}
1065 patch2.comments = [comment2a, comment2b]
1067 # This test works by setting up patches for use by the fake Rest API
1068 # function _fake_patchwork3(). The fake patch comments above should
1069 # result in new review tags that are collected and added to the commits
1070 # created in the destination branch.
1071 self.patches = [patch1, patch2]
1076 # + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1078 # + Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1079 # + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1080 # + Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1081 # 4 new responses available in patchwork
1082 # 4 responses added from patchwork into new branch 'first2'
1083 # <unittest.result.TestResult run=8 errors=0 failures=0>
1085 terminal.set_print_test_mode()
1086 status.check_patchwork_status(series, '1234', branch, dest_branch,
1087 False, False, None, self._fake_patchwork3,
1089 lines = terminal.get_print_test_lines()
1090 self.assertEqual(12, len(lines))
1092 "4 responses added from patchwork into new branch 'first2'",
1095 # Check that the destination branch has the new tags
1096 new_series = patchstream.get_metadata_for_list(dest_branch, gitdir,
1099 {'Reviewed-by': {self.joe}},
1100 new_series.commits[0].rtags)
1102 {'Tested-by': {self.leb},
1103 'Reviewed-by': {self.fred, self.mary}},
1104 new_series.commits[1].rtags)
1106 # Now check the actual test of the first commit message. We expect to
1107 # see the new tags immediately below the old ones.
1108 stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir)
1109 lines = iter([line.strip() for line in stdout.splitlines()
1112 # First patch should have the review tag
1113 self.assertEqual('Reviewed-by: %s' % self.joe, next(lines))
1115 # Second patch should have the sign-off then the tested-by and two
1117 self.assertEqual('Signed-off-by: %s' % self.leb, next(lines))
1118 self.assertEqual('Reviewed-by: %s' % self.fred, next(lines))
1119 self.assertEqual('Reviewed-by: %s' % self.mary, next(lines))
1120 self.assertEqual('Tested-by: %s' % self.leb, next(lines))
1122 def test_parse_snippets(self):
1123 """Test parsing of review snippets"""
1126 This is a comment from someone.
1130 On some recent date, Fred wrote:
1131 > This is why I wrote the patch
1134 Now a comment about the commit message
1135 A little more to say
1139 > diff --git a/file.c b/file.c
1152 > @@ -153,8 +143,13 @@ def check_patch(fname, show_types=False):
1153 > further down on the file
1156 > +Another addition here
1160 and another thing in same file
1162 > @@ -253,8 +243,13 @@
1163 > with no function context
1167 > diff --git a/tools/patman/main.py b/tools/patman/main.py
1169 now a very long comment in a different file
1178 pstrm = PatchStream.process_text(text, True)
1179 self.assertEqual([], pstrm.commit.warn)
1181 # We expect to the filename and up to 5 lines of code context before
1182 # each comment. The 'On xxx wrote:' bit should be removed.
1185 'This is a comment from someone.',
1187 ['> This is why I wrote the patch',
1189 'Now a comment about the commit message',
1190 'A little more to say', 'Even more'],
1191 ['> File: file.c', '> Code line 5', '> Code line 6',
1192 '> Code line 7', '> Code line 8', '> Code line 9',
1193 'And another comment'],
1195 '> Line: 153 / 143: def check_patch(fname, show_types=False):',
1196 '> and more code', '> +Addition here', '> +Another addition here',
1197 '> codey', '> more codey', 'and another thing in same file'],
1198 ['> File: file.c', '> Line: 253 / 243',
1199 '> with no function context', 'one more thing'],
1200 ['> File: tools/patman/main.py', '> +line of code',
1201 'now a very long comment in a different file',
1202 'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']],
1205 def test_review_snippets(self):
1206 """Test showing of review snippets"""
1207 def _to_submitter(who):
1208 m_who = re.match('(.*) <(.*)>', who)
1210 'name': m_who.group(1),
1211 'email': m_who.group(2)
1214 commit1 = Commit('abcd')
1215 commit1.subject = 'Subject 1'
1216 commit2 = Commit('ef12')
1217 commit2.subject = 'Subject 2'
1219 patch1 = status.Patch('1')
1220 patch1.parse_subject('[1/2] Subject 1')
1221 patch1.name = patch1.raw_subject
1222 patch1.content = 'This is my patch content'
1223 comment1a = {'submitter': _to_submitter(self.joe),
1224 'content': '''Hi Fred,
1226 On some date Fred wrote:
1228 > diff --git a/file.c b/file.c
1232 Here is my comment above the above...
1238 patch1.comments = [comment1a]
1240 patch2 = status.Patch('2')
1241 patch2.parse_subject('[2/2] Subject 2')
1242 patch2.name = patch2.raw_subject
1243 patch2.content = 'Some other patch content'
1245 'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1246 (self.mary, self.leb)}
1247 comment2b = {'submitter': _to_submitter(self.fred),
1248 'content': '''Hi Fred,
1250 On some date Fred wrote:
1252 > diff --git a/tools/patman/commit.py b/tools/patman/commit.py
1253 > @@ -41,6 +41,9 @@ class Commit:
1254 > self.rtags = collections.defaultdict(set)
1257 > + def __str__(self):
1258 > + return self.subject
1260 > def add_change(self, version, info):
1261 > """Add a new change line to the change list for a version.
1267 patch2.comments = [comment2a, comment2b]
1269 # This test works by setting up commits and patch for use by the fake
1270 # Rest API function _fake_patchwork2(). It calls various functions in
1271 # the status module after setting up tags in the commits, checking that
1272 # things behaves as expected
1273 self.commits = [commit1, commit2]
1274 self.patches = [patch1, patch2]
1276 # Check that the output patches expectations:
1278 # Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1280 # Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1281 # Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1282 # + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1283 # 1 new response available in patchwork
1286 series.commits = [commit1, commit2]
1287 terminal.set_print_test_mode()
1288 status.check_patchwork_status(series, '1234', None, None, False, True,
1289 None, self._fake_patchwork2)
1290 lines = iter(terminal.get_print_test_lines())
1291 col = terminal.Color()
1292 self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.BLUE),
1295 terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
1297 self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
1299 self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1301 self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(lines))
1302 self.assertEqual(terminal.PrintLine('', None), next(lines))
1303 self.assertEqual(terminal.PrintLine(' > File: file.c', col.MAGENTA),
1305 self.assertEqual(terminal.PrintLine(' > Some code', col.MAGENTA),
1307 self.assertEqual(terminal.PrintLine(' > and more code', col.MAGENTA),
1309 self.assertEqual(terminal.PrintLine(
1310 ' Here is my comment above the above...', None), next(lines))
1311 self.assertEqual(terminal.PrintLine('', None), next(lines))
1313 self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.BLUE),
1316 terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
1318 self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1321 terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
1323 self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1326 terminal.PrintLine(' + Tested-by: ', col.GREEN, newline=False),
1328 self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1331 self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1333 self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(lines))
1334 self.assertEqual(terminal.PrintLine('', None), next(lines))
1335 self.assertEqual(terminal.PrintLine(
1336 ' > File: tools/patman/commit.py', col.MAGENTA), next(lines))
1337 self.assertEqual(terminal.PrintLine(
1338 ' > Line: 41 / 41: class Commit:', col.MAGENTA), next(lines))
1339 self.assertEqual(terminal.PrintLine(
1340 ' > + return self.subject', col.MAGENTA), next(lines))
1341 self.assertEqual(terminal.PrintLine(
1342 ' > +', col.MAGENTA), next(lines))
1344 terminal.PrintLine(' > def add_change(self, version, info):',
1347 self.assertEqual(terminal.PrintLine(
1348 ' > """Add a new change line to the change list for a version.',
1349 col.MAGENTA), next(lines))
1350 self.assertEqual(terminal.PrintLine(
1351 ' >', col.MAGENTA), next(lines))
1352 self.assertEqual(terminal.PrintLine(
1353 ' A comment', None), next(lines))
1354 self.assertEqual(terminal.PrintLine('', None), next(lines))
1356 self.assertEqual(terminal.PrintLine(
1357 '4 new responses available in patchwork (use -d to write them to a new branch)',
1360 def test_insert_tags(self):
1361 """Test inserting of review tags"""
1365 'Reviewed-by: Bin Meng <bmeng.cn@gmail.com>',
1366 'Tested-by: Bin Meng <bmeng.cn@gmail.com>'
1368 signoff = 'Signed-off-by: Simon Glass <sjg@chromium.com>'
1369 tag_str = '\n'.join(tags)
1371 new_msg = patchstream.insert_tags(msg, tags)
1372 self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1374 new_msg = patchstream.insert_tags(msg + '\n', tags)
1375 self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1377 msg += '\n\n' + signoff
1378 new_msg = patchstream.insert_tags(msg, tags)
1379 self.assertEqual(msg + '\n' + tag_str, new_msg)