summaryrefslogtreecommitdiffstats
path: root/meta/lib/patchtest/tests/test_metadata.py
blob: be609dbd0488e17b5baba07fd0e54118a3333f4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# Checks related to the patch's LIC_FILES_CHKSUM  metadata variable
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only

import base
import os
import pyparsing
from data import PatchTestInput, PatchTestDataStore

class TestMetadata(base.Metadata):
    metadata_lic = 'LICENSE'
    invalid_license = 'PATCHTESTINVALID'
    metadata_chksum = 'LIC_FILES_CHKSUM'
    license_var  = 'LICENSE'
    closed   = 'CLOSED'
    lictag_re  = pyparsing.AtLineStart("License-Update:")
    lic_chksum_added = pyparsing.AtLineStart("+" + metadata_chksum)
    lic_chksum_removed = pyparsing.AtLineStart("-" + metadata_chksum)
    add_mark = pyparsing.Regex('\+ ')
    max_length = 200
    metadata_src_uri  = 'SRC_URI'
    md5sum    = 'md5sum'
    sha256sum = 'sha256sum'
    git_regex = pyparsing.Regex('^git\:\/\/.*')
    metadata_summary = 'SUMMARY'
    cve_check_ignore_var = 'CVE_CHECK_IGNORE'
    cve_status_var = 'CVE_STATUS'

    def test_license_presence(self):
        if not self.added:
            self.skip('No added recipes, skipping test')

        # TODO: this is a workaround so we can parse the recipe not
        # containing the LICENSE var: add some default license instead
        # of INVALID into auto.conf, then remove this line at the end
        auto_conf = os.path.join(os.environ.get('BUILDDIR'), 'conf', 'auto.conf')
        open_flag = 'w'
        if os.path.exists(auto_conf):
            open_flag = 'a'
        with open(auto_conf, open_flag) as fd:
            for pn in self.added:
                fd.write('LICENSE ??= "%s"\n' % self.invalid_license)

        no_license = False
        for pn in self.added:
            rd = self.tinfoil.parse_recipe(pn)
            license = rd.getVar(self.metadata_lic)
            if license == self.invalid_license:
                no_license = True
                break

        # remove auto.conf line or the file itself
        if open_flag == 'w':
            os.remove(auto_conf)
        else:
            fd = open(auto_conf, 'r')
            lines = fd.readlines()
            fd.close()
            with open(auto_conf, 'w') as fd:
                fd.write(''.join(lines[:-1]))

        if no_license:
            self.fail('Recipe does not have the LICENSE field set.')

    def test_lic_files_chksum_presence(self):
        if not self.added:
            self.skip('No added recipes, skipping test')

        for pn in self.added:
            rd = self.tinfoil.parse_recipe(pn)
            pathname = rd.getVar('FILE')
            # we are not interested in images
            if '/images/' in pathname:
                continue
            lic_files_chksum = rd.getVar(self.metadata_chksum)
            if rd.getVar(self.license_var) == self.closed:
                continue
            if not lic_files_chksum:
                self.fail('%s is missing in newly added recipe' % self.metadata_chksum)

    def test_lic_files_chksum_modified_not_mentioned(self):
        if not self.modified:
            self.skip('No modified recipes, skipping test')

        for patch in self.patchset:
            # for the moment, we are just interested in metadata
            if patch.path.endswith('.patch'):
                continue
            payload = str(patch)
            if (self.lic_chksum_added.search_string(payload) or self.lic_chksum_removed.search_string(payload)):
                # if any patch on the series contain reference on the metadata, fail
                for commit in self.commits:
                    if self.lictag_re.search_string(commit.commit_message):
                       break
                else:
                    self.fail('LIC_FILES_CHKSUM changed without "License-Update:" tag and description in commit message')

    def test_max_line_length(self):
        for patch in self.patchset:
            # for the moment, we are just interested in metadata
            if patch.path.endswith('.patch'):
                continue
            payload = str(patch)
            for line in payload.splitlines():
                if self.add_mark.search_string(line):
                    current_line_length = len(line[1:])
                    if current_line_length > self.max_length:
                        self.fail('Patch line too long (current length %s, maximum is %s)' % (current_line_length, self.max_length),
                                  data=[('Patch', patch.path), ('Line', '%s ...' % line[0:80])])

    def pretest_src_uri_left_files(self):
        # these tests just make sense on patches that can be merged
        if not PatchTestInput.repo.canbemerged:
            self.skip('Patch cannot be merged')
        if not self.modified:
            self.skip('No modified recipes, skipping pretest')

        # get the proper metadata values
        for pn in self.modified:
            # we are not interested in images
            if 'core-image' in pn:
                continue
            rd = self.tinfoil.parse_recipe(pn)
            PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata_src_uri, pn)] = rd.getVar(self.metadata_src_uri)

    def test_src_uri_left_files(self):
        # these tests just make sense on patches that can be merged
        if not PatchTestInput.repo.canbemerged:
            self.skip('Patch cannot be merged')
        if not self.modified:
            self.skip('No modified recipes, skipping pretest')

        # get the proper metadata values
        for pn in self.modified:
            # we are not interested in images
            if 'core-image' in pn:
                continue
            rd = self.tinfoil.parse_recipe(pn)
            PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata_src_uri, pn)] = rd.getVar(self.metadata_src_uri)

        for pn in self.modified:
            pretest_src_uri = PatchTestDataStore['pre%s-%s-%s' % (self.shortid(), self.metadata_src_uri, pn)].split()
            test_src_uri    = PatchTestDataStore['%s-%s-%s' % (self.shortid(), self.metadata_src_uri, pn)].split()

            pretest_files = set([os.path.basename(patch) for patch in pretest_src_uri if patch.startswith('file://')])
            test_files    = set([os.path.basename(patch) for patch in test_src_uri    if patch.startswith('file://')])

            # check if files were removed
            if len(test_files) < len(pretest_files):

                # get removals from patchset
                filesremoved_from_patchset = set()
                for patch in self.patchset:
                    if patch.is_removed_file:
                        filesremoved_from_patchset.add(os.path.basename(patch.path))

                # get the deleted files from the SRC_URI
                filesremoved_from_usr_uri = pretest_files - test_files

                # finally, get those patches removed at SRC_URI and not removed from the patchset
                # TODO: we are not taking into account  renames, so test may raise false positives
                not_removed = filesremoved_from_usr_uri - filesremoved_from_patchset
                if not_removed:
                    self.fail('Patches not removed from tree. Remove them and amend the submitted mbox',
                              data=[('Patch', f) for f in not_removed])

    def test_summary_presence(self):
        if not self.added:
            self.skip('No added recipes, skipping test')

        for pn in self.added:
            # we are not interested in images
            if 'core-image' in pn:
                continue
            rd = self.tinfoil.parse_recipe(pn)
            summary = rd.getVar(self.metadata_summary)

            # "${PN} version ${PN}-${PR}" is the default, so fail if default
            if summary.startswith('%s version' % pn):
                self.fail('%s is missing in newly added recipe' % self.metadata_summary)

    def test_cve_check_ignore(self):
        # Skip if we neither modified a recipe or target branches are not
        # Nanbield and newer. CVE_CHECK_IGNORE was first deprecated in Nanbield.
        if not self.modified or PatchTestInput.repo.branch == "kirkstone" or PatchTestInput.repo.branch == "dunfell":
            self.skip('No modified recipes or older target branch, skipping test')
        for pn in self.modified:
            # we are not interested in images
            if 'core-image' in pn:
                continue
            rd = self.tinfoil.parse_recipe(pn)
            cve_check_ignore = rd.getVar(self.cve_check_ignore_var)

            if cve_check_ignore is not None:
                self.fail('%s is deprecated and should be replaced by %s' % (self.cve_check_ignore_var, self.cve_status_var))