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
9import asyncio
10import contextlib
11import os
12import pathlib
13import re
14import shutil
15import sys
16import unittest
17
18import pygit2
19
20from u_boot_pylib import command
21from u_boot_pylib import gitutil
22from u_boot_pylib import terminal
23from u_boot_pylib import tools
24
25from patman.commit import Commit
26from patman import control
27from patman import patchstream
28from patman.patchstream import PatchStream
29from patman import patchwork
30from patman import send
31from patman.series import Series
32from patman import status
33from patman.test_common import TestCommon
34
35PATMAN_DIR = pathlib.Path(__file__).parent
36TEST_DATA_DIR = PATMAN_DIR / 'test/'
37
38
39@contextlib.contextmanager
40def directory_excursion(directory):
41    """Change directory to `directory` for a limited to the context block."""
42    current = os.getcwd()
43    try:
44        os.chdir(directory)
45        yield
46    finally:
47        os.chdir(current)
48
49
50class TestFunctional(unittest.TestCase, TestCommon):
51    """Functional tests for checking that patman behaves correctly"""
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        TestCommon.setUp(self)
60        self.repo = None
61        self._patman_pathname = sys.argv[0]
62        self._patman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
63
64    def tearDown(self):
65        TestCommon.tearDown(self)
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>'
203                  .decode('utf-8'))
204        rick = 'Richard III <richard@palace.gov>'
205        mel = b'Lord M\xc3\xablchett <clergy@palace.gov>'.decode('utf-8')
206        add_maintainers = [stefan, rick]
207        dry_run = True
208        in_reply_to = mel
209        count = 2
210        alias = {
211            'fdt': ['simon'],
212            'u-boot': ['u-boot@lists.denx.de'],
213            'simon': [self.leb],
214            'fred': [self.fred],
215            'joe': [self.joe],
216        }
217
218        text = self._get_text('test01.txt')
219        series = patchstream.get_metadata_for_test(text)
220        series.base_commit = Commit('1a44532')
221        series.branch = 'mybranch'
222        cover_fname, args = self._create_patches_for_test(series)
223        get_maintainer_script = str(pathlib.Path(__file__).parent.parent.parent
224                                    / 'get_maintainer.pl') + ' --norolestats'
225        with terminal.capture() as out:
226            patchstream.fix_patches(series, args)
227            if cover_fname and series.get('cover'):
228                patchstream.insert_cover_letter(cover_fname, series, count)
229            series.DoChecks()
230            cc_file = series.MakeCcFile(process_tags, cover_fname,
231                                        not ignore_bad_tags, add_maintainers,
232                                        None, get_maintainer_script, alias)
233            cmd = gitutil.email_patches(
234                series, cover_fname, args, dry_run, not ignore_bad_tags,
235                cc_file, alias, in_reply_to=in_reply_to, thread=None)
236            series.ShowActions(args, cmd, process_tags, alias)
237        cc_lines = tools.read_file(cc_file, binary=False).splitlines()
238        os.remove(cc_file)
239
240        itr = iter(out[0].getvalue().splitlines())
241        self.assertEqual('Cleaned %s patches' % len(series.commits),
242                         next(itr))
243        self.assertEqual('Change log missing for v2', next(itr))
244        self.assertEqual('Change log missing for v3', next(itr))
245        self.assertEqual('Change log for unknown version v4', next(itr))
246        self.assertEqual("Alias 'pci' not found", next(itr))
247        while next(itr) != 'Cc processing complete':
248            pass
249        self.assertIn('Dry run', next(itr))
250        self.assertEqual('', next(itr))
251        self.assertIn('Send a total of %d patches' % count, next(itr))
252        prev = next(itr)
253        for i in range(len(series.commits)):
254            self.assertEqual('   %s' % args[i], prev)
255            while True:
256                prev = next(itr)
257                if 'Cc:' not in prev:
258                    break
259        self.assertEqual('To:	  u-boot@lists.denx.de', prev)
260        self.assertEqual('Cc:	  %s' % stefan, next(itr))
261        self.assertEqual('Version:  3', next(itr))
262        self.assertEqual('Prefix:\t  RFC', next(itr))
263        self.assertEqual('Postfix:\t  some-branch', next(itr))
264        self.assertEqual('Cover: 4 lines', next(itr))
265        self.assertEqual('      Cc:  %s' % self.fred, next(itr))
266        self.assertEqual('      Cc:  %s' % self.joe, next(itr))
267        self.assertEqual('      Cc:  %s' % self.leb,
268                         next(itr))
269        self.assertEqual('      Cc:  %s' % mel, next(itr))
270        self.assertEqual('      Cc:  %s' % rick, next(itr))
271        expected = ('Git command: git send-email --annotate '
272                    '--in-reply-to="%s" --to u-boot@lists.denx.de '
273                    '--cc "%s" --cc-cmd "%s send --cc-cmd %s" %s %s'
274                    % (in_reply_to, stefan, sys.argv[0], cc_file, cover_fname,
275                       ' '.join(args)))
276        self.assertEqual(expected, next(itr))
277
278        self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
279        self.assertEqual(
280            '%s %s\0%s\0%s\0%s\0%s' % (args[1], self.fred, self.joe, self.leb,
281                                       rick, stefan),
282            cc_lines[1])
283
284        expected = '''
285This is a test of how the cover
286letter
287works
288
289some notes
290about some things
291from the first commit
292
293Changes in v4:
294- Multi
295  line
296  change
297- Some changes
298- Some notes for the cover letter
299- fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
300
301Simon Glass (2):
302  pci: Correct cast for sandbox
303  fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
304
305 cmd/pci.c                   | 3 ++-
306 fs/fat/fat.c                | 1 +
307 lib/efi_loader/efi_memory.c | 1 +
308 lib/fdtdec.c                | 3 ++-
309 4 files changed, 6 insertions(+), 2 deletions(-)
310
311--\x20
3122.7.4
313
314base-commit: 1a44532
315branch: mybranch
316'''
317        lines = tools.read_file(cover_fname, binary=False).splitlines()
318        self.assertEqual(
319            'Subject: [RFC PATCH some-branch v3 0/2] test: A test patch series',
320            lines[3])
321        self.assertEqual(expected.splitlines(), lines[7:])
322
323        for i, fname in enumerate(args):
324            lines = tools.read_file(fname, binary=False).splitlines()
325            subject = [line for line in lines if line.startswith('Subject')]
326            self.assertEqual('Subject: [RFC %d/%d]' % (i + 1, count),
327                             subject[0][:18])
328
329            # Check that we got our commit notes
330            start = 0
331            expected = ''
332
333            if i == 0:
334                start = 17
335                expected = '''---
336Some notes about
337the first commit
338
339(no changes since v2)
340
341Changes in v2:
342- second revision change'''
343            elif i == 1:
344                start = 17
345                expected = '''---
346
347Changes in v4:
348- Multi
349  line
350  change
351- New
352- Some changes
353
354Changes in v2:
355- Changes only for this commit'''
356
357            if expected:
358                expected = expected.splitlines()
359                self.assertEqual(expected, lines[start:(start+len(expected))])
360
361    def test_base_commit(self):
362        """Test adding a base commit with no cover letter"""
363        orig_text = self._get_text('test01.txt')
364        pos = orig_text.index(
365            'commit 5ab48490f03051875ab13d288a4bf32b507d76fd')
366        text = orig_text[:pos]
367        series = patchstream.get_metadata_for_test(text)
368        series.base_commit = Commit('1a44532')
369        series.branch = 'mybranch'
370        cover_fname, args = self._create_patches_for_test(series)
371        self.assertFalse(cover_fname)
372        with terminal.capture() as out:
373            patchstream.fix_patches(series, args, insert_base_commit=True)
374        self.assertEqual('Cleaned 1 patch\n', out[0].getvalue())
375        lines = tools.read_file(args[0], binary=False).splitlines()
376        pos = lines.index('-- ')
377
378        # We expect these lines at the end:
379        # -- (with trailing space)
380        # 2.7.4
381        # (empty)
382        # base-commit: xxx
383        # branch: xxx
384        self.assertEqual('base-commit: 1a44532', lines[pos + 3])
385        self.assertEqual('branch: mybranch', lines[pos + 4])
386
387    def test_branch(self):
388        """Test creating patches from a branch"""
389        repo = self.make_git_tree()
390        target = repo.lookup_reference('refs/heads/first')
391        # pylint doesn't seem to find this
392        # pylint: disable=E1101
393        self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
394        control.setup()
395        orig_dir = os.getcwd()
396        try:
397            os.chdir(self.tmpdir)
398
399            # Check that it can detect the current branch
400            self.assertEqual(2, gitutil.count_commits_to_branch(None))
401            col = terminal.Color()
402            with terminal.capture() as _:
403                _, cover_fname, patch_files = send.prepare_patches(
404                    col, branch=None, count=-1, start=0, end=0,
405                    ignore_binary=False, signoff=True)
406            self.assertIsNone(cover_fname)
407            self.assertEqual(2, len(patch_files))
408
409            # Check that it can detect a different branch
410            self.assertEqual(3, gitutil.count_commits_to_branch('second'))
411            with terminal.capture() as _:
412                _, cover_fname, patch_files = send.prepare_patches(
413                    col, branch='second', count=-1, start=0, end=0,
414                    ignore_binary=False, signoff=True)
415            self.assertIsNotNone(cover_fname)
416            self.assertEqual(3, len(patch_files))
417
418            cover = tools.read_file(cover_fname, binary=False)
419            lines = cover.splitlines()[-2:]
420            base = repo.lookup_reference('refs/heads/base').target
421            self.assertEqual(f'base-commit: {base}', lines[0])
422            self.assertEqual('branch: second', lines[1])
423
424            # Make sure that the base-commit is not present when it is in the
425            # cover letter
426            for fname in patch_files:
427                self.assertNotIn(b'base-commit:', tools.read_file(fname))
428
429            # Check that it can skip patches at the end
430            with terminal.capture() as _:
431                _, cover_fname, patch_files = send.prepare_patches(
432                    col, branch='second', count=-1, start=0, end=1,
433                    ignore_binary=False, signoff=True)
434            self.assertIsNotNone(cover_fname)
435            self.assertEqual(2, len(patch_files))
436
437            cover = tools.read_file(cover_fname, binary=False)
438            lines = cover.splitlines()[-2:]
439            base2 = repo.lookup_reference('refs/heads/second')
440            ref = base2.peel(pygit2.GIT_OBJ_COMMIT).parents[0].parents[0].id
441            self.assertEqual(f'base-commit: {ref}', lines[0])
442            self.assertEqual('branch: second', lines[1])
443        finally:
444            os.chdir(orig_dir)
445
446    def test_custom_get_maintainer_script(self):
447        """Validate that a custom get_maintainer script gets used."""
448        self.make_git_tree()
449        with directory_excursion(self.tmpdir):
450            # Setup git.
451            os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null'
452            os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null'
453            tools.run('git', 'config', 'user.name', 'Dummy')
454            tools.run('git', 'config', 'user.email', 'dumdum@dummy.com')
455            tools.run('git', 'branch', 'upstream')
456            tools.run('git', 'branch', '--set-upstream-to=upstream')
457
458            # Setup patman configuration.
459            tools.write_file('.patman', '[settings]\n'
460                             'get_maintainer_script: dummy-script.sh\n'
461                             'check_patch: False\n'
462                             'add_maintainers: True\n', binary=False)
463            tools.write_file('dummy-script.sh',
464                             '#!/usr/bin/env python3\n'
465                             'print("hello@there.com")\n', binary=False)
466            os.chmod('dummy-script.sh', 0x555)
467            tools.run('git', 'add', '.')
468            tools.run('git', 'commit', '-m', 'new commit')
469
470            # Finally, do the test
471            with terminal.capture():
472                output = tools.run(PATMAN_DIR / 'patman', '--dry-run')
473                # Assert the email address is part of the dry-run
474                # output.
475                self.assertIn('hello@there.com', output)
476
477    def test_tags(self):
478        """Test collection of tags in a patchstream"""
479        text = '''This is a patch
480
481Signed-off-by: Terminator
482Reviewed-by: %s
483Reviewed-by: %s
484Tested-by: %s
485''' % (self.joe, self.mary, self.leb)
486        pstrm = PatchStream.process_text(text)
487        self.assertEqual(pstrm.commit.rtags, {
488            'Reviewed-by': {self.joe, self.mary},
489            'Tested-by': {self.leb}})
490
491    def test_invalid_tag(self):
492        """Test invalid tag in a patchstream"""
493        text = '''This is a patch
494
495Serie-version: 2
496'''
497        with self.assertRaises(ValueError) as exc:
498            PatchStream.process_text(text)
499        self.assertEqual("Line 3: Invalid tag = 'Serie-version: 2'",
500                         str(exc.exception))
501
502    def test_missing_end(self):
503        """Test a missing END tag"""
504        text = '''This is a patch
505
506Cover-letter:
507This is the title
508missing END after this line
509Signed-off-by: Fred
510'''
511        pstrm = PatchStream.process_text(text)
512        self.assertEqual(["Missing 'END' in section 'cover'"],
513                         pstrm.commit.warn)
514
515    def test_missing_blank_line(self):
516        """Test a missing blank line after a tag"""
517        text = '''This is a patch
518
519Series-changes: 2
520- First line of changes
521- Missing blank line after this line
522Signed-off-by: Fred
523'''
524        pstrm = PatchStream.process_text(text)
525        self.assertEqual(["Missing 'blank line' in section 'Series-changes'"],
526                         pstrm.commit.warn)
527
528    def test_invalid_commit_tag(self):
529        """Test an invalid Commit-xxx tag"""
530        text = '''This is a patch
531
532Commit-fred: testing
533'''
534        pstrm = PatchStream.process_text(text)
535        self.assertEqual(["Line 3: Ignoring Commit-fred"], pstrm.commit.warn)
536
537    def test_self_test(self):
538        """Test a tested by tag by this user"""
539        test_line = 'Tested-by: %s@napier.com' % os.getenv('USER')
540        text = '''This is a patch
541
542%s
543''' % test_line
544        pstrm = PatchStream.process_text(text)
545        self.assertEqual(["Ignoring '%s'" % test_line], pstrm.commit.warn)
546
547    def test_space_before_tab(self):
548        """Test a space before a tab"""
549        text = '''This is a patch
550
551+ \tSomething
552'''
553        pstrm = PatchStream.process_text(text)
554        self.assertEqual(["Line 3/0 has space before tab"], pstrm.commit.warn)
555
556    def test_lines_after_test(self):
557        """Test detecting lines after TEST= line"""
558        text = '''This is a patch
559
560TEST=sometest
561more lines
562here
563'''
564        pstrm = PatchStream.process_text(text)
565        self.assertEqual(["Found 2 lines after TEST="], pstrm.commit.warn)
566
567    def test_blank_line_at_end(self):
568        """Test detecting a blank line at the end of a file"""
569        text = '''This is a patch
570
571diff --git a/lib/fdtdec.c b/lib/fdtdec.c
572index c072e54..942244f 100644
573--- a/lib/fdtdec.c
574+++ b/lib/fdtdec.c
575@@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void)
576 \t}
577
578 \tgd->ram_size = (phys_size_t)(res.end - res.start + 1);
579-	debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size);
580+	debug("%s: Initial DRAM size %llx\n", __func__,
581+	      (unsigned long long)gd->ram_size);
582+
583diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
584
585--
5862.7.4
587
588 '''
589        pstrm = PatchStream.process_text(text)
590        self.assertEqual(
591            ["Found possible blank line(s) at end of file 'lib/fdtdec.c'"],
592            pstrm.commit.warn)
593
594    def test_no_upstream(self):
595        """Test CountCommitsToBranch when there is no upstream"""
596        repo = self.make_git_tree()
597        target = repo.lookup_reference('refs/heads/base')
598        # pylint doesn't seem to find this
599        # pylint: disable=E1101
600        self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
601
602        # Check that it can detect the current branch
603        orig_dir = os.getcwd()
604        try:
605            os.chdir(self.gitdir)
606            with self.assertRaises(ValueError) as exc:
607                gitutil.count_commits_to_branch(None)
608            self.assertIn(
609                "Failed to determine upstream: fatal: no upstream configured for branch 'base'",
610                str(exc.exception))
611        finally:
612            os.chdir(orig_dir)
613
614    def run_patman(self, *args):
615        """Run patman using the provided arguments
616
617        This runs the patman executable from scratch, as opposed to calling
618        the control.do_patman() function.
619
620        Args:
621            args (list of str): Arguments to pass (excluding argv[0])
622
623        Return:
624            CommandResult: Result of execution
625        """
626        all_args = [self._patman_pathname] + list(args)
627        return command.run_one(*all_args, capture=True, capture_stderr=True)
628
629    def test_full_help(self):
630        """Test getting full help"""
631        command.TEST_RESULT = None
632        result = self.run_patman('-H')
633        help_file = os.path.join(self._patman_dir, 'README.rst')
634        # Remove possible extraneous strings
635        extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n'
636        gothelp = result.stdout.replace(extra, '')
637        self.assertEqual(len(gothelp), os.path.getsize(help_file))
638        self.assertEqual(0, len(result.stderr))
639        self.assertEqual(0, result.return_code)
640
641    def test_help(self):
642        """Test getting help with commands and arguments"""
643        command.TEST_RESULT = None
644        result = self.run_patman('-h')
645        self.assertTrue(len(result.stdout) > 1000)
646        self.assertEqual(0, len(result.stderr))
647        self.assertEqual(0, result.return_code)
648
649    @staticmethod
650    def _fake_patchwork(subpath):
651        """Fake Patchwork server for the function below
652
653        This handles accessing a series, providing a list consisting of a
654        single patch
655
656        Args:
657            subpath (str): URL subpath to use
658        """
659        re_series = re.match(r'series/(\d*)/$', subpath)
660        if re_series:
661            series_num = re_series.group(1)
662            if series_num == '1234':
663                return {'patches': [
664                    {'id': '1', 'name': 'Some patch'}]}
665        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
666
667    def test_status_mismatch(self):
668        """Test Patchwork patches not matching the series"""
669        pwork = patchwork.Patchwork.for_testing(self._fake_patchwork)
670        with terminal.capture() as (_, err):
671            loop = asyncio.get_event_loop()
672            _, patches = loop.run_until_complete(status.check_status(1234,
673                                                                     pwork))
674            status.check_patch_count(0, len(patches))
675        self.assertIn('Warning: Patchwork reports 1 patches, series has 0',
676                      err.getvalue())
677
678    def test_status_read_patch(self):
679        """Test handling a single patch in Patchwork"""
680        pwork = patchwork.Patchwork.for_testing(self._fake_patchwork)
681        loop = asyncio.get_event_loop()
682        _, patches = loop.run_until_complete(status.check_status(1234, pwork))
683        self.assertEqual(1, len(patches))
684        patch = patches[0]
685        self.assertEqual('1', patch.id)
686        self.assertEqual('Some patch', patch.raw_subject)
687
688    def test_parse_subject(self):
689        """Test parsing of the patch subject"""
690        patch = patchwork.Patch('1')
691
692        # Simple patch not in a series
693        patch.parse_subject('Testing')
694        self.assertEqual('Testing', patch.raw_subject)
695        self.assertEqual('Testing', patch.subject)
696        self.assertEqual(1, patch.seq)
697        self.assertEqual(1, patch.count)
698        self.assertEqual(None, patch.prefix)
699        self.assertEqual(None, patch.version)
700
701        # First patch in a series
702        patch.parse_subject('[1/2] Testing')
703        self.assertEqual('[1/2] Testing', patch.raw_subject)
704        self.assertEqual('Testing', patch.subject)
705        self.assertEqual(1, patch.seq)
706        self.assertEqual(2, patch.count)
707        self.assertEqual(None, patch.prefix)
708        self.assertEqual(None, patch.version)
709
710        # Second patch in a series
711        patch.parse_subject('[2/2] Testing')
712        self.assertEqual('Testing', patch.subject)
713        self.assertEqual(2, patch.seq)
714        self.assertEqual(2, patch.count)
715        self.assertEqual(None, patch.prefix)
716        self.assertEqual(None, patch.version)
717
718        # With PATCH prefix
719        patch.parse_subject('[PATCH,2/5] Testing')
720        self.assertEqual('Testing', patch.subject)
721        self.assertEqual(2, patch.seq)
722        self.assertEqual(5, patch.count)
723        self.assertEqual('PATCH', patch.prefix)
724        self.assertEqual(None, patch.version)
725
726        # RFC patch
727        patch.parse_subject('[RFC,3/7] Testing')
728        self.assertEqual('Testing', patch.subject)
729        self.assertEqual(3, patch.seq)
730        self.assertEqual(7, patch.count)
731        self.assertEqual('RFC', patch.prefix)
732        self.assertEqual(None, patch.version)
733
734        # Version patch
735        patch.parse_subject('[v2,3/7] Testing')
736        self.assertEqual('Testing', patch.subject)
737        self.assertEqual(3, patch.seq)
738        self.assertEqual(7, patch.count)
739        self.assertEqual(None, patch.prefix)
740        self.assertEqual('v2', patch.version)
741
742        # All fields
743        patch.parse_subject('[RESEND,v2,3/7] Testing')
744        self.assertEqual('Testing', patch.subject)
745        self.assertEqual(3, patch.seq)
746        self.assertEqual(7, patch.count)
747        self.assertEqual('RESEND', patch.prefix)
748        self.assertEqual('v2', patch.version)
749
750        # RFC only
751        patch.parse_subject('[RESEND] Testing')
752        self.assertEqual('Testing', patch.subject)
753        self.assertEqual(1, patch.seq)
754        self.assertEqual(1, patch.count)
755        self.assertEqual('RESEND', patch.prefix)
756        self.assertEqual(None, patch.version)
757
758    def test_compare_series(self):
759        """Test operation of compare_with_series()"""
760        commit1 = Commit('abcd')
761        commit1.subject = 'Subject 1'
762        commit2 = Commit('ef12')
763        commit2.subject = 'Subject 2'
764        commit3 = Commit('3456')
765        commit3.subject = 'Subject 2'
766
767        patch1 = patchwork.Patch('1')
768        patch1.subject = 'Subject 1'
769        patch2 = patchwork.Patch('2')
770        patch2.subject = 'Subject 2'
771        patch3 = patchwork.Patch('3')
772        patch3.subject = 'Subject 2'
773
774        series = Series()
775        series.commits = [commit1]
776        patches = [patch1]
777        patch_for_commit, commit_for_patch, warnings = (
778            status.compare_with_series(series, patches))
779        self.assertEqual(1, len(patch_for_commit))
780        self.assertEqual(patch1, patch_for_commit[0])
781        self.assertEqual(1, len(commit_for_patch))
782        self.assertEqual(commit1, commit_for_patch[0])
783
784        series.commits = [commit1]
785        patches = [patch1, patch2]
786        patch_for_commit, commit_for_patch, warnings = (
787            status.compare_with_series(series, patches))
788        self.assertEqual(1, len(patch_for_commit))
789        self.assertEqual(patch1, patch_for_commit[0])
790        self.assertEqual(1, len(commit_for_patch))
791        self.assertEqual(commit1, commit_for_patch[0])
792        self.assertEqual(["Cannot find commit for patch 2 ('Subject 2')"],
793                         warnings)
794
795        series.commits = [commit1, commit2]
796        patches = [patch1]
797        patch_for_commit, commit_for_patch, warnings = (
798            status.compare_with_series(series, patches))
799        self.assertEqual(1, len(patch_for_commit))
800        self.assertEqual(patch1, patch_for_commit[0])
801        self.assertEqual(1, len(commit_for_patch))
802        self.assertEqual(commit1, commit_for_patch[0])
803        self.assertEqual(["Cannot find patch for commit 2 ('Subject 2')"],
804                         warnings)
805
806        series.commits = [commit1, commit2, commit3]
807        patches = [patch1, patch2]
808        patch_for_commit, commit_for_patch, warnings = (
809            status.compare_with_series(series, patches))
810        self.assertEqual(2, len(patch_for_commit))
811        self.assertEqual(patch1, patch_for_commit[0])
812        self.assertEqual(patch2, patch_for_commit[1])
813        self.assertEqual(1, len(commit_for_patch))
814        self.assertEqual(commit1, commit_for_patch[0])
815        self.assertEqual(["Cannot find patch for commit 3 ('Subject 2')",
816                          "Multiple commits match patch 2 ('Subject 2'):\n"
817                          '   Subject 2\n   Subject 2'],
818                         warnings)
819
820        series.commits = [commit1, commit2]
821        patches = [patch1, patch2, patch3]
822        patch_for_commit, commit_for_patch, warnings = (
823            status.compare_with_series(series, patches))
824        self.assertEqual(1, len(patch_for_commit))
825        self.assertEqual(patch1, patch_for_commit[0])
826        self.assertEqual(2, len(commit_for_patch))
827        self.assertEqual(commit1, commit_for_patch[0])
828        self.assertEqual(["Multiple patches match commit 2 ('Subject 2'):\n"
829                          '   Subject 2\n   Subject 2',
830                          "Cannot find commit for patch 3 ('Subject 2')"],
831                         warnings)
832
833    def _fake_patchwork2(self, subpath):
834        """Fake Patchwork server for the function below
835
836        This handles accessing series, patches and comments, providing the data
837        in self.patches to the caller
838
839        Args:
840            subpath (str): URL subpath to use
841        """
842        re_series = re.match(r'series/(\d*)/$', subpath)
843        re_patch = re.match(r'patches/(\d*)/$', subpath)
844        re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
845        if re_series:
846            series_num = re_series.group(1)
847            if series_num == '1234':
848                return {'patches': self.patches}
849        elif re_patch:
850            patch_num = int(re_patch.group(1))
851            patch = self.patches[patch_num - 1]
852            return patch
853        elif re_comments:
854            patch_num = int(re_comments.group(1))
855            patch = self.patches[patch_num - 1]
856            return patch.comments
857        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
858
859    def test_find_new_responses(self):
860        """Test operation of find_new_responses()"""
861        commit1 = Commit('abcd')
862        commit1.subject = 'Subject 1'
863        commit2 = Commit('ef12')
864        commit2.subject = 'Subject 2'
865
866        patch1 = patchwork.Patch('1')
867        patch1.parse_subject('[1/2] Subject 1')
868        patch1.name = patch1.raw_subject
869        patch1.content = 'This is my patch content'
870        comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
871
872        patch1.comments = [comment1a]
873
874        patch2 = patchwork.Patch('2')
875        patch2.parse_subject('[2/2] Subject 2')
876        patch2.name = patch2.raw_subject
877        patch2.content = 'Some other patch content'
878        comment2a = {
879            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
880                       (self.mary, self.leb)}
881        comment2b = {'content': 'Reviewed-by: %s' % self.fred}
882        patch2.comments = [comment2a, comment2b]
883
884        # This test works by setting up commits and patch for use by the fake
885        # Rest API function _fake_patchwork2(). It calls various functions in
886        # the status module after setting up tags in the commits, checking that
887        # things behaves as expected
888        self.commits = [commit1, commit2]
889        self.patches = [patch1, patch2]
890
891        # Check that the tags are picked up on the first patch
892        new_rtags, _ = status.process_reviews(patch1.content, patch1.comments,
893                                              commit1.rtags)
894        self.assertEqual(new_rtags, {'Reviewed-by': {self.joe}})
895
896        # Now the second patch
897        new_rtags, _ = status.process_reviews(patch2.content, patch2.comments,
898                                              commit2.rtags)
899        self.assertEqual(new_rtags, {
900            'Reviewed-by': {self.mary, self.fred},
901            'Tested-by': {self.leb}})
902
903        # Now add some tags to the commit, which means they should not appear as
904        # 'new' tags when scanning comments
905        commit1.rtags = {'Reviewed-by': {self.joe}}
906        new_rtags, _ = status.process_reviews(patch1.content, patch1.comments,
907                                              commit1.rtags)
908        self.assertEqual(new_rtags, {})
909
910        # For the second commit, add Ed and Fred, so only Mary should be left
911        commit2.rtags = {
912            'Tested-by': {self.leb},
913            'Reviewed-by': {self.fred}}
914        new_rtags, _ = status.process_reviews(patch2.content, patch2.comments,
915                                              commit2.rtags)
916        self.assertEqual(new_rtags, {'Reviewed-by': {self.mary}})
917
918        # Check that the output patches expectations:
919        #   1 Subject 1
920        #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
921        #   2 Subject 2
922        #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
923        #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
924        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
925        # 1 new response available in patchwork
926
927        series = Series()
928        series.commits = [commit1, commit2]
929        terminal.set_print_test_mode()
930        pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2)
931        status.check_and_show_status(series, '1234', None, None, False, False,
932                                     False, pwork)
933        itr = iter(terminal.get_print_test_lines())
934        col = terminal.Color()
935        self.assertEqual(terminal.PrintLine('  1 Subject 1', col.YELLOW),
936                         next(itr))
937        self.assertEqual(
938            terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
939                               bright=False),
940            next(itr))
941        self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
942                         next(itr))
943
944        self.assertEqual(terminal.PrintLine('  2 Subject 2', col.YELLOW),
945                         next(itr))
946        self.assertEqual(
947            terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
948                               bright=False),
949            next(itr))
950        self.assertEqual(terminal.PrintLine(self.fred, col.WHITE,
951                                            bright=False), next(itr))
952        self.assertEqual(
953            terminal.PrintLine('    Tested-by: ', col.GREEN, newline=False,
954                               bright=False),
955            next(itr))
956        self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
957                         next(itr))
958        self.assertEqual(
959            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
960            next(itr))
961        self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
962                         next(itr))
963        self.assertEqual(terminal.PrintLine(
964            '1 new response available in patchwork (use -d to write them to a new branch)',
965            None), next(itr))
966
967    def _fake_patchwork3(self, subpath):
968        """Fake Patchwork server for the function below
969
970        This handles accessing series, patches and comments, providing the data
971        in self.patches to the caller
972
973        Args:
974            subpath (str): URL subpath to use
975        """
976        re_series = re.match(r'series/(\d*)/$', subpath)
977        re_patch = re.match(r'patches/(\d*)/$', subpath)
978        re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
979        if re_series:
980            series_num = re_series.group(1)
981            if series_num == '1234':
982                return {'patches': self.patches}
983        elif re_patch:
984            patch_num = int(re_patch.group(1))
985            patch = self.patches[patch_num - 1]
986            return patch
987        elif re_comments:
988            patch_num = int(re_comments.group(1))
989            patch = self.patches[patch_num - 1]
990            return patch.comments
991        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
992
993    def test_create_branch(self):
994        """Test operation of create_branch()"""
995        repo = self.make_git_tree()
996        branch = 'first'
997        dest_branch = 'first2'
998        count = 2
999        gitdir = self.gitdir
1000
1001        # Set up the test git tree. We use branch 'first' which has two commits
1002        # in it
1003        series = patchstream.get_metadata_for_list(branch, gitdir, count)
1004        self.assertEqual(2, len(series.commits))
1005
1006        patch1 = patchwork.Patch('1')
1007        patch1.parse_subject('[1/2] %s' % series.commits[0].subject)
1008        patch1.name = patch1.raw_subject
1009        patch1.content = 'This is my patch content'
1010        comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
1011
1012        patch1.comments = [comment1a]
1013
1014        patch2 = patchwork.Patch('2')
1015        patch2.parse_subject('[2/2] %s' % series.commits[1].subject)
1016        patch2.name = patch2.raw_subject
1017        patch2.content = 'Some other patch content'
1018        comment2a = {
1019            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1020                       (self.mary, self.leb)}
1021        comment2b = {
1022            'content': 'Reviewed-by: %s' % self.fred}
1023        patch2.comments = [comment2a, comment2b]
1024
1025        # This test works by setting up patches for use by the fake Rest API
1026        # function _fake_patchwork3(). The fake patch comments above should
1027        # result in new review tags that are collected and added to the commits
1028        # created in the destination branch.
1029        self.patches = [patch1, patch2]
1030        count = 2
1031
1032        # Expected output:
1033        #   1 i2c: I2C things
1034        #   + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1035        #   2 spi: SPI fixes
1036        #   + Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1037        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1038        #   + Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1039        # 4 new responses available in patchwork
1040        # 4 responses added from patchwork into new branch 'first2'
1041        # <unittest.result.TestResult run=8 errors=0 failures=0>
1042
1043        terminal.set_print_test_mode()
1044        pwork = patchwork.Patchwork.for_testing(self._fake_patchwork3)
1045        status.check_and_show_status(
1046            series, '1234', branch, dest_branch, False, False, False, pwork,
1047            repo)
1048        lines = terminal.get_print_test_lines()
1049        self.assertEqual(12, len(lines))
1050        self.assertEqual(
1051            "4 responses added from patchwork into new branch 'first2'",
1052            lines[11].text)
1053
1054        # Check that the destination branch has the new tags
1055        new_series = patchstream.get_metadata_for_list(dest_branch, gitdir,
1056                                                       count)
1057        self.assertEqual(
1058            {'Reviewed-by': {self.joe}},
1059            new_series.commits[0].rtags)
1060        self.assertEqual(
1061            {'Tested-by': {self.leb},
1062             'Reviewed-by': {self.fred, self.mary}},
1063            new_series.commits[1].rtags)
1064
1065        # Now check the actual test of the first commit message. We expect to
1066        # see the new tags immediately below the old ones.
1067        stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir)
1068        itr = iter([line.strip() for line in stdout.splitlines()
1069                    if '-by:' in line])
1070
1071        # First patch should have the review tag
1072        self.assertEqual('Reviewed-by: %s' % self.joe, next(itr))
1073
1074        # Second patch should have the sign-off then the tested-by and two
1075        # reviewed-by tags
1076        self.assertEqual('Signed-off-by: %s' % self.leb, next(itr))
1077        self.assertEqual('Reviewed-by: %s' % self.fred, next(itr))
1078        self.assertEqual('Reviewed-by: %s' % self.mary, next(itr))
1079        self.assertEqual('Tested-by: %s' % self.leb, next(itr))
1080
1081    def test_parse_snippets(self):
1082        """Test parsing of review snippets"""
1083        text = '''Hi Fred,
1084
1085This is a comment from someone.
1086
1087Something else
1088
1089On some recent date, Fred wrote:
1090> This is why I wrote the patch
1091> so here it is
1092
1093Now a comment about the commit message
1094A little more to say
1095
1096Even more
1097
1098> diff --git a/file.c b/file.c
1099> Some more code
1100> Code line 2
1101> Code line 3
1102> Code line 4
1103> Code line 5
1104> Code line 6
1105> Code line 7
1106> Code line 8
1107> Code line 9
1108
1109And another comment
1110
1111> @@ -153,8 +143,13 @@ def check_patch(fname, show_types=False):
1112>  further down on the file
1113>  and more code
1114> +Addition here
1115> +Another addition here
1116>  codey
1117>  more codey
1118
1119and another thing in same file
1120
1121> @@ -253,8 +243,13 @@
1122>  with no function context
1123
1124one more thing
1125
1126> diff --git a/tools/patman/main.py b/tools/patman/main.py
1127> +line of code
1128now a very long comment in a different file
1129line2
1130line3
1131line4
1132line5
1133line6
1134line7
1135line8
1136'''
1137        pstrm = PatchStream.process_text(text, True)
1138        self.assertEqual([], pstrm.commit.warn)
1139
1140        # We expect to the filename and up to 5 lines of code context before
1141        # each comment. The 'On xxx wrote:' bit should be removed.
1142        self.assertEqual(
1143            [['Hi Fred,',
1144              'This is a comment from someone.',
1145              'Something else'],
1146             ['> This is why I wrote the patch',
1147              '> so here it is',
1148              'Now a comment about the commit message',
1149              'A little more to say', 'Even more'],
1150             ['> File: file.c', '> Code line 5', '> Code line 6',
1151              '> Code line 7', '> Code line 8', '> Code line 9',
1152              'And another comment'],
1153             ['> File: file.c',
1154              '> Line: 153 / 143: def check_patch(fname, show_types=False):',
1155              '>  and more code', '> +Addition here',
1156              '> +Another addition here', '>  codey', '>  more codey',
1157              'and another thing in same file'],
1158             ['> File: file.c', '> Line: 253 / 243',
1159              '>  with no function context', 'one more thing'],
1160             ['> File: tools/patman/main.py', '> +line of code',
1161              'now a very long comment in a different file',
1162              'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']],
1163            pstrm.snippets)
1164
1165    def test_review_snippets(self):
1166        """Test showing of review snippets"""
1167        def _to_submitter(who):
1168            m_who = re.match('(.*) <(.*)>', who)
1169            return {
1170                'name': m_who.group(1),
1171                'email': m_who.group(2)
1172                }
1173
1174        commit1 = Commit('abcd')
1175        commit1.subject = 'Subject 1'
1176        commit2 = Commit('ef12')
1177        commit2.subject = 'Subject 2'
1178
1179        patch1 = patchwork.Patch('1')
1180        patch1.parse_subject('[1/2] Subject 1')
1181        patch1.name = patch1.raw_subject
1182        patch1.content = 'This is my patch content'
1183        comment1a = {'submitter': _to_submitter(self.joe),
1184                     'content': '''Hi Fred,
1185
1186On some date Fred wrote:
1187
1188> diff --git a/file.c b/file.c
1189> Some code
1190> and more code
1191
1192Here is my comment above the above...
1193
1194
1195Reviewed-by: %s
1196''' % self.joe}
1197
1198        patch1.comments = [comment1a]
1199
1200        patch2 = patchwork.Patch('2')
1201        patch2.parse_subject('[2/2] Subject 2')
1202        patch2.name = patch2.raw_subject
1203        patch2.content = 'Some other patch content'
1204        comment2a = {
1205            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1206                       (self.mary, self.leb)}
1207        comment2b = {'submitter': _to_submitter(self.fred),
1208                     'content': '''Hi Fred,
1209
1210On some date Fred wrote:
1211
1212> diff --git a/tools/patman/commit.py b/tools/patman/commit.py
1213> @@ -41,6 +41,9 @@ class Commit:
1214>          self.rtags = collections.defaultdict(set)
1215>          self.warn = []
1216>
1217> +    def __str__(self):
1218> +        return self.subject
1219> +
1220>      def add_change(self, version, info):
1221>          """Add a new change line to the change list for a version.
1222>
1223A comment
1224
1225Reviewed-by: %s
1226''' % self.fred}
1227        patch2.comments = [comment2a, comment2b]
1228
1229        # This test works by setting up commits and patch for use by the fake
1230        # Rest API function _fake_patchwork2(). It calls various functions in
1231        # the status module after setting up tags in the commits, checking that
1232        # things behaves as expected
1233        self.commits = [commit1, commit2]
1234        self.patches = [patch1, patch2]
1235
1236        # Check that the output patches expectations:
1237        #   1 Subject 1
1238        #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1239        #   2 Subject 2
1240        #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1241        #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1242        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1243        # 1 new response available in patchwork
1244
1245        series = Series()
1246        series.commits = [commit1, commit2]
1247        terminal.set_print_test_mode()
1248        pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2)
1249        status.check_and_show_status(
1250            series, '1234', None, None, False, True, False, pwork)
1251        itr = iter(terminal.get_print_test_lines())
1252        col = terminal.Color()
1253        self.assertEqual(terminal.PrintLine('  1 Subject 1', col.YELLOW),
1254                         next(itr))
1255        self.assertEqual(
1256            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1257            next(itr))
1258        self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(itr))
1259
1260        self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1261                         next(itr))
1262        self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(itr))
1263        self.assertEqual(terminal.PrintLine('', None), next(itr))
1264        self.assertEqual(terminal.PrintLine('    > File: file.c', col.MAGENTA),
1265                         next(itr))
1266        self.assertEqual(terminal.PrintLine('    > Some code', col.MAGENTA),
1267                         next(itr))
1268        self.assertEqual(terminal.PrintLine('    > and more code',
1269                                            col.MAGENTA),
1270                         next(itr))
1271        self.assertEqual(terminal.PrintLine(
1272            '    Here is my comment above the above...', None), next(itr))
1273        self.assertEqual(terminal.PrintLine('', None), next(itr))
1274
1275        self.assertEqual(terminal.PrintLine('  2 Subject 2', col.YELLOW),
1276                         next(itr))
1277        self.assertEqual(
1278            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1279            next(itr))
1280        self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1281                         next(itr))
1282        self.assertEqual(
1283            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1284            next(itr))
1285        self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1286                         next(itr))
1287        self.assertEqual(
1288            terminal.PrintLine('  + Tested-by: ', col.GREEN, newline=False),
1289            next(itr))
1290        self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1291                         next(itr))
1292
1293        self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1294                         next(itr))
1295        self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(itr))
1296        self.assertEqual(terminal.PrintLine('', None), next(itr))
1297        self.assertEqual(terminal.PrintLine(
1298            '    > File: tools/patman/commit.py', col.MAGENTA), next(itr))
1299        self.assertEqual(terminal.PrintLine(
1300            '    > Line: 41 / 41: class Commit:', col.MAGENTA), next(itr))
1301        self.assertEqual(terminal.PrintLine(
1302            '    > +        return self.subject', col.MAGENTA), next(itr))
1303        self.assertEqual(terminal.PrintLine(
1304            '    > +', col.MAGENTA), next(itr))
1305        self.assertEqual(
1306            terminal.PrintLine(
1307                '    >      def add_change(self, version, info):',
1308                col.MAGENTA),
1309            next(itr))
1310        self.assertEqual(terminal.PrintLine(
1311            '    >          """Add a new change line to the change list for a version.',
1312            col.MAGENTA), next(itr))
1313        self.assertEqual(terminal.PrintLine(
1314            '    >', col.MAGENTA), next(itr))
1315        self.assertEqual(terminal.PrintLine(
1316            '    A comment', None), next(itr))
1317        self.assertEqual(terminal.PrintLine('', None), next(itr))
1318
1319        self.assertEqual(terminal.PrintLine(
1320            '4 new responses available in patchwork (use -d to write them to a new branch)',
1321            None), next(itr))
1322
1323    def test_insert_tags(self):
1324        """Test inserting of review tags"""
1325        msg = '''first line
1326second line.'''
1327        tags = [
1328            'Reviewed-by: Bin Meng <bmeng.cn@gmail.com>',
1329            'Tested-by: Bin Meng <bmeng.cn@gmail.com>'
1330            ]
1331        signoff = 'Signed-off-by: Simon Glass <sjg@chromium.com>'
1332        tag_str = '\n'.join(tags)
1333
1334        new_msg = patchstream.insert_tags(msg, tags)
1335        self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1336
1337        new_msg = patchstream.insert_tags(msg + '\n', tags)
1338        self.assertEqual(msg + '\n\n' + tag_str, new_msg)
1339
1340        msg += '\n\n' + signoff
1341        new_msg = patchstream.insert_tags(msg, tags)
1342        self.assertEqual(msg + '\n' + tag_str, new_msg)
1343