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