Merge tag 'u-boot-atmel-fixes-2021.01-b' of https://gitlab.denx.de/u-boot/custodians...
[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 send --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(url, 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         Args:
635             url (str): URL of patchwork server
636             subpath (str): URL subpath to use
637         """
638         re_series = re.match(r'series/(\d*)/$', subpath)
639         if re_series:
640             series_num = re_series.group(1)
641             if series_num == '1234':
642                 return {'patches': [
643                     {'id': '1', 'name': 'Some patch'}]}
644         raise ValueError('Fake Patchwork does not understand: %s' % subpath)
645
646     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
647     def testStatusMismatch(self):
648         """Test Patchwork patches not matching the series"""
649         series = Series()
650
651         with capture_sys_output() as (_, err):
652             status.collect_patches(series, 1234, None, self._fake_patchwork)
653         self.assertIn('Warning: Patchwork reports 1 patches, series has 0',
654                       err.getvalue())
655
656     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
657     def testStatusReadPatch(self):
658         """Test handling a single patch in Patchwork"""
659         series = Series()
660         series.commits = [Commit('abcd')]
661
662         patches = status.collect_patches(series, 1234, None,
663                                          self._fake_patchwork)
664         self.assertEqual(1, len(patches))
665         patch = patches[0]
666         self.assertEqual('1', patch.id)
667         self.assertEqual('Some patch', patch.raw_subject)
668
669     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
670     def testParseSubject(self):
671         """Test parsing of the patch subject"""
672         patch = status.Patch('1')
673
674         # Simple patch not in a series
675         patch.parse_subject('Testing')
676         self.assertEqual('Testing', patch.raw_subject)
677         self.assertEqual('Testing', patch.subject)
678         self.assertEqual(1, patch.seq)
679         self.assertEqual(1, patch.count)
680         self.assertEqual(None, patch.prefix)
681         self.assertEqual(None, patch.version)
682
683         # First patch in a series
684         patch.parse_subject('[1/2] Testing')
685         self.assertEqual('[1/2] Testing', patch.raw_subject)
686         self.assertEqual('Testing', patch.subject)
687         self.assertEqual(1, patch.seq)
688         self.assertEqual(2, patch.count)
689         self.assertEqual(None, patch.prefix)
690         self.assertEqual(None, patch.version)
691
692         # Second patch in a series
693         patch.parse_subject('[2/2] Testing')
694         self.assertEqual('Testing', patch.subject)
695         self.assertEqual(2, patch.seq)
696         self.assertEqual(2, patch.count)
697         self.assertEqual(None, patch.prefix)
698         self.assertEqual(None, patch.version)
699
700         # RFC patch
701         patch.parse_subject('[RFC,3/7] Testing')
702         self.assertEqual('Testing', patch.subject)
703         self.assertEqual(3, patch.seq)
704         self.assertEqual(7, patch.count)
705         self.assertEqual('RFC', patch.prefix)
706         self.assertEqual(None, patch.version)
707
708         # Version patch
709         patch.parse_subject('[v2,3/7] Testing')
710         self.assertEqual('Testing', patch.subject)
711         self.assertEqual(3, patch.seq)
712         self.assertEqual(7, patch.count)
713         self.assertEqual(None, patch.prefix)
714         self.assertEqual('v2', patch.version)
715
716         # All fields
717         patch.parse_subject('[RESEND,v2,3/7] Testing')
718         self.assertEqual('Testing', patch.subject)
719         self.assertEqual(3, patch.seq)
720         self.assertEqual(7, patch.count)
721         self.assertEqual('RESEND', patch.prefix)
722         self.assertEqual('v2', patch.version)
723
724         # RFC only
725         patch.parse_subject('[RESEND] Testing')
726         self.assertEqual('Testing', patch.subject)
727         self.assertEqual(1, patch.seq)
728         self.assertEqual(1, patch.count)
729         self.assertEqual('RESEND', patch.prefix)
730         self.assertEqual(None, patch.version)
731
732     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
733     def testCompareSeries(self):
734         """Test operation of compare_with_series()"""
735         commit1 = Commit('abcd')
736         commit1.subject = 'Subject 1'
737         commit2 = Commit('ef12')
738         commit2.subject = 'Subject 2'
739         commit3 = Commit('3456')
740         commit3.subject = 'Subject 2'
741
742         patch1 = status.Patch('1')
743         patch1.subject = 'Subject 1'
744         patch2 = status.Patch('2')
745         patch2.subject = 'Subject 2'
746         patch3 = status.Patch('3')
747         patch3.subject = 'Subject 2'
748
749         series = Series()
750         series.commits = [commit1]
751         patches = [patch1]
752         patch_for_commit, commit_for_patch, warnings = (
753             status.compare_with_series(series, patches))
754         self.assertEqual(1, len(patch_for_commit))
755         self.assertEqual(patch1, patch_for_commit[0])
756         self.assertEqual(1, len(commit_for_patch))
757         self.assertEqual(commit1, commit_for_patch[0])
758
759         series.commits = [commit1]
760         patches = [patch1, patch2]
761         patch_for_commit, commit_for_patch, warnings = (
762             status.compare_with_series(series, patches))
763         self.assertEqual(1, len(patch_for_commit))
764         self.assertEqual(patch1, patch_for_commit[0])
765         self.assertEqual(1, len(commit_for_patch))
766         self.assertEqual(commit1, commit_for_patch[0])
767         self.assertEqual(["Cannot find commit for patch 2 ('Subject 2')"],
768                          warnings)
769
770         series.commits = [commit1, commit2]
771         patches = [patch1]
772         patch_for_commit, commit_for_patch, warnings = (
773             status.compare_with_series(series, patches))
774         self.assertEqual(1, len(patch_for_commit))
775         self.assertEqual(patch1, patch_for_commit[0])
776         self.assertEqual(1, len(commit_for_patch))
777         self.assertEqual(commit1, commit_for_patch[0])
778         self.assertEqual(["Cannot find patch for commit 2 ('Subject 2')"],
779                          warnings)
780
781         series.commits = [commit1, commit2, commit3]
782         patches = [patch1, patch2]
783         patch_for_commit, commit_for_patch, warnings = (
784             status.compare_with_series(series, patches))
785         self.assertEqual(2, len(patch_for_commit))
786         self.assertEqual(patch1, patch_for_commit[0])
787         self.assertEqual(patch2, patch_for_commit[1])
788         self.assertEqual(1, len(commit_for_patch))
789         self.assertEqual(commit1, commit_for_patch[0])
790         self.assertEqual(["Cannot find patch for commit 3 ('Subject 2')",
791                           "Multiple commits match patch 2 ('Subject 2'):\n"
792                           '   Subject 2\n   Subject 2'],
793                          warnings)
794
795         series.commits = [commit1, commit2]
796         patches = [patch1, patch2, patch3]
797         patch_for_commit, commit_for_patch, warnings = (
798             status.compare_with_series(series, patches))
799         self.assertEqual(1, len(patch_for_commit))
800         self.assertEqual(patch1, patch_for_commit[0])
801         self.assertEqual(2, len(commit_for_patch))
802         self.assertEqual(commit1, commit_for_patch[0])
803         self.assertEqual(["Multiple patches match commit 2 ('Subject 2'):\n"
804                           '   Subject 2\n   Subject 2',
805                           "Cannot find commit for patch 3 ('Subject 2')"],
806                          warnings)
807
808     def _fake_patchwork2(self, url, subpath):
809         """Fake Patchwork server for the function below
810
811         This handles accessing series, patches and comments, providing the data
812         in self.patches to the caller
813
814         Args:
815             url (str): URL of patchwork server
816             subpath (str): URL subpath to use
817         """
818         re_series = re.match(r'series/(\d*)/$', subpath)
819         re_patch = re.match(r'patches/(\d*)/$', subpath)
820         re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
821         if re_series:
822             series_num = re_series.group(1)
823             if series_num == '1234':
824                 return {'patches': self.patches}
825         elif re_patch:
826             patch_num = int(re_patch.group(1))
827             patch = self.patches[patch_num - 1]
828             return patch
829         elif re_comments:
830             patch_num = int(re_comments.group(1))
831             patch = self.patches[patch_num - 1]
832             return patch.comments
833         raise ValueError('Fake Patchwork does not understand: %s' % subpath)
834
835     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
836     def testFindNewResponses(self):
837         """Test operation of find_new_responses()"""
838         commit1 = Commit('abcd')
839         commit1.subject = 'Subject 1'
840         commit2 = Commit('ef12')
841         commit2.subject = 'Subject 2'
842
843         patch1 = status.Patch('1')
844         patch1.parse_subject('[1/2] Subject 1')
845         patch1.name = patch1.raw_subject
846         patch1.content = 'This is my patch content'
847         comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
848
849         patch1.comments = [comment1a]
850
851         patch2 = status.Patch('2')
852         patch2.parse_subject('[2/2] Subject 2')
853         patch2.name = patch2.raw_subject
854         patch2.content = 'Some other patch content'
855         comment2a = {
856             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
857                        (self.mary, self.leb)}
858         comment2b = {'content': 'Reviewed-by: %s' % self.fred}
859         patch2.comments = [comment2a, comment2b]
860
861         # This test works by setting up commits and patch for use by the fake
862         # Rest API function _fake_patchwork2(). It calls various functions in
863         # the status module after setting up tags in the commits, checking that
864         # things behaves as expected
865         self.commits = [commit1, commit2]
866         self.patches = [patch1, patch2]
867         count = 2
868         new_rtag_list = [None] * count
869         review_list = [None, None]
870
871         # Check that the tags are picked up on the first patch
872         status.find_new_responses(new_rtag_list, review_list, 0, commit1,
873                                   patch1, None, self._fake_patchwork2)
874         self.assertEqual(new_rtag_list[0], {'Reviewed-by': {self.joe}})
875
876         # Now the second patch
877         status.find_new_responses(new_rtag_list, review_list, 1, commit2,
878                                   patch2, None, self._fake_patchwork2)
879         self.assertEqual(new_rtag_list[1], {
880             'Reviewed-by': {self.mary, self.fred},
881             'Tested-by': {self.leb}})
882
883         # Now add some tags to the commit, which means they should not appear as
884         # 'new' tags when scanning comments
885         new_rtag_list = [None] * count
886         commit1.rtags = {'Reviewed-by': {self.joe}}
887         status.find_new_responses(new_rtag_list, review_list, 0, commit1,
888                                   patch1, None, self._fake_patchwork2)
889         self.assertEqual(new_rtag_list[0], {})
890
891         # For the second commit, add Ed and Fred, so only Mary should be left
892         commit2.rtags = {
893             'Tested-by': {self.leb},
894             'Reviewed-by': {self.fred}}
895         status.find_new_responses(new_rtag_list, review_list, 1, commit2,
896                                   patch2, None, self._fake_patchwork2)
897         self.assertEqual(new_rtag_list[1], {'Reviewed-by': {self.mary}})
898
899         # Check that the output patches expectations:
900         #   1 Subject 1
901         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
902         #   2 Subject 2
903         #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
904         #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
905         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
906         # 1 new response available in patchwork
907
908         series = Series()
909         series.commits = [commit1, commit2]
910         terminal.SetPrintTestMode()
911         status.check_patchwork_status(series, '1234', None, None, False, False,
912                                       None, self._fake_patchwork2)
913         lines = iter(terminal.GetPrintTestLines())
914         col = terminal.Color()
915         self.assertEqual(terminal.PrintLine('  1 Subject 1', 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.joe, col.WHITE, bright=False),
922                          next(lines))
923
924         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
925                          next(lines))
926         self.assertEqual(
927             terminal.PrintLine('    Reviewed-by: ', col.GREEN, newline=False,
928                                bright=False),
929             next(lines))
930         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
931                          next(lines))
932         self.assertEqual(
933             terminal.PrintLine('    Tested-by: ', col.GREEN, newline=False,
934                                bright=False),
935             next(lines))
936         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
937                          next(lines))
938         self.assertEqual(
939             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
940             next(lines))
941         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
942                          next(lines))
943         self.assertEqual(terminal.PrintLine(
944             '1 new response available in patchwork (use -d to write them to a new branch)',
945             None), next(lines))
946
947     def _fake_patchwork3(self, url, subpath):
948         """Fake Patchwork server for the function below
949
950         This handles accessing series, patches and comments, providing the data
951         in self.patches to the caller
952
953         Args:
954             url (str): URL of patchwork server
955             subpath (str): URL subpath to use
956         """
957         re_series = re.match(r'series/(\d*)/$', subpath)
958         re_patch = re.match(r'patches/(\d*)/$', subpath)
959         re_comments = re.match(r'patches/(\d*)/comments/$', subpath)
960         if re_series:
961             series_num = re_series.group(1)
962             if series_num == '1234':
963                 return {'patches': self.patches}
964         elif re_patch:
965             patch_num = int(re_patch.group(1))
966             patch = self.patches[patch_num - 1]
967             return patch
968         elif re_comments:
969             patch_num = int(re_comments.group(1))
970             patch = self.patches[patch_num - 1]
971             return patch.comments
972         raise ValueError('Fake Patchwork does not understand: %s' % subpath)
973
974     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
975     def testCreateBranch(self):
976         """Test operation of create_branch()"""
977         repo = self.make_git_tree()
978         branch = 'first'
979         dest_branch = 'first2'
980         count = 2
981         gitdir = os.path.join(self.gitdir, '.git')
982
983         # Set up the test git tree. We use branch 'first' which has two commits
984         # in it
985         series = patchstream.get_metadata_for_list(branch, gitdir, count)
986         self.assertEqual(2, len(series.commits))
987
988         patch1 = status.Patch('1')
989         patch1.parse_subject('[1/2] %s' % series.commits[0].subject)
990         patch1.name = patch1.raw_subject
991         patch1.content = 'This is my patch content'
992         comment1a = {'content': 'Reviewed-by: %s\n' % self.joe}
993
994         patch1.comments = [comment1a]
995
996         patch2 = status.Patch('2')
997         patch2.parse_subject('[2/2] %s' % series.commits[1].subject)
998         patch2.name = patch2.raw_subject
999         patch2.content = 'Some other patch content'
1000         comment2a = {
1001             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1002                        (self.mary, self.leb)}
1003         comment2b = {
1004             'content': 'Reviewed-by: %s' % self.fred}
1005         patch2.comments = [comment2a, comment2b]
1006
1007         # This test works by setting up patches for use by the fake Rest API
1008         # function _fake_patchwork3(). The fake patch comments above should
1009         # result in new review tags that are collected and added to the commits
1010         # created in the destination branch.
1011         self.patches = [patch1, patch2]
1012         count = 2
1013
1014         # Expected output:
1015         #   1 i2c: I2C things
1016         #   + Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1017         #   2 spi: SPI fixes
1018         #   + Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1019         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1020         #   + Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1021         # 4 new responses available in patchwork
1022         # 4 responses added from patchwork into new branch 'first2'
1023         # <unittest.result.TestResult run=8 errors=0 failures=0>
1024
1025         terminal.SetPrintTestMode()
1026         status.check_patchwork_status(series, '1234', branch, dest_branch,
1027                                       False, False, None, self._fake_patchwork3,
1028                                       repo)
1029         lines = terminal.GetPrintTestLines()
1030         self.assertEqual(12, len(lines))
1031         self.assertEqual(
1032             "4 responses added from patchwork into new branch 'first2'",
1033             lines[11].text)
1034
1035         # Check that the destination branch has the new tags
1036         new_series = patchstream.get_metadata_for_list(dest_branch, gitdir,
1037                                                        count)
1038         self.assertEqual(
1039             {'Reviewed-by': {self.joe}},
1040             new_series.commits[0].rtags)
1041         self.assertEqual(
1042             {'Tested-by': {self.leb},
1043              'Reviewed-by': {self.fred, self.mary}},
1044             new_series.commits[1].rtags)
1045
1046         # Now check the actual test of the first commit message. We expect to
1047         # see the new tags immediately below the old ones.
1048         stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir)
1049         lines = iter([line.strip() for line in stdout.splitlines()
1050                       if '-by:' in line])
1051
1052         # First patch should have the review tag
1053         self.assertEqual('Reviewed-by: %s' % self.joe, next(lines))
1054
1055         # Second patch should have the sign-off then the tested-by and two
1056         # reviewed-by tags
1057         self.assertEqual('Signed-off-by: %s' % self.leb, next(lines))
1058         self.assertEqual('Reviewed-by: %s' % self.fred, next(lines))
1059         self.assertEqual('Reviewed-by: %s' % self.mary, next(lines))
1060         self.assertEqual('Tested-by: %s' % self.leb, next(lines))
1061
1062     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
1063     def testParseSnippets(self):
1064         """Test parsing of review snippets"""
1065         text = '''Hi Fred,
1066
1067 This is a comment from someone.
1068
1069 Something else
1070
1071 On some recent date, Fred wrote:
1072 > This is why I wrote the patch
1073 > so here it is
1074
1075 Now a comment about the commit message
1076 A little more to say
1077
1078 Even more
1079
1080 > diff --git a/file.c b/file.c
1081 > Some more code
1082 > Code line 2
1083 > Code line 3
1084 > Code line 4
1085 > Code line 5
1086 > Code line 6
1087 > Code line 7
1088 > Code line 8
1089 > Code line 9
1090
1091 And another comment
1092
1093 > @@ -153,8 +143,13 @@ def CheckPatch(fname, show_types=False):
1094 >  further down on the file
1095 >  and more code
1096 > +Addition here
1097 > +Another addition here
1098 >  codey
1099 >  more codey
1100
1101 and another thing in same file
1102
1103 > @@ -253,8 +243,13 @@
1104 >  with no function context
1105
1106 one more thing
1107
1108 > diff --git a/tools/patman/main.py b/tools/patman/main.py
1109 > +line of code
1110 now a very long comment in a different file
1111 line2
1112 line3
1113 line4
1114 line5
1115 line6
1116 line7
1117 line8
1118 '''
1119         pstrm = PatchStream.process_text(text, True)
1120         self.assertEqual([], pstrm.commit.warn)
1121
1122         # We expect to the filename and up to 5 lines of code context before
1123         # each comment. The 'On xxx wrote:' bit should be removed.
1124         self.assertEqual(
1125             [['Hi Fred,',
1126               'This is a comment from someone.',
1127               'Something else'],
1128              ['> This is why I wrote the patch',
1129               '> so here it is',
1130               'Now a comment about the commit message',
1131               'A little more to say', 'Even more'],
1132              ['> File: file.c', '> Code line 5', '> Code line 6',
1133               '> Code line 7', '> Code line 8', '> Code line 9',
1134               'And another comment'],
1135              ['> File: file.c',
1136               '> Line: 153 / 143: def CheckPatch(fname, show_types=False):',
1137               '>  and more code', '> +Addition here', '> +Another addition here',
1138               '>  codey', '>  more codey', 'and another thing in same file'],
1139              ['> File: file.c', '> Line: 253 / 243',
1140               '>  with no function context', 'one more thing'],
1141              ['> File: tools/patman/main.py', '> +line of code',
1142               'now a very long comment in a different file',
1143               'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']],
1144             pstrm.snippets)
1145
1146     @unittest.skipIf(not HAVE_PYGIT2, 'Missing python3-pygit2')
1147     def testReviewSnippets(self):
1148         """Test showing of review snippets"""
1149         def _to_submitter(who):
1150             m_who = re.match('(.*) <(.*)>', who)
1151             return {
1152                 'name': m_who.group(1),
1153                 'email': m_who.group(2)
1154                 }
1155
1156         commit1 = Commit('abcd')
1157         commit1.subject = 'Subject 1'
1158         commit2 = Commit('ef12')
1159         commit2.subject = 'Subject 2'
1160
1161         patch1 = status.Patch('1')
1162         patch1.parse_subject('[1/2] Subject 1')
1163         patch1.name = patch1.raw_subject
1164         patch1.content = 'This is my patch content'
1165         comment1a = {'submitter': _to_submitter(self.joe),
1166                      'content': '''Hi Fred,
1167
1168 On some date Fred wrote:
1169
1170 > diff --git a/file.c b/file.c
1171 > Some code
1172 > and more code
1173
1174 Here is my comment above the above...
1175
1176
1177 Reviewed-by: %s
1178 ''' % self.joe}
1179
1180         patch1.comments = [comment1a]
1181
1182         patch2 = status.Patch('2')
1183         patch2.parse_subject('[2/2] Subject 2')
1184         patch2.name = patch2.raw_subject
1185         patch2.content = 'Some other patch content'
1186         comment2a = {
1187             'content': 'Reviewed-by: %s\nTested-by: %s\n' %
1188                        (self.mary, self.leb)}
1189         comment2b = {'submitter': _to_submitter(self.fred),
1190                      'content': '''Hi Fred,
1191
1192 On some date Fred wrote:
1193
1194 > diff --git a/tools/patman/commit.py b/tools/patman/commit.py
1195 > @@ -41,6 +41,9 @@ class Commit:
1196 >          self.rtags = collections.defaultdict(set)
1197 >          self.warn = []
1198 >
1199 > +    def __str__(self):
1200 > +        return self.subject
1201 > +
1202 >      def AddChange(self, version, info):
1203 >          """Add a new change line to the change list for a version.
1204 >
1205 A comment
1206
1207 Reviewed-by: %s
1208 ''' % self.fred}
1209         patch2.comments = [comment2a, comment2b]
1210
1211         # This test works by setting up commits and patch for use by the fake
1212         # Rest API function _fake_patchwork2(). It calls various functions in
1213         # the status module after setting up tags in the commits, checking that
1214         # things behaves as expected
1215         self.commits = [commit1, commit2]
1216         self.patches = [patch1, patch2]
1217
1218         # Check that the output patches expectations:
1219         #   1 Subject 1
1220         #     Reviewed-by: Joe Bloggs <joe@napierwallies.co.nz>
1221         #   2 Subject 2
1222         #     Tested-by: Lord Edmund Blackaddër <weasel@blackadder.org>
1223         #     Reviewed-by: Fred Bloggs <f.bloggs@napier.net>
1224         #   + Reviewed-by: Mary Bloggs <mary@napierwallies.co.nz>
1225         # 1 new response available in patchwork
1226
1227         series = Series()
1228         series.commits = [commit1, commit2]
1229         terminal.SetPrintTestMode()
1230         status.check_patchwork_status(series, '1234', None, None, False, True,
1231                                       None, self._fake_patchwork2)
1232         lines = iter(terminal.GetPrintTestLines())
1233         col = terminal.Color()
1234         self.assertEqual(terminal.PrintLine('  1 Subject 1', col.BLUE),
1235                          next(lines))
1236         self.assertEqual(
1237             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1238             next(lines))
1239         self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
1240
1241         self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
1242                          next(lines))
1243         self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1244         self.assertEqual(terminal.PrintLine('', None), next(lines))
1245         self.assertEqual(terminal.PrintLine('    > File: file.c', col.MAGENTA),
1246                          next(lines))
1247         self.assertEqual(terminal.PrintLine('    > Some code', col.MAGENTA),
1248                          next(lines))
1249         self.assertEqual(terminal.PrintLine('    > and more code', col.MAGENTA),
1250                          next(lines))
1251         self.assertEqual(terminal.PrintLine(
1252             '    Here is my comment above the above...', None), next(lines))
1253         self.assertEqual(terminal.PrintLine('', None), next(lines))
1254
1255         self.assertEqual(terminal.PrintLine('  2 Subject 2', col.BLUE),
1256                          next(lines))
1257         self.assertEqual(
1258             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1259             next(lines))
1260         self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
1261                          next(lines))
1262         self.assertEqual(
1263             terminal.PrintLine('  + Reviewed-by: ', col.GREEN, newline=False),
1264             next(lines))
1265         self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
1266                          next(lines))
1267         self.assertEqual(
1268             terminal.PrintLine('  + Tested-by: ', col.GREEN, newline=False),
1269             next(lines))
1270         self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
1271                          next(lines))
1272
1273         self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
1274                          next(lines))
1275         self.assertEqual(terminal.PrintLine('    Hi Fred,', None), next(lines))
1276         self.assertEqual(terminal.PrintLine('', None), next(lines))
1277         self.assertEqual(terminal.PrintLine(
1278             '    > File: tools/patman/commit.py', col.MAGENTA), next(lines))
1279         self.assertEqual(terminal.PrintLine(
1280             '    > Line: 41 / 41: class Commit:', col.MAGENTA), next(lines))
1281         self.assertEqual(terminal.PrintLine(
1282             '    > +        return self.subject', col.MAGENTA), next(lines))
1283         self.assertEqual(terminal.PrintLine(
1284             '    > +', col.MAGENTA), next(lines))
1285         self.assertEqual(
1286             terminal.PrintLine('    >      def AddChange(self, version, info):',
1287                                col.MAGENTA),
1288             next(lines))
1289         self.assertEqual(terminal.PrintLine(
1290             '    >          """Add a new change line to the change list for a version.',
1291             col.MAGENTA), next(lines))
1292         self.assertEqual(terminal.PrintLine(
1293             '    >', col.MAGENTA), next(lines))
1294         self.assertEqual(terminal.PrintLine(
1295             '    A comment', None), next(lines))
1296         self.assertEqual(terminal.PrintLine('', None), next(lines))
1297
1298         self.assertEqual(terminal.PrintLine(
1299             '4 new responses available in patchwork (use -d to write them to a new branch)',
1300             None), next(lines))