1#!/usr/bin/env python3
2#
3# This file is part of GCC.
4#
5# GCC is free software; you can redistribute it and/or modify it under
6# the terms of the GNU General Public License as published by the Free
7# Software Foundation; either version 3, or (at your option) any later
8# version.
9#
10# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
11# WARRANTY; without even the implied warranty of MERCHANTABILITY or
12# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
13# for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with GCC; see the file COPYING3.  If not see
17# <http://www.gnu.org/licenses/>.  */
18
19import os
20import tempfile
21import unittest
22
23from git_commit import GitCommit
24
25from git_email import GitEmail
26
27import unidiff
28
29script_path = os.path.dirname(os.path.realpath(__file__))
30
31unidiff_supports_renaming = hasattr(unidiff.PatchedFile(), 'is_rename')
32
33
34NAME_STATUS1 = """
35M	gcc/ada/impunit.adb'
36R097	gcc/ada/libgnat/s-atopar.adb	gcc/ada/libgnat/s-aoinar.adb
37"""
38
39
40class TestGccChangelog(unittest.TestCase):
41    def setUp(self):
42        self.patches = {}
43        self.temps = []
44
45        filename = None
46        patch_lines = []
47        with open(os.path.join(script_path, 'test_patches.txt')) as f:
48            lines = f.read()
49        for line in lines.split('\n'):
50            if line.startswith('==='):
51                if patch_lines:
52                    self.patches[filename] = patch_lines
53                filename = line.split(' ')[1]
54                patch_lines = []
55            else:
56                patch_lines.append(line)
57        if patch_lines:
58            self.patches[filename] = patch_lines
59
60    def tearDown(self):
61        for t in self.temps:
62            assert t.endswith('.patch')
63            os.remove(t)
64
65    def get_git_email(self, filename):
66        with tempfile.NamedTemporaryFile(mode='w+', suffix='.patch',
67                                         delete=False) as f:
68            f.write('\n'.join(self.patches[filename]))
69            self.temps.append(f.name)
70        return GitEmail(f.name)
71
72    def from_patch_glob(self, name):
73        files = [f for f in self.patches.keys() if f.startswith(name)]
74        assert len(files) == 1
75        return self.get_git_email(files[0])
76
77    def test_simple_patch_format(self):
78        email = self.get_git_email('0577-aarch64-Add-an-and.patch')
79        assert not email.errors
80        assert len(email.changelog_entries) == 2
81        entry = email.changelog_entries[0]
82        assert (entry.author_lines ==
83                [('Richard Sandiford  <richard.sandiford@arm.com>',
84                  '2020-02-06')])
85        assert len(entry.authors) == 1
86        assert (entry.authors[0]
87                == 'Richard Sandiford  <richard.sandiford@arm.com>')
88        assert entry.folder == 'gcc'
89        assert entry.prs == ['PR target/87763']
90        assert len(entry.files) == 3
91        assert entry.files[0] == 'config/aarch64/aarch64-protos.h'
92
93    def test_daily_bump(self):
94        email = self.get_git_email('0085-Daily-bump.patch')
95        assert not email.errors
96        assert not email.changelog_entries
97
98    def test_deduce_changelog_entries(self):
99        email = self.from_patch_glob('0040')
100        assert len(email.changelog_entries) == 2
101        assert email.changelog_entries[0].folder == 'gcc/cp'
102        assert email.changelog_entries[0].prs == ['PR c++/90916']
103        assert email.changelog_entries[0].files == ['pt.c']
104        # this one is added automatically
105        assert email.changelog_entries[1].folder == 'gcc/testsuite'
106
107    def test_only_changelog_updated(self):
108        email = self.from_patch_glob('0129')
109        assert not email.errors
110        assert not email.changelog_entries
111
112    def test_wrong_mentioned_filename(self):
113        email = self.from_patch_glob('0096')
114        assert email.errors
115        err = email.errors[0]
116        assert err.message == 'unchanged file mentioned in a ChangeLog (did ' \
117            'you mean "gcc/testsuite/gcc.target/aarch64/' \
118            'advsimd-intrinsics/vdot-3-1.c"?)'
119        assert err.line == 'gcc/testsuite/gcc.target/aarch64/' \
120                           'advsimd-intrinsics/vdot-compile-3-1.c'
121
122    def test_missing_tab(self):
123        email = self.from_patch_glob('0031')
124        assert len(email.errors) == 2
125        err = email.errors[0]
126        assert err.message == 'line should start with a tab'
127        assert err.line == '    * cfgloopanal.c (average_num_loop_insns): ' \
128                           'Free bbs when early'
129
130    def test_leading_changelog_format(self):
131        email = self.from_patch_glob('0184')
132        assert len(email.errors) == 4
133        assert email.errors[0].line == 'gcc/c-family/c-cppbuiltins.c'
134        assert email.errors[2].line == 'gcc/c-family/c-cppbuiltin.c'
135
136    def test_cannot_deduce_no_blank_line(self):
137        email = self.from_patch_glob('0334')
138        assert len(email.errors) == 1
139        assert len(email.changelog_entries) == 1
140        assert email.changelog_entries[0].folder is None
141
142    def test_author_lines(self):
143        email = self.from_patch_glob('0814')
144        assert not email.errors
145        assert (email.changelog_entries[0].author_lines ==
146                [('Martin Jambor  <mjambor@suse.cz>', '2020-02-19')])
147
148    def test_multiple_authors_and_prs(self):
149        email = self.from_patch_glob('0735')
150        assert len(email.changelog_entries) == 1
151        entry = email.changelog_entries[0]
152        assert len(entry.author_lines) == 2
153        assert len(entry.authors) == 2
154        assert (entry.author_lines[1] ==
155                ('Bernd Edlinger  <bernd.edlinger@hotmail.de>', None))
156
157    def test_multiple_prs(self):
158        email = self.from_patch_glob('1699')
159        assert len(email.changelog_entries) == 2
160        assert len(email.changelog_entries[0].prs) == 2
161
162    def test_missing_PR_component(self):
163        email = self.from_patch_glob('0735')
164        assert len(email.errors) == 1
165        assert email.errors[0].message == 'missing PR component'
166
167    def test_invalid_PR_component(self):
168        email = self.from_patch_glob('0198')
169        assert len(email.errors) == 1
170        assert email.errors[0].message == 'invalid PR component'
171
172    def test_additional_author_list(self):
173        email = self.from_patch_glob('0342')
174        msg = 'additional author must be indented ' \
175              'with one tab and four spaces'
176        assert email.errors[1].message == msg
177
178    def test_trailing_whitespaces(self):
179        email = self.get_git_email('trailing-whitespaces.patch')
180        assert len(email.errors) == 3
181
182    def test_space_after_asterisk(self):
183        email = self.from_patch_glob('1999')
184        assert len(email.errors) == 1
185        assert email.errors[0].message == 'one space should follow asterisk'
186
187    def test_long_lines(self):
188        email = self.get_git_email('long-lines.patch')
189        assert len(email.errors) == 1
190        assert email.errors[0].message == 'line exceeds 100 character limit'
191
192    def test_new_files(self):
193        email = self.from_patch_glob('0030')
194        assert not email.errors
195
196    def test_wrong_changelog_location(self):
197        email = self.from_patch_glob('0043')
198        assert len(email.errors) == 2
199        assert (email.errors[0].message ==
200                'wrong ChangeLog location "gcc", should be "gcc/testsuite"')
201
202    def test_single_author_name(self):
203        email = self.from_patch_glob('1975')
204        assert len(email.changelog_entries) == 2
205        assert len(email.changelog_entries[0].author_lines) == 1
206        assert len(email.changelog_entries[1].author_lines) == 1
207
208    def test_bad_first_line(self):
209        email = self.from_patch_glob('0413')
210        assert len(email.errors) == 1
211
212    def test_co_authored_by(self):
213        email = self.from_patch_glob('1850')
214        assert email.co_authors == ['Jakub Jelinek  <jakub@redhat.com>']
215        output_entries = list(email.to_changelog_entries())
216        assert len(output_entries) == 2
217        ent0 = output_entries[0]
218        assert ent0[1].startswith('2020-04-16  Martin Liska  '
219                                  '<mliska@suse.cz>\n\t'
220                                  '    Jakub Jelinek  <jakub@redhat.com>')
221
222    def test_multiple_co_author_formats(self):
223        email = self.get_git_email('co-authored-by.patch')
224        assert len(email.co_authors) == 3
225        assert email.co_authors[0] == 'Jakub Jelinek  <jakub@redhat.com>'
226        assert email.co_authors[1] == 'John Miller  <jm@example.com>'
227        assert email.co_authors[2] == 'John Miller2  <jm2@example.com>'
228
229    def test_new_file_added_entry(self):
230        email = self.from_patch_glob('1957')
231        output_entries = list(email.to_changelog_entries())
232        assert len(output_entries) == 2
233        needle = ('\t* g++.dg/cpp2a/lambda-generic-variadic20.C'
234                  ': New file.')
235        assert output_entries[1][1].endswith(needle)
236        assert email.changelog_entries[1].prs == ['PR c++/94546']
237
238    def test_global_pr_entry(self):
239        email = self.from_patch_glob('2004')
240        assert not email.errors
241        assert email.changelog_entries[0].prs == ['PR other/94629']
242
243    def test_unique_prs(self):
244        email = self.get_git_email('pr-check1.patch')
245        assert not email.errors
246        assert email.changelog_entries[0].prs == ['PR ipa/12345']
247        assert email.changelog_entries[1].prs == []
248
249    def test_multiple_prs_not_added(self):
250        email = self.from_patch_glob('0002-Add-patch_are')
251        assert not email.errors
252        assert email.changelog_entries[0].prs == ['PR target/93492']
253        assert email.changelog_entries[1].prs == ['PR target/12345']
254        assert email.changelog_entries[2].prs == []
255        assert email.changelog_entries[2].folder == 'gcc/testsuite'
256
257    def test_strict_mode(self):
258        email = self.from_patch_glob('0001-Add-patch_are')
259        msg = 'ChangeLog, DATESTAMP, BASE-VER and DEV-PHASE updates should ' \
260              'be done separately from normal commits'
261        assert email.errors[0].message.startswith(msg)
262
263    def test_strict_mode_normal_patch(self):
264        email = self.get_git_email('0001-Just-test-it.patch')
265        assert not email.errors
266
267    def test_strict_mode_datestamp_only(self):
268        email = self.get_git_email('0002-Bump-date.patch')
269        assert not email.errors
270
271    def test_wrong_changelog_entry(self):
272        email = self.from_patch_glob('0020-IPA-Avoid')
273        msg = 'first line should start with a tab, an asterisk and a space'
274        assert (email.errors[0].message == msg)
275
276    def test_cherry_pick_format(self):
277        email = self.from_patch_glob('0001-c-Alias.patch')
278        assert not email.errors
279
280    def test_signatures(self):
281        email = self.from_patch_glob('0001-RISC-V-Make-unique.patch')
282        assert not email.errors
283        assert len(email.changelog_entries) == 1
284
285    def test_duplicate_top_level_author(self):
286        email = self.from_patch_glob('0001-Fortran-ProcPtr-function.patch')
287        assert not email.errors
288        assert len(email.changelog_entries[0].author_lines) == 1
289
290    def test_dr_entry(self):
291        email = self.from_patch_glob('0001-c-C-20-DR-2237.patch')
292        assert email.changelog_entries[0].prs == ['DR 2237']
293
294    def test_changes_only_in_ignored_location(self):
295        email = self.from_patch_glob('0001-go-in-ignored-location.patch')
296        assert not email.errors
297
298    def test_changelog_for_ignored_location(self):
299        email = self.from_patch_glob('0001-Update-merge.sh-to-reflect.patch')
300        assert (email.changelog_entries[0].lines[0]
301                == '\t* LOCAL_PATCHES: Use git hash instead of SVN id.')
302
303    def test_multiline_file_list(self):
304        email = self.from_patch_glob(
305            '0001-Ada-Reuse-Is_Package_Or_Generic_Package-where-possib.patch')
306        assert (email.changelog_entries[0].files
307                == ['contracts.adb', 'einfo.adb', 'exp_ch9.adb',
308                    'sem_ch12.adb', 'sem_ch4.adb', 'sem_ch7.adb',
309                    'sem_ch8.adb', 'sem_elab.adb', 'sem_type.adb',
310                    'sem_util.adb'])
311
312    @unittest.skipIf(not unidiff_supports_renaming,
313                     'Newer version of unidiff is needed (0.6.0+)')
314    def test_renamed_file(self):
315        email = self.from_patch_glob(
316            '0001-Ada-Add-support-for-XDR-streaming-in-the-default-run.patch')
317        assert not email.errors
318
319    def test_duplicite_author_lines(self):
320        email = self.from_patch_glob('0001-Fortran-type-is-real-kind-1.patch')
321        assert (email.changelog_entries[0].author_lines[0][0]
322                == 'Steven G. Kargl  <kargl@gcc.gnu.org>')
323        assert (email.changelog_entries[0].author_lines[1][0]
324                == 'Mark Eggleston  <markeggleston@gcc.gnu.org>')
325
326    def test_missing_change_description(self):
327        email = self.from_patch_glob('0001-Missing-change-description.patch')
328        assert len(email.errors) == 2
329        assert email.errors[0].message == 'missing description of a change'
330        assert email.errors[1].message == 'missing description of a change'
331
332    def test_libstdcxx_html_regenerated(self):
333        email = self.from_patch_glob('0001-Fix-text-of-hyperlink')
334        assert not email.errors
335        email = self.from_patch_glob('0002-libstdc-Fake-test-change-1.patch')
336        assert len(email.errors) == 1
337        msg = "pattern doesn't match any changed files"
338        assert email.errors[0].message == msg
339        assert email.errors[0].line == 'libstdc++-v3/doc/html/'
340        email = self.from_patch_glob('0003-libstdc-Fake-test-change-2.patch')
341        assert len(email.errors) == 1
342        msg = 'changed file not mentioned in a ChangeLog'
343        assert email.errors[0].message == msg
344
345    def test_not_deduce(self):
346        email = self.from_patch_glob('0001-configure.patch')
347        assert not email.errors
348        assert len(email.changelog_entries) == 2
349
350    def test_parse_git_name_status(self):
351        modified_files = GitCommit.parse_git_name_status(NAME_STATUS1)
352        assert len(modified_files) == 3
353        assert modified_files[1] == ('gcc/ada/libgnat/s-atopar.adb', 'D')
354        assert modified_files[2] == ('gcc/ada/libgnat/s-aoinar.adb', 'A')
355
356    def test_backport(self):
357        email = self.from_patch_glob('0001-asan-fix-RTX-emission.patch')
358        assert not email.errors
359        expected_hash = '8cff672cb9a132d3d3158c2edfc9a64b55292b80'
360        assert email.cherry_pick_commit == expected_hash
361        assert len(email.changelog_entries) == 1
362        entry = list(email.to_changelog_entries())[0][1]
363        assert entry.startswith('2020-06-11  Martin Liska  <mliska@suse.cz>')
364        assert '\tBackported from master:' in entry
365        assert '\t2020-06-11  Martin Liska  <mliska@suse.cz>' in entry
366        assert '\t\t    Jakub Jelinek  <jakub@redhat.com>' in entry
367
368    def test_backport_double_cherry_pick(self):
369        email = self.from_patch_glob('double-cherry-pick.patch')
370        assert email.errors[0].message.startswith('multiple cherry pick lines')
371
372    def test_square_and_lt_gt(self):
373        email = self.from_patch_glob('0001-Check-for-more-missing')
374        assert not email.errors
375
376    def test_empty_parenthesis(self):
377        email = self.from_patch_glob('0001-tree-optimization-97633-fix')
378        assert len(email.errors) == 1
379        assert email.errors[0].message == 'empty group "()" found'
380
381    def test_emptry_entry_desc(self):
382        email = self.from_patch_glob('0001-c-Set-CALL_FROM_NEW_OR')
383        assert len(email.errors) == 1
384        assert email.errors[0].message == 'missing description of a change'
385
386    def test_emptry_entry_desc_2(self):
387        email = self.from_patch_glob('0001-lto-fix-LTO-debug')
388        assert not email.errors
389        assert len(email.changelog_entries) == 1
390
391    def test_wildcard_in_subdir(self):
392        email = self.from_patch_glob('0001-Wildcard-subdirs.patch')
393        assert len(email.changelog_entries) == 1
394        err = email.errors[0]
395        assert err.message == "pattern doesn't match any changed files"
396        assert err.line == 'libstdc++-v3/testsuite/28_regex_not-existing/'
397
398    def test_unicode_chars_in_filename(self):
399        email = self.from_patch_glob('0001-Add-horse.patch')
400        assert not email.errors
401
402    def test_bad_unicode_chars_in_filename(self):
403        email = self.from_patch_glob('0001-Add-horse2.patch')
404        assert not email.errors
405        assert email.changelog_entries[0].files == ['kon����ek.txt']
406
407    def test_modification_of_old_changelog(self):
408        email = self.from_patch_glob('0001-fix-old-ChangeLog.patch')
409        assert not email.errors
410
411    def test_multiline_parentheses(self):
412        email = self.from_patch_glob('0001-Add-macro.patch')
413        assert not email.errors
414
415    def test_multiline_bad_parentheses(self):
416        email = self.from_patch_glob('0002-Wrong-macro-changelog.patch')
417        assert email.errors[0].message == 'bad parentheses wrapping'
418        assert email.errors[0].line == '	* config/i386/i386.md (*fix_trunc<mode>_i387_1,'
419
420    def test_changelog_removal(self):
421        email = self.from_patch_glob('0001-ChangeLog-removal.patch')
422        assert not email.errors
423
424    def test_long_filenames(self):
425        email = self.from_patch_glob('0001-long-filenames')
426        assert not email.errors
427
428    def test_multi_same_file(self):
429        email = self.from_patch_glob('0001-OpenMP-Fix-SIMT')
430        assert email.errors[0].message == 'same file specified multiple times'
431
432    def test_pr_only_in_subject(self):
433        email = self.from_patch_glob('0001-rs6000-Support-doubleword')
434        assert (email.errors[0].message ==
435                'PR 100085 in subject but not in changelog')
436
437    def test_wrong_pr_comp_in_subject(self):
438        email = self.from_patch_glob('pr-wrong-comp.patch')
439        assert email.errors[0].message == 'invalid PR component in subject'
440
441    def test_copyright_years(self):
442        email = self.from_patch_glob('copyright-years.patch')
443        assert not email.errors
444
445    def test_non_ascii_email(self):
446        email = self.from_patch_glob('non-ascii-email.patch')
447        assert (email.errors[0].message ==
448                'non-ASCII characters in git commit email address (jbglaw@��ug-owl.de)')
449
450    def test_new_file_in_root_folder(self):
451        email = self.from_patch_glob('toplev-new-file.patch')
452        assert (email.errors[0].message ==
453                'new file in the top-level folder not mentioned in a ChangeLog')
454