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