]>
Commit | Line | Data |
---|---|---|
fbc9cb4c SZ |
1 | # @file EccCheck.py\r |
2 | #\r | |
22fe311b | 3 | # Copyright (c) 2021, Arm Limited. All rights reserved.<BR>\r |
fbc9cb4c SZ |
4 | # Copyright (c) 2020, Intel Corporation. All rights reserved.<BR>\r |
5 | # SPDX-License-Identifier: BSD-2-Clause-Patent\r | |
6 | ##\r | |
7 | \r | |
8 | import os\r | |
9 | import shutil\r | |
10 | import re\r | |
11 | import csv\r | |
12 | import xml.dom.minidom\r | |
13 | from typing import List, Dict, Tuple\r | |
14 | import logging\r | |
15 | from io import StringIO\r | |
16 | from edk2toolext.environment import shell_environment\r | |
17 | from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin\r | |
18 | from edk2toolext.environment.var_dict import VarDict\r | |
19 | from edk2toollib.utility_functions import RunCmd\r | |
20 | \r | |
21 | \r | |
22 | class EccCheck(ICiBuildPlugin):\r | |
23 | """\r | |
24 | A CiBuildPlugin that finds the Ecc issues of newly added code in pull request.\r | |
25 | \r | |
26 | Configuration options:\r | |
27 | "EccCheck": {\r | |
28 | "ExceptionList": [],\r | |
29 | "IgnoreFiles": []\r | |
30 | },\r | |
31 | """\r | |
32 | \r | |
33 | ReModifyFile = re.compile(r'[B-Q,S-Z]+[\d]*\t(.*)')\r | |
34 | FindModifyFile = re.compile(r'\+\+\+ b\/(.*)')\r | |
35 | LineScopePattern = (r'@@ -\d*\,*\d* \+\d*\,*\d* @@.*')\r | |
36 | LineNumRange = re.compile(r'@@ -\d*\,*\d* \+(\d*)\,*(\d*) @@.*')\r | |
37 | \r | |
38 | def GetTestName(self, packagename: str, environment: VarDict) -> tuple:\r | |
39 | """ Provide the testcase name and classname for use in reporting\r | |
40 | testclassname: a descriptive string for the testcase can include whitespace\r | |
41 | classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>\r | |
42 | \r | |
43 | Args:\r | |
44 | packagename: string containing name of package to build\r | |
45 | environment: The VarDict for the test to run in\r | |
46 | Returns:\r | |
47 | a tuple containing the testcase name and the classname\r | |
48 | (testcasename, classname)\r | |
49 | """\r | |
50 | return ("Check for efi coding style for " + packagename, packagename + ".EccCheck")\r | |
51 | \r | |
52 | ##\r | |
53 | # External function of plugin. This function is used to perform the task of the ci_build_plugin Plugin\r | |
54 | #\r | |
55 | # - package is the edk2 path to package. This means workspace/packagepath relative.\r | |
56 | # - edk2path object configured with workspace and packages path\r | |
57 | # - PkgConfig Object (dict) for the pkg\r | |
58 | # - EnvConfig Object\r | |
59 | # - Plugin Manager Instance\r | |
60 | # - Plugin Helper Obj Instance\r | |
61 | # - Junit Logger\r | |
62 | # - output_stream the StringIO output stream from this plugin via logging\r | |
63 | def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):\r | |
a050c599 | 64 | workspace_path = Edk2pathObj.WorkspacePath\r |
22fe311b PG |
65 | basetools_path = environment.GetValue("EDK_TOOLS_PATH")\r |
66 | python_path = os.path.join(basetools_path, "Source", "Python")\r | |
fbc9cb4c SZ |
67 | env = shell_environment.GetEnvironment()\r |
68 | env.set_shell_var('PYTHONPATH', python_path)\r | |
a050c599 | 69 | env.set_shell_var('WORKSPACE', workspace_path)\r |
fbc9cb4c | 70 | self.ECC_PASS = True\r |
a050c599 | 71 | self.ApplyConfig(pkgconfig, workspace_path, basetools_path, packagename)\r |
fbc9cb4c SZ |
72 | modify_dir_list = self.GetModifyDir(packagename)\r |
73 | patch = self.GetDiff(packagename)\r | |
a050c599 PG |
74 | ecc_diff_range = self.GetDiffRange(patch, packagename, workspace_path)\r |
75 | self.GenerateEccReport(modify_dir_list, ecc_diff_range, workspace_path, basetools_path)\r | |
76 | ecc_log = os.path.join(workspace_path, "Ecc.log")\r | |
fbc9cb4c SZ |
77 | self.RevertCode()\r |
78 | if self.ECC_PASS:\r | |
79 | tc.SetSuccess()\r | |
80 | self.RemoveFile(ecc_log)\r | |
81 | return 0\r | |
82 | else:\r | |
83 | with open(ecc_log, encoding='utf8') as output:\r | |
84 | ecc_output = output.readlines()\r | |
85 | for line in ecc_output:\r | |
86 | logging.error(line.strip())\r | |
87 | self.RemoveFile(ecc_log)\r | |
88 | tc.SetFailed("EccCheck failed for {0}".format(packagename), "Ecc detected issues")\r | |
89 | return 1\r | |
90 | \r | |
91 | def RevertCode(self) -> None:\r | |
92 | submoudle_params = "submodule update --init"\r | |
93 | RunCmd("git", submoudle_params)\r | |
94 | reset_params = "reset HEAD --hard"\r | |
95 | RunCmd("git", reset_params)\r | |
96 | \r | |
97 | def GetDiff(self, pkg: str) -> List[str]:\r | |
98 | return_buffer = StringIO()\r | |
99 | params = "diff --unified=0 origin/master HEAD"\r | |
100 | RunCmd("git", params, outstream=return_buffer)\r | |
101 | p = return_buffer.getvalue().strip()\r | |
102 | patch = p.split("\n")\r | |
103 | return_buffer.close()\r | |
104 | \r | |
105 | return patch\r | |
106 | \r | |
107 | def RemoveFile(self, file: str) -> None:\r | |
108 | if os.path.exists(file):\r | |
109 | os.remove(file)\r | |
110 | return\r | |
111 | \r | |
112 | def GetModifyDir(self, pkg: str) -> List[str]:\r | |
113 | return_buffer = StringIO()\r | |
114 | params = "diff --name-status" + ' HEAD' + ' origin/master'\r | |
115 | RunCmd("git", params, outstream=return_buffer)\r | |
116 | p1 = return_buffer.getvalue().strip()\r | |
117 | dir_list = p1.split("\n")\r | |
118 | return_buffer.close()\r | |
119 | modify_dir_list = []\r | |
120 | for modify_dir in dir_list:\r | |
121 | file_path = self.ReModifyFile.findall(modify_dir)\r | |
122 | if file_path:\r | |
123 | file_dir = os.path.dirname(file_path[0])\r | |
124 | else:\r | |
125 | continue\r | |
126 | if pkg in file_dir and file_dir != pkg:\r | |
127 | modify_dir_list.append('%s' % file_dir)\r | |
128 | else:\r | |
129 | continue\r | |
130 | \r | |
131 | modify_dir_list = list(set(modify_dir_list))\r | |
132 | return modify_dir_list\r | |
133 | \r | |
134 | def GetDiffRange(self, patch_diff: List[str], pkg: str, workingdir: str) -> Dict[str, List[Tuple[int, int]]]:\r | |
135 | IsDelete = True\r | |
136 | StartCheck = False\r | |
137 | range_directory: Dict[str, List[Tuple[int, int]]] = {}\r | |
138 | for line in patch_diff:\r | |
139 | modify_file = self.FindModifyFile.findall(line)\r | |
140 | if modify_file and pkg in modify_file[0] and not StartCheck and os.path.isfile(modify_file[0]):\r | |
141 | modify_file_comment_dic = self.GetCommentRange(modify_file[0], workingdir)\r | |
142 | IsDelete = False\r | |
143 | StartCheck = True\r | |
144 | modify_file_dic = modify_file[0]\r | |
145 | modify_file_dic = modify_file_dic.replace("/", os.sep)\r | |
146 | range_directory[modify_file_dic] = []\r | |
147 | elif line.startswith('--- '):\r | |
148 | StartCheck = False\r | |
149 | elif re.match(self.LineScopePattern, line, re.I) and not IsDelete and StartCheck:\r | |
150 | start_line = self.LineNumRange.search(line).group(1)\r | |
151 | line_range = self.LineNumRange.search(line).group(2)\r | |
152 | if not line_range:\r | |
153 | line_range = '1'\r | |
154 | range_directory[modify_file_dic].append((int(start_line), int(start_line) + int(line_range) - 1))\r | |
155 | for i in modify_file_comment_dic:\r | |
156 | if int(i[0]) <= int(start_line) <= int(i[1]):\r | |
157 | range_directory[modify_file_dic].append(i)\r | |
158 | return range_directory\r | |
159 | \r | |
160 | def GetCommentRange(self, modify_file: str, workingdir: str) -> List[Tuple[int, int]]:\r | |
161 | modify_file_path = os.path.join(workingdir, modify_file)\r | |
162 | with open(modify_file_path) as f:\r | |
163 | line_no = 1\r | |
164 | comment_range: List[Tuple[int, int]] = []\r | |
165 | Start = False\r | |
166 | for line in f:\r | |
167 | if line.startswith('/**'):\r | |
168 | start_no = line_no\r | |
169 | Start = True\r | |
170 | if line.startswith('**/') and Start:\r | |
171 | end_no = line_no\r | |
172 | Start = False\r | |
173 | comment_range.append((int(start_no), int(end_no)))\r | |
174 | line_no += 1\r | |
175 | \r | |
176 | if comment_range and comment_range[0][0] == 1:\r | |
177 | del comment_range[0]\r | |
178 | return comment_range\r | |
179 | \r | |
180 | def GenerateEccReport(self, modify_dir_list: List[str], ecc_diff_range: Dict[str, List[Tuple[int, int]]],\r | |
a050c599 | 181 | workspace_path: str, basetools_path: str) -> None:\r |
fbc9cb4c SZ |
182 | ecc_need = False\r |
183 | ecc_run = True\r | |
22fe311b PG |
184 | config = os.path.join(basetools_path, "Source", "Python", "Ecc", "config.ini")\r |
185 | exception = os.path.join(basetools_path, "Source", "Python", "Ecc", "exception.xml")\r | |
a050c599 | 186 | report = os.path.join(workspace_path, "Ecc.csv")\r |
fbc9cb4c | 187 | for modify_dir in modify_dir_list:\r |
a050c599 | 188 | target = os.path.join(workspace_path, modify_dir)\r |
fbc9cb4c SZ |
189 | logging.info('Run ECC tool for the commit in %s' % modify_dir)\r |
190 | ecc_need = True\r | |
191 | ecc_params = "-c {0} -e {1} -t {2} -r {3}".format(config, exception, target, report)\r | |
a050c599 | 192 | return_code = RunCmd("Ecc", ecc_params, workingdir=workspace_path)\r |
fbc9cb4c SZ |
193 | if return_code != 0:\r |
194 | ecc_run = False\r | |
195 | break\r | |
196 | if not ecc_run:\r | |
197 | logging.error('Fail to run ECC tool')\r | |
a050c599 | 198 | self.ParseEccReport(ecc_diff_range, workspace_path)\r |
fbc9cb4c SZ |
199 | \r |
200 | if not ecc_need:\r | |
201 | logging.info("Doesn't need run ECC check")\r | |
202 | \r | |
203 | revert_params = "checkout -- {}".format(exception)\r | |
204 | RunCmd("git", revert_params)\r | |
205 | return\r | |
206 | \r | |
a050c599 PG |
207 | def ParseEccReport(self, ecc_diff_range: Dict[str, List[Tuple[int, int]]], workspace_path: str) -> None:\r |
208 | ecc_log = os.path.join(workspace_path, "Ecc.log")\r | |
50672d26 | 209 | ecc_csv = os.path.join(workspace_path, "Ecc.csv")\r |
fbc9cb4c SZ |
210 | row_lines = []\r |
211 | ignore_error_code = self.GetIgnoreErrorCode()\r | |
50672d26 | 212 | if os.path.exists(ecc_csv):\r |
fbc9cb4c SZ |
213 | with open(ecc_csv) as csv_file:\r |
214 | reader = csv.reader(csv_file)\r | |
215 | for row in reader:\r | |
216 | for modify_file in ecc_diff_range:\r | |
217 | if modify_file in row[3]:\r | |
218 | for i in ecc_diff_range[modify_file]:\r | |
219 | line_no = int(row[4])\r | |
220 | if i[0] <= line_no <= i[1] and row[1] not in ignore_error_code:\r | |
221 | row[0] = '\nEFI coding style error'\r | |
222 | row[1] = 'Error code: ' + row[1]\r | |
223 | row[3] = 'file: ' + row[3]\r | |
224 | row[4] = 'Line number: ' + row[4]\r | |
225 | row_line = '\n *'.join(row)\r | |
226 | row_lines.append(row_line)\r | |
227 | break\r | |
228 | break\r | |
229 | if row_lines:\r | |
230 | self.ECC_PASS = False\r | |
231 | \r | |
232 | with open(ecc_log, 'a') as log:\r | |
233 | all_line = '\n'.join(row_lines)\r | |
234 | all_line = all_line + '\n'\r | |
235 | log.writelines(all_line)\r | |
236 | return\r | |
237 | \r | |
a050c599 | 238 | def ApplyConfig(self, pkgconfig: Dict[str, List[str]], workspace_path: str, basetools_path: str, pkg: str) -> None:\r |
fbc9cb4c SZ |
239 | if "IgnoreFiles" in pkgconfig:\r |
240 | for a in pkgconfig["IgnoreFiles"]:\r | |
a050c599 | 241 | a = os.path.join(workspace_path, pkg, a)\r |
fbc9cb4c SZ |
242 | a = a.replace(os.sep, "/")\r |
243 | \r | |
244 | logging.info("Ignoring Files {0}".format(a))\r | |
245 | if os.path.exists(a):\r | |
246 | if os.path.isfile(a):\r | |
247 | self.RemoveFile(a)\r | |
248 | elif os.path.isdir(a):\r | |
249 | shutil.rmtree(a)\r | |
250 | else:\r | |
251 | logging.error("EccCheck.IgnoreInf -> {0} not found in filesystem. Invalid ignore files".format(a))\r | |
252 | \r | |
253 | if "ExceptionList" in pkgconfig:\r | |
254 | exception_list = pkgconfig["ExceptionList"]\r | |
22fe311b | 255 | exception_xml = os.path.join(basetools_path, "Source", "Python", "Ecc", "exception.xml")\r |
fbc9cb4c SZ |
256 | try:\r |
257 | logging.info("Appending exceptions")\r | |
258 | self.AppendException(exception_list, exception_xml)\r | |
259 | except Exception as e:\r | |
260 | logging.error("Fail to apply exceptions")\r | |
261 | raise e\r | |
262 | return\r | |
263 | \r | |
264 | def AppendException(self, exception_list: List[str], exception_xml: str) -> None:\r | |
265 | error_code_list = exception_list[::2]\r | |
266 | keyword_list = exception_list[1::2]\r | |
267 | dom_tree = xml.dom.minidom.parse(exception_xml)\r | |
268 | root_node = dom_tree.documentElement\r | |
269 | for error_code, keyword in zip(error_code_list, keyword_list):\r | |
270 | customer_node = dom_tree.createElement("Exception")\r | |
271 | keyword_node = dom_tree.createElement("KeyWord")\r | |
272 | keyword_node_text_value = dom_tree.createTextNode(keyword)\r | |
273 | keyword_node.appendChild(keyword_node_text_value)\r | |
274 | customer_node.appendChild(keyword_node)\r | |
275 | error_code_node = dom_tree.createElement("ErrorID")\r | |
276 | error_code_text_value = dom_tree.createTextNode(error_code)\r | |
277 | error_code_node.appendChild(error_code_text_value)\r | |
278 | customer_node.appendChild(error_code_node)\r | |
279 | root_node.appendChild(customer_node)\r | |
280 | with open(exception_xml, 'w') as f:\r | |
281 | dom_tree.writexml(f, indent='', addindent='', newl='\n', encoding='UTF-8')\r | |
282 | return\r | |
283 | \r | |
284 | def GetIgnoreErrorCode(self) -> set:\r | |
285 | """\r | |
286 | Below are kinds of error code that are accurate in ecc scanning of edk2 level.\r | |
287 | But EccCheck plugin is partial scanning so they are always false positive issues.\r | |
288 | The mapping relationship of error code and error message is listed BaseTools/Sourc/Python/Ecc/EccToolError.py\r | |
289 | """\r | |
290 | ignore_error_code = {\r | |
291 | "10000",\r | |
292 | "10001",\r | |
293 | "10002",\r | |
294 | "10003",\r | |
295 | "10004",\r | |
296 | "10005",\r | |
297 | "10006",\r | |
298 | "10007",\r | |
299 | "10008",\r | |
300 | "10009",\r | |
301 | "10010",\r | |
302 | "10011",\r | |
303 | "10012",\r | |
304 | "10013",\r | |
305 | "10015",\r | |
306 | "10016",\r | |
307 | "10017",\r | |
308 | "10022",\r | |
309 | }\r | |
310 | return ignore_error_code\r |