Merge tag 'dm-pull5nov20' of git://git.denx.de/u-boot-dm
[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         review_list = [None, None]
861
862         # Check that the tags are picked up on the first patch
863         status.find_new_responses(new_rtag_list, review_list, 0, commit1,
864                                   patch1, self._fake_patchwork2)
865         self.assertEqual(new_rtag_list[0], {'Reviewed-by': {self.joe}})
866
867         # Now the second patch
868         status.find_new_responses(new_rtag_list, review_list, 1, commit2,
869                                   patch2, self._fake_patchwork2)
870         self.assertEqual(new_rtag_list[1], {
871             'Reviewed-by': {self.mary, self.fred},
872             'Tested-by': {self.leb}})
873
874         # Now add some tags to the commit, which means they should not appear as
875         # 'new' tags when scanning comments
876         new_rtag_list = [None] * count
877         commit1.rtags = {'Reviewed-by': {self.joe}}
878         status.find_new_responses(new_rtag_list, review_list, 0, commit1,
879                                   patch1, self._fake_patchwork2)
880         self.assertEqual(new_rtag_list[0], {})
881
882         # For the second commit, add Ed and Fred, so only Mary should be left
883         commit2.rtags = {
884             'Tested-by': {self.leb},
885             'Reviewed-by': {self.fred}}
886         status.find_new_responses(new_rtag_list, review_list, 1, commit2,
887                                   patch2, self._fake_patchwork2)
888         self.assertEqual(new_rtag_list[1], {'Reviewed-by': {self.mary}})
889
890         # Check that the output patches expectations:
891         #   1 Subject 1
892         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
893         #   2 Subject 2
894         #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
895         #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
896         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
897         # 1 new response available in patchwork
898
899         series = Series()
900         series.commits = [commit1, commit2]
901         terminal.SetPrintTestMode()
902         status.check_patchwork_status(series, '1234', None, None, False, False,
903                                       self._fake_patchwork2)
904         lines = iter(terminal.GetPrintTestLines())
905         col = terminal.Color()
906         self.assertEqual(terminal.PrintLine('  1 Subject 1', col.BLUE),
907                          next(lines))
908         self.assertEqual(
909             terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
910                                bright=False),
911             next(lines))
912         self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
913                          next(lines))
914
915         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
916                          next(lines))
917         self.assertEqual(
918             terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
919                                bright=False),
920             next(lines))
921         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
922                          next(lines))
923         self.assertEqual(
924             terminal.PrintLine('    Tested-by: ', col.GREEN, newline=False,
925                                bright=False),
926             next(lines))
927         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
928                          next(lines))
929         self.assertEqual(
930             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
931             next(lines))
932         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
933                          next(lines))
934         self.assertEqual(terminal.PrintLine(
935             '1 new response available in patchwork (use -d to write them to a new branch)',
936             None), next(lines))
937
938     def _fake_patchwork3(self, subpath):
939         """Fake Patchwork server for the function below
940
941         This handles accessing series, patches and comments, providing the data
942         in self.patches to the caller
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     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
962     def testCreateBranch(self):
963         """Test operation of create_branch()"""
964         repo = self.make_git_tree()
965         branch = 'first'
966         dest_branch = 'first2'
967         count = 2
968         gitdir = os.path.join(self.gitdir, '.git')
969
970         # Set up the test git tree. We use branch 'first' which has two commits
971         # in it
972         series = patchstream.get_metadata_for_list(branch, gitdir, count)
973         self.assertEqual(2, len(series.commits))
974
975         patch1 = status.Patch('1')
976         patch1.parse_subject('[1/2] %s' % series.commits[0].subject)
977         patch1.name = patch1.raw_subject
978         patch1.content = 'This is my patch content'
979         comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
980
981         patch1.comments = [comment1a]
982
983         patch2 = status.Patch('2')
984         patch2.parse_subject('[2/2] %s' % series.commits[1].subject)
985         patch2.name = patch2.raw_subject
986         patch2.content = 'Some other patch content'
987         comment2a = {
988             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
989                        (self.mary, self.leb)}
990         comment2b = {
991             'content': 'Reviewed-by: %s' % self.fred}
992         patch2.comments = [comment2a, comment2b]
993
994         # This test works by setting up patches for use by the fake Rest API
995         # function _fake_patchwork3(). The fake patch comments above should
996         # result in new review tags that are collected and added to the commits
997         # created in the destination branch.
998         self.patches = [patch1, patch2]
999         count = 2
1000
1001         # Expected output:
1002         #   1 i2c: I2C things
1003         #   + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1004         #   2 spi: SPI fixes
1005         #   + Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1006         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1007         #   + Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1008         # 4 new responses available in patchwork
1009         # 4 responses added from patchwork into new branch 'first2'
1010         # <unittest.result.TestResult run=8 errors=0 failures=0>
1011
1012         terminal.SetPrintTestMode()
1013         status.check_patchwork_status(series, '1234', branch, dest_branch,
1014                                       False, False, self._fake_patchwork3, 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     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
1049     def testParseSnippets(self):
1050         """Test parsing of review snippets"""
1051         text = '''Hi Fred,
1052
1053 This is a comment from someone.
1054
1055 Something else
1056
1057 On some recent date, Fred wrote:
1058 > This is why I wrote the patch
1059 > so here it is
1060
1061 Now a comment about the commit message
1062 A little more to say
1063
1064 Even more
1065
1066 > diff --git a/file.c b/file.c
1067 > Some more code
1068 > Code line 2
1069 > Code line 3
1070 > Code line 4
1071 > Code line 5
1072 > Code line 6
1073 > Code line 7
1074 > Code line 8
1075 > Code line 9
1076
1077 And another comment
1078
1079 > @@ -153,8 +143,13 @@ def CheckPatch(fname, show_types=False):
1080 >  further down on the file
1081 >  and more code
1082 > +Addition here
1083 > +Another addition here
1084 >  codey
1085 >  more codey
1086
1087 and another thing in same file
1088
1089 > @@ -253,8 +243,13 @@
1090 >  with no function context
1091
1092 one more thing
1093
1094 > diff --git a/tools/patman/main.py b/tools/patman/main.py
1095 > +line of code
1096 now a very long comment in a different file
1097 line2
1098 line3
1099 line4
1100 line5
1101 line6
1102 line7
1103 line8
1104 '''
1105         pstrm = PatchStream.process_text(text, True)
1106         self.assertEqual([], pstrm.commit.warn)
1107
1108         # We expect to the filename and up to 5 lines of code context before
1109         # each comment. The 'On xxx wrote:' bit should be removed.
1110         self.assertEqual(
1111             [['Hi Fred,',
1112               'This is a comment from someone.',
1113               'Something else'],
1114              ['> This is why I wrote the patch',
1115               '> so here it is',
1116               'Now a comment about the commit message',
1117               'A little more to say', 'Even more'],
1118              ['> File: file.c', '> Code line 5', '> Code line 6',
1119               '> Code line 7', '> Code line 8', '> Code line 9',
1120               'And another comment'],
1121              ['> File: file.c',
1122               '> Line: 153 / 143: def CheckPatch(fname, show_types=False):',
1123               '>  and more code', '> +Addition here', '> +Another addition here',
1124               '>  codey', '>  more codey', 'and another thing in same file'],
1125              ['> File: file.c', '> Line: 253 / 243',
1126               '>  with no function context', 'one more thing'],
1127              ['> File: tools/patman/main.py', '> +line of code',
1128               'now a very long comment in a different file',
1129               'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']],
1130             pstrm.snippets)
1131
1132     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
1133     def testReviewSnippets(self):
1134         """Test showing of review snippets"""
1135         def _to_submitter(who):
1136             m_who = re.match('(.*) <(.*)>', who)
1137             return {
1138                 'name': m_who.group(1),
1139                 'email': m_who.group(2)
1140                 }
1141
1142         commit1 = Commit('abcd')
1143         commit1.subject = 'Subject 1'
1144         commit2 = Commit('ef12')
1145         commit2.subject = 'Subject 2'
1146
1147         patch1 = status.Patch('1')
1148         patch1.parse_subject('[1/2] Subject 1')
1149         patch1.name = patch1.raw_subject
1150         patch1.content = 'This is my patch content'
1151         comment1a = {'submitter': _to_submitter(self.joe),
1152                      'content': '''Hi Fred,
1153
1154 On some date Fred wrote:
1155
1156 > diff --git a/file.c b/file.c
1157 > Some code
1158 > and more code
1159
1160 Here is my comment above the above...
1161
1162
1163 Reviewed-by: %s
1164 ''' % self.joe}
1165
1166         patch1.comments = [comment1a]
1167
1168         patch2 = status.Patch('2')
1169         patch2.parse_subject('[2/2] Subject 2')
1170         patch2.name = patch2.raw_subject
1171         patch2.content = 'Some other patch content'
1172         comment2a = {
1173             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1174                        (self.mary, self.leb)}
1175         comment2b = {'submitter': _to_submitter(self.fred),
1176                      'content': '''Hi Fred,
1177
1178 On some date Fred wrote:
1179
1180 > diff --git a/tools/patman/commit.py b/tools/patman/commit.py
1181 > @@ -41,6 +41,9 @@ class Commit:
1182 >          self.rtags = collections.defaultdict(set)
1183 >          self.warn = []
1184 >
1185 > +    def __str__(self):
1186 > +        return self.subject
1187 > +
1188 >      def AddChange(self, version, info):
1189 >          """Add a new change line to the change list for a version.
1190 >
1191 A comment
1192
1193 Reviewed-by: %s
1194 ''' % self.fred}
1195         patch2.comments = [comment2a, comment2b]
1196
1197         # This test works by setting up commits and patch for use by the fake
1198         # Rest API function _fake_patchwork2(). It calls various functions in
1199         # the status module after setting up tags in the commits, checking that
1200         # things behaves as expected
1201         self.commits = [commit1, commit2]
1202         self.patches = [patch1, patch2]
1203
1204         # Check that the output patches expectations:
1205         #   1 Subject 1
1206         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1207         #   2 Subject 2
1208         #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1209         #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1210         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1211         # 1 new response available in patchwork
1212
1213         series = Series()
1214         series.commits = [commit1, commit2]
1215         terminal.SetPrintTestMode()
1216         status.check_patchwork_status(series, '1234', None, None, False, True,
1217                                       self._fake_patchwork2)
1218         lines = iter(terminal.GetPrintTestLines())
1219         col = terminal.Color()
1220         self.assertEqual(terminal.PrintLine('  1 Subject 1', col.BLUE),
1221                          next(lines))
1222         self.assertEqual(
1223             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1224             next(lines))
1225         self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
1226
1227         self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1228                          next(lines))
1229         self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1230         self.assertEqual(terminal.PrintLine('', None), next(lines))
1231         self.assertEqual(terminal.PrintLine('    > File: file.c', col.MAGENTA),
1232                          next(lines))
1233         self.assertEqual(terminal.PrintLine('    > Some code', col.MAGENTA),
1234                          next(lines))
1235         self.assertEqual(terminal.PrintLine('    > and more code', col.MAGENTA),
1236                          next(lines))
1237         self.assertEqual(terminal.PrintLine(
1238             '    Here is my comment above the above...', None), next(lines))
1239         self.assertEqual(terminal.PrintLine('', None), next(lines))
1240
1241         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
1242                          next(lines))
1243         self.assertEqual(
1244             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1245             next(lines))
1246         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1247                          next(lines))
1248         self.assertEqual(
1249             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1250             next(lines))
1251         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1252                          next(lines))
1253         self.assertEqual(
1254             terminal.PrintLine('  + Tested-by: ', col.GREEN, newline=False),
1255             next(lines))
1256         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1257                          next(lines))
1258
1259         self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1260                          next(lines))
1261         self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1262         self.assertEqual(terminal.PrintLine('', None), next(lines))
1263         self.assertEqual(terminal.PrintLine(
1264             '    > File: tools/patman/commit.py', col.MAGENTA), next(lines))
1265         self.assertEqual(terminal.PrintLine(
1266             '    > Line: 41 / 41: class Commit:', col.MAGENTA), next(lines))
1267         self.assertEqual(terminal.PrintLine(
1268             '    > +        return self.subject', col.MAGENTA), next(lines))
1269         self.assertEqual(terminal.PrintLine(
1270             '    > +', col.MAGENTA), next(lines))
1271         self.assertEqual(
1272             terminal.PrintLine('    >      def AddChange(self, version, info):',
1273                                col.MAGENTA),
1274             next(lines))
1275         self.assertEqual(terminal.PrintLine(
1276             '    >          """Add a new change line to the change list for a version.',
1277             col.MAGENTA), next(lines))
1278         self.assertEqual(terminal.PrintLine(
1279             '    >', col.MAGENTA), next(lines))
1280         self.assertEqual(terminal.PrintLine(
1281             '    A comment', None), next(lines))
1282         self.assertEqual(terminal.PrintLine('', None), next(lines))
1283
1284         self.assertEqual(terminal.PrintLine(
1285             '4 new responses available in patchwork (use -d to write them to a new branch)',
1286             None), next(lines))