Prepare v2023.10
[platform/kernel/u-boot.git] / tools / patman / func_test.py
1 # -*- coding: utf-8 -*-
2 # SPDX-License-Identifier:      GPL-2.0+
3 #
4 # Copyright 2017 Google, Inc
5 #
6
7 """Functional tests for checking that patman behaves correctly"""
8
9 import contextlib
10 import os
11 import pathlib
12 import re
13 import shutil
14 import sys
15 import tempfile
16 import unittest
17
18
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
29
30 import pygit2
31 from patman import status
32
33 PATMAN_DIR = pathlib.Path(__file__).parent
34 TEST_DATA_DIR = PATMAN_DIR / 'test/'
35
36
37 @contextlib.contextmanager
38 def directory_excursion(directory):
39     """Change directory to `directory` for a limited to the context block."""
40     current = os.getcwd()
41     try:
42         os.chdir(directory)
43         yield
44     finally:
45         os.chdir(current)
46
47
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>'.
51            decode('utf-8'))
52     fred = 'Fred Bloggs <f.bloggs@napier.net>'
53     joe = 'Joe Bloggs <joe@napierwallies.co.nz>'
54     mary = 'Mary Bloggs <mary@napierwallies.co.nz>'
55     commits = None
56     patches = None
57
58     def setUp(self):
59         self.tmpdir = tempfile.mkdtemp(prefix='patman.')
60         self.gitdir = os.path.join(self.tmpdir, 'git')
61         self.repo = None
62
63     def tearDown(self):
64         shutil.rmtree(self.tmpdir)
65         terminal.set_print_test_mode(False)
66
67     @staticmethod
68     def _get_path(fname):
69         """Get the path to a test file
70
71         Args:
72             fname (str): Filename to obtain
73
74         Returns:
75             str: Full path to file in the test directory
76         """
77         return TEST_DATA_DIR / fname
78
79     @classmethod
80     def _get_text(cls, fname):
81         """Read a file as text
82
83         Args:
84             fname (str): Filename to read
85
86         Returns:
87             str: Contents of file
88         """
89         return open(cls._get_path(fname), encoding='utf-8').read()
90
91     @classmethod
92     def _get_patch_name(cls, subject):
93         """Get the filename of a patch given its subject
94
95         Args:
96             subject (str): Patch subject
97
98         Returns:
99             str: Filename for that patch
100         """
101         fname = re.sub('[ :]', '-', subject)
102         return fname.replace('--', '-')
103
104     def _create_patches_for_test(self, series):
105         """Create patch files for use by tests
106
107         This copies patch files from the test directory as needed by the series
108
109         Args:
110             series (Series): Series containing commits to convert
111
112         Returns:
113             tuple:
114                 str: Cover-letter filename, or None if none
115                 fname_list: list of str, each a patch filename
116         """
117         cover_fname = None
118         fname_list = []
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)
130
131         return cover_fname, fname_list
132
133     def test_basic(self):
134         """Tests the basic flow of patman
135
136         This creates a series from some hard-coded patches build from a simple
137         tree with the following metadata in the top commit:
138
139             Series-to: u-boot
140             Series-prefix: RFC
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>
144             Series-version: 3
145             Patch-cc: fred
146             Series-process-log: sort, uniq
147             Series-changes: 4
148             - Some changes
149             - Multi
150               line
151               change
152
153             Commit-changes: 2
154             - Changes only for this commit
155
156 '            Cover-changes: 4
157             - Some notes for the cover letter
158
159             Cover-letter:
160             test: A test patch series
161             This is a test of how the cover
162             letter
163             works
164             END
165
166         and this in the first commit:
167
168             Commit-changes: 2
169             - second revision change
170
171             Series-notes:
172             some notes
173             about some things
174             from the first commit
175             END
176
177             Commit-notes:
178             Some notes about
179             the first commit
180             END
181
182         with the following commands:
183
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
187
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
198             - Commit-notes
199         """
200         process_tags = True
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]
206         dry_run = True
207         in_reply_to = mel
208         count = 2
209         settings.alias = {
210             'fdt': ['simon'],
211             'u-boot': ['u-boot@lists.denx.de'],
212             'simon': [self.leb],
213             'fred': [self.fred],
214         }
215
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)
225             series.DoChecks()
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()
234         os.remove(cc_file)
235
236         lines = iter(out[0].getvalue().splitlines())
237         self.assertEqual('Cleaned %s patches' % len(series.commits),
238                          next(lines))
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':
244             pass
245         self.assertIn('Dry run', next(lines))
246         self.assertEqual('', next(lines))
247         self.assertIn('Send a total of %d patches' % count, next(lines))
248         prev = next(lines)
249         for i, commit in enumerate(series.commits):
250             self.assertEqual('   %s' % args[i], prev)
251             while True:
252                 prev = next(lines)
253                 if 'Cc:' not in prev:
254                     break
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,
263                          next(lines))
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,
270                        ' '.join(args)))
271         self.assertEqual(expected, next(lines))
272
273         self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
274         self.assertEqual(
275             '%s %s\0%s\0%s\0%s' % (args[1], self.fred, self.leb, rick, stefan),
276             cc_lines[1])
277
278         expected = '''
279 This is a test of how the cover
280 letter
281 works
282
283 some notes
284 about some things
285 from the first commit
286
287 Changes in v4:
288 - Multi
289   line
290   change
291 - Some changes
292 - Some notes for the cover letter
293
294 Simon Glass (2):
295   pci: Correct cast for sandbox
296   fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
297
298  cmd/pci.c                   | 3 ++-
299  fs/fat/fat.c                | 1 +
300  lib/efi_loader/efi_memory.c | 1 +
301  lib/fdtdec.c                | 3 ++-
302  4 files changed, 6 insertions(+), 2 deletions(-)
303
304 --\x20
305 2.7.4
306
307 '''
308         lines = open(cover_fname, encoding='utf-8').read().splitlines()
309         self.assertEqual(
310             'Subject: [RFC PATCH some-branch v3 0/2] test: A test patch series',
311             lines[3])
312         self.assertEqual(expected.splitlines(), lines[7:])
313
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),
318                              subject[0][:18])
319
320             # Check that we got our commit notes
321             start = 0
322             expected = ''
323
324             if i == 0:
325                 start = 17
326                 expected = '''---
327 Some notes about
328 the first commit
329
330 (no changes since v2)
331
332 Changes in v2:
333 - second revision change'''
334             elif i == 1:
335                 start = 17
336                 expected = '''---
337
338 Changes in v4:
339 - Multi
340   line
341   change
342 - Some changes
343
344 Changes in v2:
345 - Changes only for this commit'''
346
347             if expected:
348                 expected = expected.splitlines()
349                 self.assertEqual(expected, lines[start:(start+len(expected))])
350
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
353
354         Args:
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
359         """
360         path = os.path.join(self.gitdir, fname)
361         tools.write_file(path, text, binary=False)
362         index = self.repo.index
363         index.add(fname)
364         # pylint doesn't seem to find this
365         # pylint: disable=E1101
366         author = pygit2.Signature('Test user', 'test@email.com')
367         committer = author
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])
372
373     def make_git_tree(self):
374         """Make a simple git tree suitable for testing
375
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
380
381         Returns:
382             pygit2.Repository: repository
383         """
384         repo = pygit2.init_repository(self.gitdir)
385         self.repo = repo
386         new_tree = repo.TreeBuilder().write()
387
388         # pylint doesn't seem to find this
389         # pylint: disable=E1101
390         author = pygit2.Signature('Test user', 'test@email.com')
391         committer = author
392         _ = repo.create_commit('HEAD', author, committer, 'Created master',
393                                new_tree, [])
394
395         self.make_commit_with_file('Initial commit', '''
396 Add a README
397
398 ''', 'README', '''This is the README file
399 describing this project
400 in very little detail''')
401
402         self.make_commit_with_file('pci: PCI implementation', '''
403 Here is a basic PCI implementation
404
405 ''', 'pci.c', '''This is a file
406 it has some contents
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
413 if we want to
414
415 Series-to: u-boot
416 Series-cc: Barry Crump <bcrump@whataroa.nz>
417 ''')
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', '''
424 SPI needs some fixes
425 and here they are
426
427 Signed-off-by: %s
428
429 Series-to: u-boot
430 Commit-notes:
431 title of the series
432 This is the cover letter for the series
433 with various details
434 END
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')
439
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
447 a very nice colour.
448 ''', 'video.c', '''More purple here
449 Purple and purple
450 Even more purple
451 Could not be any more purple''')
452         self.make_commit_with_file('serial: Add a serial driver', '''
453 Here is the serial driver
454 for my chip.
455
456 Cover-letter:
457 Series for my board
458 This series implements support
459 for my glorious board.
460 END
461 Series-links: 183237
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
467 command
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')
472
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')
476
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')
480
481         repo.branches.local.create('base', base_target)
482         return repo
483
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)
491         control.setup()
492         orig_dir = os.getcwd()
493         try:
494             os.chdir(self.gitdir)
495
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))
505
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))
514
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))
522         finally:
523             os.chdir(orig_dir)
524
525     def test_custom_get_maintainer_script(self):
526         """Validate that a custom get_maintainer script gets used."""
527         self.make_git_tree()
528         with directory_excursion(self.gitdir):
529             # Setup git.
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')
538
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)
548
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
553                 # output.
554                 self.assertIn('hello@there.com', output)
555
556     def test_tags(self):
557         """Test collection of tags in a patchstream"""
558         text = '''This is a patch
559
560 Signed-off-by: Terminator
561 Reviewed-by: %s
562 Reviewed-by: %s
563 Tested-by: %s
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}})
569
570     def test_invalid_tag(self):
571         """Test invalid tag in a patchstream"""
572         text = '''This is a patch
573
574 Serie-version: 2
575 '''
576         with self.assertRaises(ValueError) as exc:
577             pstrm = PatchStream.process_text(text)
578         self.assertEqual("Line 3: Invalid tag = 'Serie-version: 2'",
579                          str(exc.exception))
580
581     def test_missing_end(self):
582         """Test a missing END tag"""
583         text = '''This is a patch
584
585 Cover-letter:
586 This is the title
587 missing END after this line
588 Signed-off-by: Fred
589 '''
590         pstrm = PatchStream.process_text(text)
591         self.assertEqual(["Missing 'END' in section 'cover'"],
592                          pstrm.commit.warn)
593
594     def test_missing_blank_line(self):
595         """Test a missing blank line after a tag"""
596         text = '''This is a patch
597
598 Series-changes: 2
599 - First line of changes
600 - Missing blank line after this line
601 Signed-off-by: Fred
602 '''
603         pstrm = PatchStream.process_text(text)
604         self.assertEqual(["Missing 'blank line' in section 'Series-changes'"],
605                          pstrm.commit.warn)
606
607     def test_invalid_commit_tag(self):
608         """Test an invalid Commit-xxx tag"""
609         text = '''This is a patch
610
611 Commit-fred: testing
612 '''
613         pstrm = PatchStream.process_text(text)
614         self.assertEqual(["Line 3: Ignoring Commit-fred"], pstrm.commit.warn)
615
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
620
621 %s
622 ''' % test_line
623         pstrm = PatchStream.process_text(text)
624         self.assertEqual(["Ignoring '%s'" % test_line], pstrm.commit.warn)
625
626     def test_space_before_tab(self):
627         """Test a space before a tab"""
628         text = '''This is a patch
629
630 + \tSomething
631 '''
632         pstrm = PatchStream.process_text(text)
633         self.assertEqual(["Line 3/0 has space before tab"], pstrm.commit.warn)
634
635     def test_lines_after_test(self):
636         """Test detecting lines after TEST= line"""
637         text = '''This is a patch
638
639 TEST=sometest
640 more lines
641 here
642 '''
643         pstrm = PatchStream.process_text(text)
644         self.assertEqual(["Found 2 lines after TEST="], pstrm.commit.warn)
645
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
649
650 diff --git a/lib/fdtdec.c b/lib/fdtdec.c
651 index c072e54..942244f 100644
652 --- a/lib/fdtdec.c
653 +++ b/lib/fdtdec.c
654 @@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void)
655         }
656
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);
661 +
662 diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
663
664 --
665 2.7.4
666
667  '''
668         pstrm = PatchStream.process_text(text)
669         self.assertEqual(
670             ["Found possible blank line(s) at end of file 'lib/fdtdec.c'"],
671             pstrm.commit.warn)
672
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)
680
681         # Check that it can detect the current branch
682         orig_dir = os.getcwd()
683         try:
684             os.chdir(self.gitdir)
685             with self.assertRaises(ValueError) as exc:
686                 gitutil.count_commits_to_branch(None)
687             self.assertIn(
688                 "Failed to determine upstream: fatal: no upstream configured for branch 'base'",
689                 str(exc.exception))
690         finally:
691             os.chdir(orig_dir)
692
693     @staticmethod
694     def _fake_patchwork(url, subpath):
695         """Fake Patchwork server for the function below
696
697         This handles accessing a series, providing a list consisting of a
698         single patch
699
700         Args:
701             url (str): URL of patchwork server
702             subpath (str): URL subpath to use
703         """
704         re_series = re.match(r'series/(\d*)/$', subpath)
705         if re_series:
706             series_num = re_series.group(1)
707             if series_num == '1234':
708                 return {'patches': [
709                     {'id': '1', 'name': 'Some patch'}]}
710         raise ValueError('Fake Patchwork does not understand: %s' % subpath)
711
712     def test_status_mismatch(self):
713         """Test Patchwork patches not matching the series"""
714         series = Series()
715
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',
719                       err.getvalue())
720
721     def test_status_read_patch(self):
722         """Test handling a single patch in Patchwork"""
723         series = Series()
724         series.commits = [Commit('abcd')]
725
726         patches = status.collect_patches(series, 1234, None,
727                                          self._fake_patchwork)
728         self.assertEqual(1, len(patches))
729         patch = patches[0]
730         self.assertEqual('1', patch.id)
731         self.assertEqual('Some patch', patch.raw_subject)
732
733     def test_parse_subject(self):
734         """Test parsing of the patch subject"""
735         patch = status.Patch('1')
736
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)
745
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)
754
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)
762
763         # RFC patch
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)
770
771         # Version patch
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)
778
779         # All fields
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)
786
787         # RFC only
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)
794
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'
803
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'
810
811         series = Series()
812         series.commits = [commit1]
813         patches = [patch1]
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])
820
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')"],
830                          warnings)
831
832         series.commits = [commit1, commit2]
833         patches = [patch1]
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')"],
841                          warnings)
842
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'],
855                          warnings)
856
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')"],
868                          warnings)
869
870     def _fake_patchwork2(self, url, subpath):
871         """Fake Patchwork server for the function below
872
873         This handles accessing series, patches and comments, providing the data
874         in self.patches to the caller
875
876         Args:
877             url (str): URL of patchwork server
878             subpath (str): URL subpath to use
879         """
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)
883         if re_series:
884             series_num = re_series.group(1)
885             if series_num == '1234':
886                 return {'patches': self.patches}
887         elif re_patch:
888             patch_num = int(re_patch.group(1))
889             patch = self.patches[patch_num - 1]
890             return patch
891         elif re_comments:
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)
896
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'
903
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}
909
910         patch1.comments = [comment1a]
911
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'
916         comment2a = {
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]
921
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]
928         count = 2
929         new_rtag_list = [None] * count
930         review_list = [None, None]
931
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}})
936
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}})
943
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], {})
951
952         # For the second commit, add Ed and Fred, so only Mary should be left
953         commit2.rtags = {
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}})
959
960         # Check that the output patches expectations:
961         #   1 Subject 1
962         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
963         #   2 Subject 2
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
968
969         series = Series()
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),
977                          next(lines))
978         self.assertEqual(
979             terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
980                                bright=False),
981             next(lines))
982         self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
983                          next(lines))
984
985         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
986                          next(lines))
987         self.assertEqual(
988             terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
989                                bright=False),
990             next(lines))
991         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
992                          next(lines))
993         self.assertEqual(
994             terminal.PrintLine('    Tested-by: ', col.GREEN, newline=False,
995                                bright=False),
996             next(lines))
997         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
998                          next(lines))
999         self.assertEqual(
1000             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1001             next(lines))
1002         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1003                          next(lines))
1004         self.assertEqual(terminal.PrintLine(
1005             '1 new response available in patchwork (use -d to write them to a new branch)',
1006             None), next(lines))
1007
1008     def _fake_patchwork3(self, url, subpath):
1009         """Fake Patchwork server for the function below
1010
1011         This handles accessing series, patches and comments, providing the data
1012         in self.patches to the caller
1013
1014         Args:
1015             url (str): URL of patchwork server
1016             subpath (str): URL subpath to use
1017         """
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)
1021         if re_series:
1022             series_num = re_series.group(1)
1023             if series_num == '1234':
1024                 return {'patches': self.patches}
1025         elif re_patch:
1026             patch_num = int(re_patch.group(1))
1027             patch = self.patches[patch_num - 1]
1028             return patch
1029         elif re_comments:
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)
1034
1035     def test_create_branch(self):
1036         """Test operation of create_branch()"""
1037         repo = self.make_git_tree()
1038         branch = 'first'
1039         dest_branch = 'first2'
1040         count = 2
1041         gitdir = os.path.join(self.gitdir, '.git')
1042
1043         # Set up the test git tree. We use branch 'first' which has two commits
1044         # in it
1045         series = patchstream.get_metadata_for_list(branch, gitdir, count)
1046         self.assertEqual(2, len(series.commits))
1047
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}
1053
1054         patch1.comments = [comment1a]
1055
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'
1060         comment2a = {
1061             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1062                        (self.mary, self.leb)}
1063         comment2b = {
1064             'content': 'Reviewed-by: %s' % self.fred}
1065         patch2.comments = [comment2a, comment2b]
1066
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]
1072         count = 2
1073
1074         # Expected output:
1075         #   1 i2c: I2C things
1076         #   + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1077         #   2 spi: SPI fixes
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>
1084
1085         terminal.set_print_test_mode()
1086         status.check_patchwork_status(series, '1234', branch, dest_branch,
1087                                       False, False, None, self._fake_patchwork3,
1088                                       repo)
1089         lines = terminal.get_print_test_lines()
1090         self.assertEqual(12, len(lines))
1091         self.assertEqual(
1092             "4 responses added from patchwork into new branch 'first2'",
1093             lines[11].text)
1094
1095         # Check that the destination branch has the new tags
1096         new_series = patchstream.get_metadata_for_list(dest_branch, gitdir,
1097                                                        count)
1098         self.assertEqual(
1099             {'Reviewed-by': {self.joe}},
1100             new_series.commits[0].rtags)
1101         self.assertEqual(
1102             {'Tested-by': {self.leb},
1103              'Reviewed-by': {self.fred, self.mary}},
1104             new_series.commits[1].rtags)
1105
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()
1110                       if '-by:' in line])
1111
1112         # First patch should have the review tag
1113         self.assertEqual('Reviewed-by: %s' % self.joe, next(lines))
1114
1115         # Second patch should have the sign-off then the tested-by and two
1116         # reviewed-by tags
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))
1121
1122     def test_parse_snippets(self):
1123         """Test parsing of review snippets"""
1124         text = '''Hi Fred,
1125
1126 This is a comment from someone.
1127
1128 Something else
1129
1130 On some recent date, Fred wrote:
1131 > This is why I wrote the patch
1132 > so here it is
1133
1134 Now a comment about the commit message
1135 A little more to say
1136
1137 Even more
1138
1139 > diff --git a/file.c b/file.c
1140 > Some more code
1141 > Code line 2
1142 > Code line 3
1143 > Code line 4
1144 > Code line 5
1145 > Code line 6
1146 > Code line 7
1147 > Code line 8
1148 > Code line 9
1149
1150 And another comment
1151
1152 > @@ -153,8 +143,13 @@ def check_patch(fname, show_types=False):
1153 >  further down on the file
1154 >  and more code
1155 > +Addition here
1156 > +Another addition here
1157 >  codey
1158 >  more codey
1159
1160 and another thing in same file
1161
1162 > @@ -253,8 +243,13 @@
1163 >  with no function context
1164
1165 one more thing
1166
1167 > diff --git a/tools/patman/main.py b/tools/patman/main.py
1168 > +line of code
1169 now a very long comment in a different file
1170 line2
1171 line3
1172 line4
1173 line5
1174 line6
1175 line7
1176 line8
1177 '''
1178         pstrm = PatchStream.process_text(text, True)
1179         self.assertEqual([], pstrm.commit.warn)
1180
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.
1183         self.assertEqual(
1184             [['Hi Fred,',
1185               'This is a comment from someone.',
1186               'Something else'],
1187              ['> This is why I wrote the patch',
1188               '> so here it is',
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'],
1194              ['> File: file.c',
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']],
1203             pstrm.snippets)
1204
1205     def test_review_snippets(self):
1206         """Test showing of review snippets"""
1207         def _to_submitter(who):
1208             m_who = re.match('(.*) <(.*)>', who)
1209             return {
1210                 'name': m_who.group(1),
1211                 'email': m_who.group(2)
1212                 }
1213
1214         commit1 = Commit('abcd')
1215         commit1.subject = 'Subject 1'
1216         commit2 = Commit('ef12')
1217         commit2.subject = 'Subject 2'
1218
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,
1225
1226 On some date Fred wrote:
1227
1228 > diff --git a/file.c b/file.c
1229 > Some code
1230 > and more code
1231
1232 Here is my comment above the above...
1233
1234
1235 Reviewed-by: %s
1236 ''' % self.joe}
1237
1238         patch1.comments = [comment1a]
1239
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'
1244         comment2a = {
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,
1249
1250 On some date Fred wrote:
1251
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)
1255 >          self.warn = []
1256 >
1257 > +    def __str__(self):
1258 > +        return self.subject
1259 > +
1260 >      def add_change(self, version, info):
1261 >          """Add a new change line to the change list for a version.
1262 >
1263 A comment
1264
1265 Reviewed-by: %s
1266 ''' % self.fred}
1267         patch2.comments = [comment2a, comment2b]
1268
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]
1275
1276         # Check that the output patches expectations:
1277         #   1 Subject 1
1278         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1279         #   2 Subject 2
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
1284
1285         series = Series()
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),
1293                          next(lines))
1294         self.assertEqual(
1295             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1296             next(lines))
1297         self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
1298
1299         self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1300                          next(lines))
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),
1304                          next(lines))
1305         self.assertEqual(terminal.PrintLine('    > Some code', col.MAGENTA),
1306                          next(lines))
1307         self.assertEqual(terminal.PrintLine('    > and more code', col.MAGENTA),
1308                          next(lines))
1309         self.assertEqual(terminal.PrintLine(
1310             '    Here is my comment above the above...', None), next(lines))
1311         self.assertEqual(terminal.PrintLine('', None), next(lines))
1312
1313         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
1314                          next(lines))
1315         self.assertEqual(
1316             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1317             next(lines))
1318         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1319                          next(lines))
1320         self.assertEqual(
1321             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1322             next(lines))
1323         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1324                          next(lines))
1325         self.assertEqual(
1326             terminal.PrintLine('  + Tested-by: ', col.GREEN, newline=False),
1327             next(lines))
1328         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1329                          next(lines))
1330
1331         self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1332                          next(lines))
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))
1343         self.assertEqual(
1344             terminal.PrintLine('    >      def add_change(self, version, info):',
1345                                col.MAGENTA),
1346             next(lines))
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))
1355
1356         self.assertEqual(terminal.PrintLine(
1357             '4 new responses available in patchwork (use -d to write them to a new branch)',
1358             None), next(lines))
1359
1360     def test_insert_tags(self):
1361         """Test inserting of review tags"""
1362         msg = '''first line
1363 second line.'''
1364         tags = [
1365             'Reviewed-by: Bin Meng <bmeng.cn@gmail.com>',
1366             'Tested-by: Bin Meng <bmeng.cn@gmail.com>'
1367             ]
1368         signoff = 'Signed-off-by: Simon Glass <sjg@chromium.com>'
1369         tag_str = '\n'.join(tags)
1370
1371         new_msg = patchstream.insert_tags(msg, tags)
1372         self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1373
1374         new_msg = patchstream.insert_tags(msg + '\n', tags)
1375         self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1376
1377         msg += '\n\n' + signoff
1378         new_msg = patchstream.insert_tags(msg, tags)
1379         self.assertEqual(msg + '\n' + tag_str, new_msg)