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