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