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 os
10import re
11import shutil
12import sys
13import tempfile
14import unittest
15
16
17from patman.commit import Commit
18from patman import control
19from patman import gitutil
20from patman import patchstream
21from patman.patchstream import PatchStream
22from patman.series import Series
23from patman import settings
24from patman import terminal
25from patman import tools
26from patman.test_util import capture_sys_output
27
28import pygit2
29from patman import status
30
31class TestFunctional(unittest.TestCase):
32    """Functional tests for checking that patman behaves correctly"""
33    leb = (b'Lord Edmund Blackadd\xc3\xabr <weasel@blackadder.org>'.
34           decode('utf-8'))
35    fred = 'Fred Bloggs <f.bloggs@napier.net>'
36    joe = 'Joe Bloggs <joe@napierwallies.co.nz>'
37    mary = 'Mary Bloggs <mary@napierwallies.co.nz>'
38    commits = None
39    patches = None
40
41    def setUp(self):
42        self.tmpdir = tempfile.mkdtemp(prefix='patman.')
43        self.gitdir = os.path.join(self.tmpdir, 'git')
44        self.repo = None
45
46    def tearDown(self):
47        shutil.rmtree(self.tmpdir)
48        terminal.SetPrintTestMode(False)
49
50    @staticmethod
51    def _get_path(fname):
52        """Get the path to a test file
53
54        Args:
55            fname (str): Filename to obtain
56
57        Returns:
58            str: Full path to file in the test directory
59        """
60        return os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
61                            'test', fname)
62
63    @classmethod
64    def _get_text(cls, fname):
65        """Read a file as text
66
67        Args:
68            fname (str): Filename to read
69
70        Returns:
71            str: Contents of file
72        """
73        return open(cls._get_path(fname), encoding='utf-8').read()
74
75    @classmethod
76    def _get_patch_name(cls, subject):
77        """Get the filename of a patch given its subject
78
79        Args:
80            subject (str): Patch subject
81
82        Returns:
83            str: Filename for that patch
84        """
85        fname = re.sub('[ :]', '-', subject)
86        return fname.replace('--', '-')
87
88    def _create_patches_for_test(self, series):
89        """Create patch files for use by tests
90
91        This copies patch files from the test directory as needed by the series
92
93        Args:
94            series (Series): Series containing commits to convert
95
96        Returns:
97            tuple:
98                str: Cover-letter filename, or None if none
99                fname_list: list of str, each a patch filename
100        """
101        cover_fname = None
102        fname_list = []
103        for i, commit in enumerate(series.commits):
104            clean_subject = self._get_patch_name(commit.subject)
105            src_fname = '%04d-%s.patch' % (i + 1, clean_subject[:52])
106            fname = os.path.join(self.tmpdir, src_fname)
107            shutil.copy(self._get_path(src_fname), fname)
108            fname_list.append(fname)
109        if series.get('cover'):
110            src_fname = '0000-cover-letter.patch'
111            cover_fname = os.path.join(self.tmpdir, src_fname)
112            fname = os.path.join(self.tmpdir, src_fname)
113            shutil.copy(self._get_path(src_fname), fname)
114
115        return cover_fname, fname_list
116
117    def testBasic(self):
118        """Tests the basic flow of patman
119
120        This creates a series from some hard-coded patches build from a simple
121        tree with the following metadata in the top commit:
122
123            Series-to: u-boot
124            Series-prefix: RFC
125            Series-cc: Stefan Brüns <stefan.bruens@rwth-aachen.de>
126            Cover-letter-cc: Lord Mëlchett <clergy@palace.gov>
127            Series-version: 3
128            Patch-cc: fred
129            Series-process-log: sort, uniq
130            Series-changes: 4
131            - Some changes
132            - Multi
133              line
134              change
135
136            Commit-changes: 2
137            - Changes only for this commit
138
139            Cover-changes: 4
140            - Some notes for the cover letter
141
142            Cover-letter:
143            test: A test patch series
144            This is a test of how the cover
145            letter
146            works
147            END
148
149        and this in the first commit:
150
151            Commit-changes: 2
152            - second revision change
153
154            Series-notes:
155            some notes
156            about some things
157            from the first commit
158            END
159
160            Commit-notes:
161            Some notes about
162            the first commit
163            END
164
165        with the following commands:
166
167           git log -n2 --reverse >/path/to/tools/patman/test/test01.txt
168           git format-patch --subject-prefix RFC --cover-letter HEAD~2
169           mv 00* /path/to/tools/patman/test
170
171        It checks these aspects:
172            - git log can be processed by patchstream
173            - emailing patches uses the correct command
174            - CC file has information on each commit
175            - cover letter has the expected text and subject
176            - each patch has the correct subject
177            - dry-run information prints out correctly
178            - unicode is handled correctly
179            - Series-to, Series-cc, Series-prefix, Cover-letter
180            - Cover-letter-cc, Series-version, Series-changes, Series-notes
181            - Commit-notes
182        """
183        process_tags = True
184        ignore_bad_tags = False
185        stefan = b'Stefan Br\xc3\xbcns <stefan.bruens@rwth-aachen.de>'.decode('utf-8')
186        rick = 'Richard III <richard@palace.gov>'
187        mel = b'Lord M\xc3\xablchett <clergy@palace.gov>'.decode('utf-8')
188        add_maintainers = [stefan, rick]
189        dry_run = True
190        in_reply_to = mel
191        count = 2
192        settings.alias = {
193            'fdt': ['simon'],
194            'u-boot': ['u-boot@lists.denx.de'],
195            'simon': [self.leb],
196            'fred': [self.fred],
197        }
198
199        text = self._get_text('test01.txt')
200        series = patchstream.get_metadata_for_test(text)
201        cover_fname, args = self._create_patches_for_test(series)
202        with capture_sys_output() as out:
203            patchstream.fix_patches(series, args)
204            if cover_fname and series.get('cover'):
205                patchstream.insert_cover_letter(cover_fname, series, count)
206            series.DoChecks()
207            cc_file = series.MakeCcFile(process_tags, cover_fname,
208                                        not ignore_bad_tags, add_maintainers,
209                                        None)
210            cmd = gitutil.EmailPatches(
211                series, cover_fname, args, dry_run, not ignore_bad_tags,
212                cc_file, in_reply_to=in_reply_to, thread=None)
213            series.ShowActions(args, cmd, process_tags)
214        cc_lines = open(cc_file, encoding='utf-8').read().splitlines()
215        os.remove(cc_file)
216
217        lines = iter(out[0].getvalue().splitlines())
218        self.assertEqual('Cleaned %s patches' % len(series.commits),
219                         next(lines))
220        self.assertEqual('Change log missing for v2', next(lines))
221        self.assertEqual('Change log missing for v3', next(lines))
222        self.assertEqual('Change log for unknown version v4', next(lines))
223        self.assertEqual("Alias 'pci' not found", next(lines))
224        self.assertIn('Dry run', next(lines))
225        self.assertEqual('', next(lines))
226        self.assertIn('Send a total of %d patches' % count, next(lines))
227        prev = next(lines)
228        for i, commit in enumerate(series.commits):
229            self.assertEqual('   %s' % args[i], prev)
230            while True:
231                prev = next(lines)
232                if 'Cc:' not in prev:
233                    break
234        self.assertEqual('To:	  u-boot@lists.denx.de', prev)
235        self.assertEqual('Cc:	  %s' % stefan, next(lines))
236        self.assertEqual('Version:  3', next(lines))
237        self.assertEqual('Prefix:\t  RFC', next(lines))
238        self.assertEqual('Cover: 4 lines', next(lines))
239        self.assertEqual('      Cc:  %s' % self.fred, next(lines))
240        self.assertEqual('      Cc:  %s' % self.leb,
241                         next(lines))
242        self.assertEqual('      Cc:  %s' % mel, next(lines))
243        self.assertEqual('      Cc:  %s' % rick, next(lines))
244        expected = ('Git command: git send-email --annotate '
245                    '--in-reply-to="%s" --to "u-boot@lists.denx.de" '
246                    '--cc "%s" --cc-cmd "%s send --cc-cmd %s" %s %s'
247                    % (in_reply_to, stefan, sys.argv[0], cc_file, cover_fname,
248                       ' '.join(args)))
249        self.assertEqual(expected, next(lines))
250
251        self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
252        self.assertEqual(
253            '%s %s\0%s\0%s\0%s' % (args[1], self.fred, self.leb, rick, stefan),
254            cc_lines[1])
255
256        expected = '''
257This is a test of how the cover
258letter
259works
260
261some notes
262about some things
263from the first commit
264
265Changes in v4:
266- Multi
267  line
268  change
269- Some changes
270- Some notes for the cover letter
271
272Simon Glass (2):
273  pci: Correct cast for sandbox
274  fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
275
276 cmd/pci.c                   | 3 ++-
277 fs/fat/fat.c                | 1 +
278 lib/efi_loader/efi_memory.c | 1 +
279 lib/fdtdec.c                | 3 ++-
280 4 files changed, 6 insertions(+), 2 deletions(-)
281
282--\x20
2832.7.4
284
285'''
286        lines = open(cover_fname, encoding='utf-8').read().splitlines()
287        self.assertEqual(
288            'Subject: [RFC PATCH v3 0/2] test: A test patch series',
289            lines[3])
290        self.assertEqual(expected.splitlines(), lines[7:])
291
292        for i, fname in enumerate(args):
293            lines = open(fname, encoding='utf-8').read().splitlines()
294            subject = [line for line in lines if line.startswith('Subject')]
295            self.assertEqual('Subject: [RFC %d/%d]' % (i + 1, count),
296                             subject[0][:18])
297
298            # Check that we got our commit notes
299            start = 0
300            expected = ''
301
302            if i == 0:
303                start = 17
304                expected = '''---
305Some notes about
306the first commit
307
308(no changes since v2)
309
310Changes in v2:
311- second revision change'''
312            elif i == 1:
313                start = 17
314                expected = '''---
315
316Changes in v4:
317- Multi
318  line
319  change
320- Some changes
321
322Changes in v2:
323- Changes only for this commit'''
324
325            if expected:
326                expected = expected.splitlines()
327                self.assertEqual(expected, lines[start:(start+len(expected))])
328
329    def make_commit_with_file(self, subject, body, fname, text):
330        """Create a file and add it to the git repo with a new commit
331
332        Args:
333            subject (str): Subject for the commit
334            body (str): Body text of the commit
335            fname (str): Filename of file to create
336            text (str): Text to put into the file
337        """
338        path = os.path.join(self.gitdir, fname)
339        tools.WriteFile(path, text, binary=False)
340        index = self.repo.index
341        index.add(fname)
342        author = pygit2.Signature('Test user', 'test@email.com')
343        committer = author
344        tree = index.write_tree()
345        message = subject + '\n' + body
346        self.repo.create_commit('HEAD', author, committer, message, tree,
347                                [self.repo.head.target])
348
349    def make_git_tree(self):
350        """Make a simple git tree suitable for testing
351
352        It has three branches:
353            'base' has two commits: PCI, main
354            'first' has base as upstream and two more commits: I2C, SPI
355            'second' has base as upstream and three more: video, serial, bootm
356
357        Returns:
358            pygit2.Repository: repository
359        """
360        repo = pygit2.init_repository(self.gitdir)
361        self.repo = repo
362        new_tree = repo.TreeBuilder().write()
363
364        author = pygit2.Signature('Test user', 'test@email.com')
365        committer = author
366        _ = repo.create_commit('HEAD', author, committer, 'Created master',
367                               new_tree, [])
368
369        self.make_commit_with_file('Initial commit', '''
370Add a README
371
372''', 'README', '''This is the README file
373describing this project
374in very little detail''')
375
376        self.make_commit_with_file('pci: PCI implementation', '''
377Here is a basic PCI implementation
378
379''', 'pci.c', '''This is a file
380it has some contents
381and some more things''')
382        self.make_commit_with_file('main: Main program', '''
383Hello here is the second commit.
384''', 'main.c', '''This is the main file
385there is very little here
386but we can always add more later
387if we want to
388
389Series-to: u-boot
390Series-cc: Barry Crump <bcrump@whataroa.nz>
391''')
392        base_target = repo.revparse_single('HEAD')
393        self.make_commit_with_file('i2c: I2C things', '''
394This has some stuff to do with I2C
395''', 'i2c.c', '''And this is the file contents
396with some I2C-related things in it''')
397        self.make_commit_with_file('spi: SPI fixes', '''
398SPI needs some fixes
399and here they are
400
401Signed-off-by: %s
402
403Series-to: u-boot
404Commit-notes:
405title of the series
406This is the cover letter for the series
407with various details
408END
409''' % self.leb, 'spi.c', '''Some fixes for SPI in this
410file to make SPI work
411better than before''')
412        first_target = repo.revparse_single('HEAD')
413
414        target = repo.revparse_single('HEAD~2')
415        repo.reset(target.oid, pygit2.GIT_CHECKOUT_FORCE)
416        self.make_commit_with_file('video: Some video improvements', '''
417Fix up the video so that
418it looks more purple. Purple is
419a very nice colour.
420''', 'video.c', '''More purple here
421Purple and purple
422Even more purple
423Could not be any more purple''')
424        self.make_commit_with_file('serial: Add a serial driver', '''
425Here is the serial driver
426for my chip.
427
428Cover-letter:
429Series for my board
430This series implements support
431for my glorious board.
432END
433Series-links: 183237
434''', 'serial.c', '''The code for the
435serial driver is here''')
436        self.make_commit_with_file('bootm: Make it boot', '''
437This makes my board boot
438with a fix to the bootm
439command
440''', 'bootm.c', '''Fix up the bootm
441command to make the code as
442complicated as possible''')
443        second_target = repo.revparse_single('HEAD')
444
445        repo.branches.local.create('first', first_target)
446        repo.config.set_multivar('branch.first.remote', '', '.')
447        repo.config.set_multivar('branch.first.merge', '', 'refs/heads/base')
448
449        repo.branches.local.create('second', second_target)
450        repo.config.set_multivar('branch.second.remote', '', '.')
451        repo.config.set_multivar('branch.second.merge', '', 'refs/heads/base')
452
453        repo.branches.local.create('base', base_target)
454        return repo
455
456    def testBranch(self):
457        """Test creating patches from a branch"""
458        repo = self.make_git_tree()
459        target = repo.lookup_reference('refs/heads/first')
460        self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
461        control.setup()
462        try:
463            orig_dir = os.getcwd()
464            os.chdir(self.gitdir)
465
466            # Check that it can detect the current branch
467            self.assertEqual(2, gitutil.CountCommitsToBranch(None))
468            col = terminal.Color()
469            with capture_sys_output() as _:
470                _, cover_fname, patch_files = control.prepare_patches(
471                    col, branch=None, count=-1, start=0, end=0,
472                    ignore_binary=False, signoff=True)
473            self.assertIsNone(cover_fname)
474            self.assertEqual(2, len(patch_files))
475
476            # Check that it can detect a different branch
477            self.assertEqual(3, gitutil.CountCommitsToBranch('second'))
478            with capture_sys_output() as _:
479                _, cover_fname, patch_files = control.prepare_patches(
480                    col, branch='second', count=-1, start=0, end=0,
481                    ignore_binary=False, signoff=True)
482            self.assertIsNotNone(cover_fname)
483            self.assertEqual(3, len(patch_files))
484
485            # Check that it can skip patches at the end
486            with capture_sys_output() as _:
487                _, cover_fname, patch_files = control.prepare_patches(
488                    col, branch='second', count=-1, start=0, end=1,
489                    ignore_binary=False, signoff=True)
490            self.assertIsNotNone(cover_fname)
491            self.assertEqual(2, len(patch_files))
492        finally:
493            os.chdir(orig_dir)
494
495    def testTags(self):
496        """Test collection of tags in a patchstream"""
497        text = '''This is a patch
498
499Signed-off-by: Terminator
500Reviewed-by: %s
501Reviewed-by: %s
502Tested-by: %s
503''' % (self.joe, self.mary, self.leb)
504        pstrm = PatchStream.process_text(text)
505        self.assertEqual(pstrm.commit.rtags, {
506            'Reviewed-by': {self.joe, self.mary},
507            'Tested-by': {self.leb}})
508
509    def testMissingEnd(self):
510        """Test a missing END tag"""
511        text = '''This is a patch
512
513Cover-letter:
514This is the title
515missing END after this line
516Signed-off-by: Fred
517'''
518        pstrm = PatchStream.process_text(text)
519        self.assertEqual(["Missing 'END' in section 'cover'"],
520                         pstrm.commit.warn)
521
522    def testMissingBlankLine(self):
523        """Test a missing blank line after a tag"""
524        text = '''This is a patch
525
526Series-changes: 2
527- First line of changes
528- Missing blank line after this line
529Signed-off-by: Fred
530'''
531        pstrm = PatchStream.process_text(text)
532        self.assertEqual(["Missing 'blank line' in section 'Series-changes'"],
533                         pstrm.commit.warn)
534
535    def testInvalidCommitTag(self):
536        """Test an invalid Commit-xxx tag"""
537        text = '''This is a patch
538
539Commit-fred: testing
540'''
541        pstrm = PatchStream.process_text(text)
542        self.assertEqual(["Line 3: Ignoring Commit-fred"], pstrm.commit.warn)
543
544    def testSelfTest(self):
545        """Test a tested by tag by this user"""
546        test_line = 'Tested-by: %s@napier.com' % os.getenv('USER')
547        text = '''This is a patch
548
549%s
550''' % test_line
551        pstrm = PatchStream.process_text(text)
552        self.assertEqual(["Ignoring '%s'" % test_line], pstrm.commit.warn)
553
554    def testSpaceBeforeTab(self):
555        """Test a space before a tab"""
556        text = '''This is a patch
557
558+ \tSomething
559'''
560        pstrm = PatchStream.process_text(text)
561        self.assertEqual(["Line 3/0 has space before tab"], pstrm.commit.warn)
562
563    def testLinesAfterTest(self):
564        """Test detecting lines after TEST= line"""
565        text = '''This is a patch
566
567TEST=sometest
568more lines
569here
570'''
571        pstrm = PatchStream.process_text(text)
572        self.assertEqual(["Found 2 lines after TEST="], pstrm.commit.warn)
573
574    def testBlankLineAtEnd(self):
575        """Test detecting a blank line at the end of a file"""
576        text = '''This is a patch
577
578diff --git a/lib/fdtdec.c b/lib/fdtdec.c
579index c072e54..942244f 100644
580--- a/lib/fdtdec.c
581+++ b/lib/fdtdec.c
582@@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void)
583 	}
584
585 	gd->ram_size = (phys_size_t)(res.end - res.start + 1);
586-	debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size);
587+	debug("%s: Initial DRAM size %llx\n", __func__,
588+	      (unsigned long long)gd->ram_size);
589+
590diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
591
592--
5932.7.4
594
595 '''
596        pstrm = PatchStream.process_text(text)
597        self.assertEqual(
598            ["Found possible blank line(s) at end of file 'lib/fdtdec.c'"],
599            pstrm.commit.warn)
600
601    def testNoUpstream(self):
602        """Test CountCommitsToBranch when there is no upstream"""
603        repo = self.make_git_tree()
604        target = repo.lookup_reference('refs/heads/base')
605        self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
606
607        # Check that it can detect the current branch
608        try:
609            orig_dir = os.getcwd()
610            os.chdir(self.gitdir)
611            with self.assertRaises(ValueError) as exc:
612                gitutil.CountCommitsToBranch(None)
613            self.assertIn(
614                "Failed to determine upstream: fatal: no upstream configured for branch 'base'",
615                str(exc.exception))
616        finally:
617            os.chdir(orig_dir)
618
619    @staticmethod
620    def _fake_patchwork(url, subpath):
621        """Fake Patchwork server for the function below
622
623        This handles accessing a series, providing a list consisting of a
624        single patch
625
626        Args:
627            url (str): URL of patchwork server
628            subpath (str): URL subpath to use
629        """
630        re_series = re.match(r'series/(\d*)/$', subpath)
631        if re_series:
632            series_num = re_series.group(1)
633            if series_num == '1234':
634                return {'patches': [
635                    {'id': '1', 'name': 'Some patch'}]}
636        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
637
638    def testStatusMismatch(self):
639        """Test Patchwork patches not matching the series"""
640        series = Series()
641
642        with capture_sys_output() as (_, err):
643            status.collect_patches(series, 1234, None, self._fake_patchwork)
644        self.assertIn('Warning: Patchwork reports 1 patches, series has 0',
645                      err.getvalue())
646
647    def testStatusReadPatch(self):
648        """Test handling a single patch in Patchwork"""
649        series = Series()
650        series.commits = [Commit('abcd')]
651
652        patches = status.collect_patches(series, 1234, None,
653                                         self._fake_patchwork)
654        self.assertEqual(1, len(patches))
655        patch = patches[0]
656        self.assertEqual('1', patch.id)
657        self.assertEqual('Some patch', patch.raw_subject)
658
659    def testParseSubject(self):
660        """Test parsing of the patch subject"""
661        patch = status.Patch('1')
662
663        # Simple patch not in a series
664        patch.parse_subject('Testing')
665        self.assertEqual('Testing', patch.raw_subject)
666        self.assertEqual('Testing', patch.subject)
667        self.assertEqual(1, patch.seq)
668        self.assertEqual(1, patch.count)
669        self.assertEqual(None, patch.prefix)
670        self.assertEqual(None, patch.version)
671
672        # First patch in a series
673        patch.parse_subject('[1/2] Testing')
674        self.assertEqual('[1/2] Testing', patch.raw_subject)
675        self.assertEqual('Testing', patch.subject)
676        self.assertEqual(1, patch.seq)
677        self.assertEqual(2, patch.count)
678        self.assertEqual(None, patch.prefix)
679        self.assertEqual(None, patch.version)
680
681        # Second patch in a series
682        patch.parse_subject('[2/2] Testing')
683        self.assertEqual('Testing', patch.subject)
684        self.assertEqual(2, patch.seq)
685        self.assertEqual(2, patch.count)
686        self.assertEqual(None, patch.prefix)
687        self.assertEqual(None, patch.version)
688
689        # RFC patch
690        patch.parse_subject('[RFC,3/7] Testing')
691        self.assertEqual('Testing', patch.subject)
692        self.assertEqual(3, patch.seq)
693        self.assertEqual(7, patch.count)
694        self.assertEqual('RFC', patch.prefix)
695        self.assertEqual(None, patch.version)
696
697        # Version patch
698        patch.parse_subject('[v2,3/7] Testing')
699        self.assertEqual('Testing', patch.subject)
700        self.assertEqual(3, patch.seq)
701        self.assertEqual(7, patch.count)
702        self.assertEqual(None, patch.prefix)
703        self.assertEqual('v2', patch.version)
704
705        # All fields
706        patch.parse_subject('[RESEND,v2,3/7] Testing')
707        self.assertEqual('Testing', patch.subject)
708        self.assertEqual(3, patch.seq)
709        self.assertEqual(7, patch.count)
710        self.assertEqual('RESEND', patch.prefix)
711        self.assertEqual('v2', patch.version)
712
713        # RFC only
714        patch.parse_subject('[RESEND] Testing')
715        self.assertEqual('Testing', patch.subject)
716        self.assertEqual(1, patch.seq)
717        self.assertEqual(1, patch.count)
718        self.assertEqual('RESEND', patch.prefix)
719        self.assertEqual(None, patch.version)
720
721    def testCompareSeries(self):
722        """Test operation of compare_with_series()"""
723        commit1 = Commit('abcd')
724        commit1.subject = 'Subject 1'
725        commit2 = Commit('ef12')
726        commit2.subject = 'Subject 2'
727        commit3 = Commit('3456')
728        commit3.subject = 'Subject 2'
729
730        patch1 = status.Patch('1')
731        patch1.subject = 'Subject 1'
732        patch2 = status.Patch('2')
733        patch2.subject = 'Subject 2'
734        patch3 = status.Patch('3')
735        patch3.subject = 'Subject 2'
736
737        series = Series()
738        series.commits = [commit1]
739        patches = [patch1]
740        patch_for_commit, commit_for_patch, warnings = (
741            status.compare_with_series(series, patches))
742        self.assertEqual(1, len(patch_for_commit))
743        self.assertEqual(patch1, patch_for_commit[0])
744        self.assertEqual(1, len(commit_for_patch))
745        self.assertEqual(commit1, commit_for_patch[0])
746
747        series.commits = [commit1]
748        patches = [patch1, patch2]
749        patch_for_commit, commit_for_patch, warnings = (
750            status.compare_with_series(series, patches))
751        self.assertEqual(1, len(patch_for_commit))
752        self.assertEqual(patch1, patch_for_commit[0])
753        self.assertEqual(1, len(commit_for_patch))
754        self.assertEqual(commit1, commit_for_patch[0])
755        self.assertEqual(["Cannot find commit for patch 2 ('Subject 2')"],
756                         warnings)
757
758        series.commits = [commit1, commit2]
759        patches = [patch1]
760        patch_for_commit, commit_for_patch, warnings = (
761            status.compare_with_series(series, patches))
762        self.assertEqual(1, len(patch_for_commit))
763        self.assertEqual(patch1, patch_for_commit[0])
764        self.assertEqual(1, len(commit_for_patch))
765        self.assertEqual(commit1, commit_for_patch[0])
766        self.assertEqual(["Cannot find patch for commit 2 ('Subject 2')"],
767                         warnings)
768
769        series.commits = [commit1, commit2, commit3]
770        patches = [patch1, patch2]
771        patch_for_commit, commit_for_patch, warnings = (
772            status.compare_with_series(series, patches))
773        self.assertEqual(2, len(patch_for_commit))
774        self.assertEqual(patch1, patch_for_commit[0])
775        self.assertEqual(patch2, patch_for_commit[1])
776        self.assertEqual(1, len(commit_for_patch))
777        self.assertEqual(commit1, commit_for_patch[0])
778        self.assertEqual(["Cannot find patch for commit 3 ('Subject 2')",
779                          "Multiple commits match patch 2 ('Subject 2'):\n"
780                          '   Subject 2\n   Subject 2'],
781                         warnings)
782
783        series.commits = [commit1, commit2]
784        patches = [patch1, patch2, patch3]
785        patch_for_commit, commit_for_patch, warnings = (
786            status.compare_with_series(series, patches))
787        self.assertEqual(1, len(patch_for_commit))
788        self.assertEqual(patch1, patch_for_commit[0])
789        self.assertEqual(2, len(commit_for_patch))
790        self.assertEqual(commit1, commit_for_patch[0])
791        self.assertEqual(["Multiple patches match commit 2 ('Subject 2'):\n"
792                          '   Subject 2\n   Subject 2',
793                          "Cannot find commit for patch 3 ('Subject 2')"],
794                         warnings)
795
796    def _fake_patchwork2(self, url, subpath):
797        """Fake Patchwork server for the function below
798
799        This handles accessing series, patches and comments, providing the data
800        in self.patches to the caller
801
802        Args:
803            url (str): URL of patchwork server
804            subpath (str): URL subpath to use
805        """
806        re_series = re.match(r'series/(\d*)/$', subpath)
807        re_patch = re.match(r'patches/(\d*)/$', subpath)
808        re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
809        if re_series:
810            series_num = re_series.group(1)
811            if series_num == '1234':
812                return {'patches': self.patches}
813        elif re_patch:
814            patch_num = int(re_patch.group(1))
815            patch = self.patches[patch_num - 1]
816            return patch
817        elif re_comments:
818            patch_num = int(re_comments.group(1))
819            patch = self.patches[patch_num - 1]
820            return patch.comments
821        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
822
823    def testFindNewResponses(self):
824        """Test operation of find_new_responses()"""
825        commit1 = Commit('abcd')
826        commit1.subject = 'Subject 1'
827        commit2 = Commit('ef12')
828        commit2.subject = 'Subject 2'
829
830        patch1 = status.Patch('1')
831        patch1.parse_subject('[1/2] Subject 1')
832        patch1.name = patch1.raw_subject
833        patch1.content = 'This is my patch content'
834        comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
835
836        patch1.comments = [comment1a]
837
838        patch2 = status.Patch('2')
839        patch2.parse_subject('[2/2] Subject 2')
840        patch2.name = patch2.raw_subject
841        patch2.content = 'Some other patch content'
842        comment2a = {
843            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
844                       (self.mary, self.leb)}
845        comment2b = {'content': 'Reviewed-by: %s' % self.fred}
846        patch2.comments = [comment2a, comment2b]
847
848        # This test works by setting up commits and patch for use by the fake
849        # Rest API function _fake_patchwork2(). It calls various functions in
850        # the status module after setting up tags in the commits, checking that
851        # things behaves as expected
852        self.commits = [commit1, commit2]
853        self.patches = [patch1, patch2]
854        count = 2
855        new_rtag_list = [None] * count
856        review_list = [None, None]
857
858        # Check that the tags are picked up on the first patch
859        status.find_new_responses(new_rtag_list, review_list, 0, commit1,
860                                  patch1, None, self._fake_patchwork2)
861        self.assertEqual(new_rtag_list[0], {'Reviewed-by': {self.joe}})
862
863        # Now the second patch
864        status.find_new_responses(new_rtag_list, review_list, 1, commit2,
865                                  patch2, None, self._fake_patchwork2)
866        self.assertEqual(new_rtag_list[1], {
867            'Reviewed-by': {self.mary, self.fred},
868            'Tested-by': {self.leb}})
869
870        # Now add some tags to the commit, which means they should not appear as
871        # 'new' tags when scanning comments
872        new_rtag_list = [None] * count
873        commit1.rtags = {'Reviewed-by': {self.joe}}
874        status.find_new_responses(new_rtag_list, review_list, 0, commit1,
875                                  patch1, None, self._fake_patchwork2)
876        self.assertEqual(new_rtag_list[0], {})
877
878        # For the second commit, add Ed and Fred, so only Mary should be left
879        commit2.rtags = {
880            'Tested-by': {self.leb},
881            'Reviewed-by': {self.fred}}
882        status.find_new_responses(new_rtag_list, review_list, 1, commit2,
883                                  patch2, None, self._fake_patchwork2)
884        self.assertEqual(new_rtag_list[1], {'Reviewed-by': {self.mary}})
885
886        # Check that the output patches expectations:
887        #   1 Subject 1
888        #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
889        #   2 Subject 2
890        #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
891        #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
892        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
893        # 1 new response available in patchwork
894
895        series = Series()
896        series.commits = [commit1, commit2]
897        terminal.SetPrintTestMode()
898        status.check_patchwork_status(series, '1234', None, None, False, False,
899                                      None, self._fake_patchwork2)
900        lines = iter(terminal.GetPrintTestLines())
901        col = terminal.Color()
902        self.assertEqual(terminal.PrintLine('  1 Subject 1', col.BLUE),
903                         next(lines))
904        self.assertEqual(
905            terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
906                               bright=False),
907            next(lines))
908        self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
909                         next(lines))
910
911        self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
912                         next(lines))
913        self.assertEqual(
914            terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
915                               bright=False),
916            next(lines))
917        self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
918                         next(lines))
919        self.assertEqual(
920            terminal.PrintLine('    Tested-by: ', col.GREEN, newline=False,
921                               bright=False),
922            next(lines))
923        self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
924                         next(lines))
925        self.assertEqual(
926            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
927            next(lines))
928        self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
929                         next(lines))
930        self.assertEqual(terminal.PrintLine(
931            '1 new response available in patchwork (use -d to write them to a new branch)',
932            None), next(lines))
933
934    def _fake_patchwork3(self, url, subpath):
935        """Fake Patchwork server for the function below
936
937        This handles accessing series, patches and comments, providing the data
938        in self.patches to the caller
939
940        Args:
941            url (str): URL of patchwork server
942            subpath (str): URL subpath to use
943        """
944        re_series = re.match(r'series/(\d*)/$', subpath)
945        re_patch = re.match(r'patches/(\d*)/$', subpath)
946        re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
947        if re_series:
948            series_num = re_series.group(1)
949            if series_num == '1234':
950                return {'patches': self.patches}
951        elif re_patch:
952            patch_num = int(re_patch.group(1))
953            patch = self.patches[patch_num - 1]
954            return patch
955        elif re_comments:
956            patch_num = int(re_comments.group(1))
957            patch = self.patches[patch_num - 1]
958            return patch.comments
959        raise ValueError('Fake Patchwork does not understand: %s' % subpath)
960
961    def testCreateBranch(self):
962        """Test operation of create_branch()"""
963        repo = self.make_git_tree()
964        branch = 'first'
965        dest_branch = 'first2'
966        count = 2
967        gitdir = os.path.join(self.gitdir, '.git')
968
969        # Set up the test git tree. We use branch 'first' which has two commits
970        # in it
971        series = patchstream.get_metadata_for_list(branch, gitdir, count)
972        self.assertEqual(2, len(series.commits))
973
974        patch1 = status.Patch('1')
975        patch1.parse_subject('[1/2] %s' % series.commits[0].subject)
976        patch1.name = patch1.raw_subject
977        patch1.content = 'This is my patch content'
978        comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
979
980        patch1.comments = [comment1a]
981
982        patch2 = status.Patch('2')
983        patch2.parse_subject('[2/2] %s' % series.commits[1].subject)
984        patch2.name = patch2.raw_subject
985        patch2.content = 'Some other patch content'
986        comment2a = {
987            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
988                       (self.mary, self.leb)}
989        comment2b = {
990            'content': 'Reviewed-by: %s' % self.fred}
991        patch2.comments = [comment2a, comment2b]
992
993        # This test works by setting up patches for use by the fake Rest API
994        # function _fake_patchwork3(). The fake patch comments above should
995        # result in new review tags that are collected and added to the commits
996        # created in the destination branch.
997        self.patches = [patch1, patch2]
998        count = 2
999
1000        # Expected output:
1001        #   1 i2c: I2C things
1002        #   + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1003        #   2 spi: SPI fixes
1004        #   + Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1005        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1006        #   + Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1007        # 4 new responses available in patchwork
1008        # 4 responses added from patchwork into new branch 'first2'
1009        # <unittest.result.TestResult run=8 errors=0 failures=0>
1010
1011        terminal.SetPrintTestMode()
1012        status.check_patchwork_status(series, '1234', branch, dest_branch,
1013                                      False, False, None, self._fake_patchwork3,
1014                                      repo)
1015        lines = terminal.GetPrintTestLines()
1016        self.assertEqual(12, len(lines))
1017        self.assertEqual(
1018            "4 responses added from patchwork into new branch 'first2'",
1019            lines[11].text)
1020
1021        # Check that the destination branch has the new tags
1022        new_series = patchstream.get_metadata_for_list(dest_branch, gitdir,
1023                                                       count)
1024        self.assertEqual(
1025            {'Reviewed-by': {self.joe}},
1026            new_series.commits[0].rtags)
1027        self.assertEqual(
1028            {'Tested-by': {self.leb},
1029             'Reviewed-by': {self.fred, self.mary}},
1030            new_series.commits[1].rtags)
1031
1032        # Now check the actual test of the first commit message. We expect to
1033        # see the new tags immediately below the old ones.
1034        stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir)
1035        lines = iter([line.strip() for line in stdout.splitlines()
1036                      if '-by:' in line])
1037
1038        # First patch should have the review tag
1039        self.assertEqual('Reviewed-by: %s' % self.joe, next(lines))
1040
1041        # Second patch should have the sign-off then the tested-by and two
1042        # reviewed-by tags
1043        self.assertEqual('Signed-off-by: %s' % self.leb, next(lines))
1044        self.assertEqual('Reviewed-by: %s' % self.fred, next(lines))
1045        self.assertEqual('Reviewed-by: %s' % self.mary, next(lines))
1046        self.assertEqual('Tested-by: %s' % self.leb, next(lines))
1047
1048    def testParseSnippets(self):
1049        """Test parsing of review snippets"""
1050        text = '''Hi Fred,
1051
1052This is a comment from someone.
1053
1054Something else
1055
1056On some recent date, Fred wrote:
1057> This is why I wrote the patch
1058> so here it is
1059
1060Now a comment about the commit message
1061A little more to say
1062
1063Even more
1064
1065> diff --git a/file.c b/file.c
1066> Some more code
1067> Code line 2
1068> Code line 3
1069> Code line 4
1070> Code line 5
1071> Code line 6
1072> Code line 7
1073> Code line 8
1074> Code line 9
1075
1076And another comment
1077
1078> @@ -153,8 +143,13 @@ def CheckPatch(fname, show_types=False):
1079>  further down on the file
1080>  and more code
1081> +Addition here
1082> +Another addition here
1083>  codey
1084>  more codey
1085
1086and another thing in same file
1087
1088> @@ -253,8 +243,13 @@
1089>  with no function context
1090
1091one more thing
1092
1093> diff --git a/tools/patman/main.py b/tools/patman/main.py
1094> +line of code
1095now a very long comment in a different file
1096line2
1097line3
1098line4
1099line5
1100line6
1101line7
1102line8
1103'''
1104        pstrm = PatchStream.process_text(text, True)
1105        self.assertEqual([], pstrm.commit.warn)
1106
1107        # We expect to the filename and up to 5 lines of code context before
1108        # each comment. The 'On xxx wrote:' bit should be removed.
1109        self.assertEqual(
1110            [['Hi Fred,',
1111              'This is a comment from someone.',
1112              'Something else'],
1113             ['> This is why I wrote the patch',
1114              '> so here it is',
1115              'Now a comment about the commit message',
1116              'A little more to say', 'Even more'],
1117             ['> File: file.c', '> Code line 5', '> Code line 6',
1118              '> Code line 7', '> Code line 8', '> Code line 9',
1119              'And another comment'],
1120             ['> File: file.c',
1121              '> Line: 153 / 143: def CheckPatch(fname, show_types=False):',
1122              '>  and more code', '> +Addition here', '> +Another addition here',
1123              '>  codey', '>  more codey', 'and another thing in same file'],
1124             ['> File: file.c', '> Line: 253 / 243',
1125              '>  with no function context', 'one more thing'],
1126             ['> File: tools/patman/main.py', '> +line of code',
1127              'now a very long comment in a different file',
1128              'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']],
1129            pstrm.snippets)
1130
1131    def testReviewSnippets(self):
1132        """Test showing of review snippets"""
1133        def _to_submitter(who):
1134            m_who = re.match('(.*) <(.*)>', who)
1135            return {
1136                'name': m_who.group(1),
1137                'email': m_who.group(2)
1138                }
1139
1140        commit1 = Commit('abcd')
1141        commit1.subject = 'Subject 1'
1142        commit2 = Commit('ef12')
1143        commit2.subject = 'Subject 2'
1144
1145        patch1 = status.Patch('1')
1146        patch1.parse_subject('[1/2] Subject 1')
1147        patch1.name = patch1.raw_subject
1148        patch1.content = 'This is my patch content'
1149        comment1a = {'submitter': _to_submitter(self.joe),
1150                     'content': '''Hi Fred,
1151
1152On some date Fred wrote:
1153
1154> diff --git a/file.c b/file.c
1155> Some code
1156> and more code
1157
1158Here is my comment above the above...
1159
1160
1161Reviewed-by: %s
1162''' % self.joe}
1163
1164        patch1.comments = [comment1a]
1165
1166        patch2 = status.Patch('2')
1167        patch2.parse_subject('[2/2] Subject 2')
1168        patch2.name = patch2.raw_subject
1169        patch2.content = 'Some other patch content'
1170        comment2a = {
1171            'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1172                       (self.mary, self.leb)}
1173        comment2b = {'submitter': _to_submitter(self.fred),
1174                     'content': '''Hi Fred,
1175
1176On some date Fred wrote:
1177
1178> diff --git a/tools/patman/commit.py b/tools/patman/commit.py
1179> @@ -41,6 +41,9 @@ class Commit:
1180>          self.rtags = collections.defaultdict(set)
1181>          self.warn = []
1182>
1183> +    def __str__(self):
1184> +        return self.subject
1185> +
1186>      def AddChange(self, version, info):
1187>          """Add a new change line to the change list for a version.
1188>
1189A comment
1190
1191Reviewed-by: %s
1192''' % self.fred}
1193        patch2.comments = [comment2a, comment2b]
1194
1195        # This test works by setting up commits and patch for use by the fake
1196        # Rest API function _fake_patchwork2(). It calls various functions in
1197        # the status module after setting up tags in the commits, checking that
1198        # things behaves as expected
1199        self.commits = [commit1, commit2]
1200        self.patches = [patch1, patch2]
1201
1202        # Check that the output patches expectations:
1203        #   1 Subject 1
1204        #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1205        #   2 Subject 2
1206        #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1207        #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1208        #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1209        # 1 new response available in patchwork
1210
1211        series = Series()
1212        series.commits = [commit1, commit2]
1213        terminal.SetPrintTestMode()
1214        status.check_patchwork_status(series, '1234', None, None, False, True,
1215                                      None, self._fake_patchwork2)
1216        lines = iter(terminal.GetPrintTestLines())
1217        col = terminal.Color()
1218        self.assertEqual(terminal.PrintLine('  1 Subject 1', col.BLUE),
1219                         next(lines))
1220        self.assertEqual(
1221            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1222            next(lines))
1223        self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
1224
1225        self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1226                         next(lines))
1227        self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1228        self.assertEqual(terminal.PrintLine('', None), next(lines))
1229        self.assertEqual(terminal.PrintLine('    > File: file.c', col.MAGENTA),
1230                         next(lines))
1231        self.assertEqual(terminal.PrintLine('    > Some code', col.MAGENTA),
1232                         next(lines))
1233        self.assertEqual(terminal.PrintLine('    > and more code', col.MAGENTA),
1234                         next(lines))
1235        self.assertEqual(terminal.PrintLine(
1236            '    Here is my comment above the above...', None), next(lines))
1237        self.assertEqual(terminal.PrintLine('', None), next(lines))
1238
1239        self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
1240                         next(lines))
1241        self.assertEqual(
1242            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1243            next(lines))
1244        self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1245                         next(lines))
1246        self.assertEqual(
1247            terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1248            next(lines))
1249        self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1250                         next(lines))
1251        self.assertEqual(
1252            terminal.PrintLine('  + Tested-by: ', col.GREEN, newline=False),
1253            next(lines))
1254        self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1255                         next(lines))
1256
1257        self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1258                         next(lines))
1259        self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1260        self.assertEqual(terminal.PrintLine('', None), next(lines))
1261        self.assertEqual(terminal.PrintLine(
1262            '    > File: tools/patman/commit.py', col.MAGENTA), next(lines))
1263        self.assertEqual(terminal.PrintLine(
1264            '    > Line: 41 / 41: class Commit:', col.MAGENTA), next(lines))
1265        self.assertEqual(terminal.PrintLine(
1266            '    > +        return self.subject', col.MAGENTA), next(lines))
1267        self.assertEqual(terminal.PrintLine(
1268            '    > +', col.MAGENTA), next(lines))
1269        self.assertEqual(
1270            terminal.PrintLine('    >      def AddChange(self, version, info):',
1271                               col.MAGENTA),
1272            next(lines))
1273        self.assertEqual(terminal.PrintLine(
1274            '    >          """Add a new change line to the change list for a version.',
1275            col.MAGENTA), next(lines))
1276        self.assertEqual(terminal.PrintLine(
1277            '    >', col.MAGENTA), next(lines))
1278        self.assertEqual(terminal.PrintLine(
1279            '    A comment', None), next(lines))
1280        self.assertEqual(terminal.PrintLine('', None), next(lines))
1281
1282        self.assertEqual(terminal.PrintLine(
1283            '4 new responses available in patchwork (use -d to write them to a new branch)',
1284            None), next(lines))
1285