xref: /openbmc/openbmc/poky/meta/lib/patchtest/tests/test_metadata.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1# Checks related to the patch's LIC_FILES_CHKSUM  metadata variable
2#
3# Copyright (C) 2016 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6
7import base
8import collections
9import os
10import patchtest_patterns
11import pyparsing
12from patchtest_parser import PatchtestParser
13
14# Data store commonly used to share values between pre and post-merge tests
15PatchTestDataStore = collections.defaultdict(str)
16
17class TestMetadata(base.Metadata):
18
19    def test_license_presence(self):
20        if not self.added:
21            self.skip('No added recipes, skipping test')
22
23        # TODO: this is a workaround so we can parse the recipe not
24        # containing the LICENSE var: add some default license instead
25        # of INVALID into auto.conf, then remove this line at the end
26        auto_conf = os.path.join(os.environ.get('BUILDDIR'), 'conf', 'auto.conf')
27        open_flag = 'w'
28        if os.path.exists(auto_conf):
29            open_flag = 'a'
30        with open(auto_conf, open_flag) as fd:
31            for pn in self.added:
32                fd.write('LICENSE ??= "%s"\n' % patchtest_patterns.invalid_license)
33
34        no_license = False
35        for pn in self.added:
36            rd = self.tinfoil.parse_recipe(pn)
37            license = rd.getVar(patchtest_patterns.metadata_lic)
38            if license == patchtest_patterns.invalid_license:
39                no_license = True
40                break
41
42        # remove auto.conf line or the file itself
43        if open_flag == 'w':
44            os.remove(auto_conf)
45        else:
46            fd = open(auto_conf, 'r')
47            lines = fd.readlines()
48            fd.close()
49            with open(auto_conf, 'w') as fd:
50                fd.write(''.join(lines[:-1]))
51
52        if no_license:
53            self.fail('Recipe does not have the LICENSE field set.')
54
55    def test_lic_files_chksum_presence(self):
56        if not self.added:
57            self.skip('No added recipes, skipping test')
58
59        for pn in self.added:
60            rd = self.tinfoil.parse_recipe(pn)
61            pathname = rd.getVar('FILE')
62            # we are not interested in images
63            if '/images/' in pathname:
64                continue
65            lic_files_chksum = rd.getVar(patchtest_patterns.metadata_chksum)
66            if rd.getVar(patchtest_patterns.license_var) == patchtest_patterns.closed:
67                continue
68            if not lic_files_chksum:
69                self.fail(
70                    "%s is missing in newly added recipe" % patchtest_patterns.metadata_chksum
71                )
72
73    def test_lic_files_chksum_modified_not_mentioned(self):
74        if not self.modified:
75            self.skip('No modified recipes, skipping test')
76
77        for patch in self.patchset:
78            # for the moment, we are just interested in metadata
79            if patch.path.endswith('.patch'):
80                continue
81            payload = str(patch)
82            if patchtest_patterns.lic_chksum_added.search_string(
83                payload
84            ) or patchtest_patterns.lic_chksum_removed.search_string(payload):
85                # if any patch on the series contain reference on the metadata, fail
86                for commit in self.commits:
87                    if patchtest_patterns.lictag_re.search_string(commit.commit_message):
88                        break
89                else:
90                    self.fail('LIC_FILES_CHKSUM changed without "License-Update:" tag and description in commit message')
91
92    def test_max_line_length(self):
93        for patch in self.patchset:
94            # for the moment, we are just interested in metadata
95            if patch.path.endswith('.patch'):
96                continue
97            payload = str(patch)
98            for line in payload.splitlines():
99                if patchtest_patterns.add_mark.search_string(line):
100                    current_line_length = len(line[1:])
101                    if current_line_length > patchtest_patterns.patch_max_line_length:
102                        self.fail(
103                            "Patch line too long (current length %s, maximum is %s)"
104                            % (current_line_length, patchtest_patterns.patch_max_line_length),
105                            data=[
106                                ("Patch", patch.path),
107                                ("Line", "%s ..." % line[0:80]),
108                            ],
109                        )
110
111    def pretest_src_uri_left_files(self):
112        # these tests just make sense on patches that can be merged
113        if not PatchtestParser.repo.canbemerged:
114            self.skip("Patch cannot be merged")
115        if not self.modified:
116            self.skip('No modified recipes, skipping pretest')
117
118        # get the proper metadata values
119        for pn in self.modified:
120            # we are not interested in images
121            if 'core-image' in pn:
122                continue
123            rd = self.tinfoil.parse_recipe(pn)
124            PatchTestDataStore[
125                "%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
126            ] = rd.getVar(patchtest_patterns.metadata_src_uri)
127
128    def test_src_uri_left_files(self):
129        # these tests just make sense on patches that can be merged
130        if not PatchtestParser.repo.canbemerged:
131            self.skip("Patch cannot be merged")
132        if not self.modified:
133            self.skip('No modified recipes, skipping pretest')
134
135        # get the proper metadata values
136        for pn in self.modified:
137            # we are not interested in images
138            if 'core-image' in pn:
139                continue
140            rd = self.tinfoil.parse_recipe(pn)
141            PatchTestDataStore[
142                "%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
143            ] = rd.getVar(patchtest_patterns.metadata_src_uri)
144
145        for pn in self.modified:
146            pretest_src_uri = PatchTestDataStore[
147                "pre%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
148            ].split()
149            test_src_uri = PatchTestDataStore[
150                "%s-%s-%s" % (self.shortid(), patchtest_patterns.metadata_src_uri, pn)
151            ].split()
152
153            pretest_files = set([os.path.basename(patch) for patch in pretest_src_uri if patch.startswith('file://')])
154            test_files    = set([os.path.basename(patch) for patch in test_src_uri    if patch.startswith('file://')])
155
156            # check if files were removed
157            if len(test_files) < len(pretest_files):
158
159                # get removals from patchset
160                filesremoved_from_patchset = set()
161                for patch in self.patchset:
162                    if patch.is_removed_file:
163                        filesremoved_from_patchset.add(os.path.basename(patch.path))
164
165                # get the deleted files from the SRC_URI
166                filesremoved_from_usr_uri = pretest_files - test_files
167
168                # finally, get those patches removed at SRC_URI and not removed from the patchset
169                # TODO: we are not taking into account  renames, so test may raise false positives
170                not_removed = filesremoved_from_usr_uri - filesremoved_from_patchset
171                if not_removed:
172                    self.fail('Patches not removed from tree. Remove them and amend the submitted mbox',
173                              data=[('Patch', f) for f in not_removed])
174
175    def test_summary_presence(self):
176        if not self.added:
177            self.skip('No added recipes, skipping test')
178
179        for pn in self.added:
180            # we are not interested in images
181            if 'core-image' in pn:
182                continue
183            rd = self.tinfoil.parse_recipe(pn)
184            summary = rd.getVar(patchtest_patterns.metadata_summary)
185
186            # "${PN} version ${PN}-${PR}" is the default, so fail if default
187            if summary.startswith("%s version" % pn):
188                self.fail(
189                    "%s is missing in newly added recipe" % patchtest_patterns.metadata_summary
190                )
191
192    def test_cve_check_ignore(self):
193        # Skip if we neither modified a recipe or target branches are not
194        # Nanbield and newer. CVE_CHECK_IGNORE was first deprecated in Nanbield.
195        if (
196            not self.modified
197            or PatchtestParser.repo.patch.branch == "kirkstone"
198            or PatchtestParser.repo.patch.branch == "dunfell"
199        ):
200            self.skip("No modified recipes or older target branch, skipping test")
201        for pn in self.modified:
202            # we are not interested in images
203            if 'core-image' in pn:
204                continue
205            rd = self.tinfoil.parse_recipe(pn)
206            cve_check_ignore = rd.getVar(patchtest_patterns.cve_check_ignore_var)
207
208            if cve_check_ignore is not None:
209                self.fail(
210                    "%s is deprecated and should be replaced by %s"
211                    % (patchtest_patterns.cve_check_ignore_var, patchtest_patterns.cve_status_var)
212                )
213