PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
map.py
Go to the documentation of this file.
1 
13 
14 
15 
16 import os
17 import json
18 import shutil
19 import subprocess
20 from Map.fuzzyMatch import *
21 from Load.loadData import loadLib
22 from Tool.tool import removeParameter,getFileName,resolvePythonExecutable,buildRunCommand,getArtifactHash,getArtifactDisplayName
23 from Extract.getCall import getCallFunction
24 from Preprocess.preprocess import addDictSingle
25 
26 
27 
28 
34 def isAlias(callApi,assignDict):
35  capilst=callApi.split('.')
36  candidate={}
37  for k in assignDict:
38  keylst=k.split('.')
39  if capilst[-1]==keylst[-1]:
40  candidate[k]=len(set(capilst)&set(keylst))/len(keylst)
41  if len(candidate)>0:
42  ansKey=sorted(candidate,key=lambda i:candidate[i],reverse=True)[0]
43  realName=assignDict[ansKey]
44  return realName
45  return None
46 
47 
48 
49 
57 def fuzzymatch(formatAPI,libName,version,builtinFlag): #callAPIDict是传入传出参数
58  libAPIs,assignDict,libAPIIns=loadLib(libName,version)
59  Fuzz=fuzzyMatch()
60  if builtinFlag and len(libAPIIns)>0:
61  ans=Fuzz.fmatch(formatAPI,libAPIIns) #只从.pyi文件里找,但如果没有注释的话,也不一定能找到,
62  else:
63  ans=Fuzz.fmatch(formatAPI,libAPIs)
64 
65  if len(ans)==0 and not builtinFlag:
66  aliasName=Fuzz.alias
67  realName=isAlias(aliasName,assignDict) #检查是否是别名,是的话就将别名还原成真名,然后再进行模糊匹配
68  if realName is not None:
69  ans=Fuzz.fmatch(realName,libAPIs)
70 
71  #对匹配出的结果按照不同的函数名进行分类,得到一个形式为{同名:[重载]}字典
72  ansDict={}
73  for it in ans: #ans是模糊得到的结果,当然也可能为空
74  pos=it.find('(')
75  if pos!=-1:
76  apiName=it[0:pos]
77  parameters=it[pos:]
78  if removeParameter(it)==formatAPI:#若和formatAPI完全相等,则说明匹配的结果是唯一且正确
79  return {apiName:[parameters]}
80 
81  if apiName not in ansDict:
82  ansDict[apiName]=[] #初始化字典,把同一个API的不同重载放到一起
83  ansDict[apiName].append(parameters)
84  return ansDict
85 
86 
87 
97 def saveDynamicMatchSnapshot(pklKey,version,curr,dynamicMatchDict,pklFile=None,*,runtimePaths):
98  if not isinstance(dynamicMatchDict,dict):
99  return
100  dataDir=runtimePaths['data_dir']
101  phase='current' if curr else 'target'
102  # getFileName(...,'.json')会处理Windows非法文件名字符,再去掉扩展名用于拼接快照文件名
103  safeKey=getFileName(pklKey,'.json')[:-5]
104  safeVersion=str(version).replace(os.sep,'_').replace('/','_').replace('\\','_')
105  snapshotDict=dict(dynamicMatchDict)
106  # 保留current/target最终动态匹配结果,原dynamicMatch.json仍作为子进程通信文件
107  snapshotDict['_pcart']={
108  'phase': phase,
109  'version': str(version),
110  'callKey': pklKey,
111  'artifact': pklKey,
112  'artifactHash': getArtifactHash(pklKey),
113  'debugName': getArtifactDisplayName(pklKey),
114  }
115  if pklFile is not None:
116  snapshotDict['_pcart']['pklFile']=pklFile
117  os.makedirs(dataDir,exist_ok=True)
118  fileName=f"{phase}_{safeKey}_{safeVersion}_dynamicMatch.json"
119  with open(os.path.join(dataDir,fileName),'w',encoding='UTF-8') as fw:
120  json.dump(snapshotDict,fw,indent=4,ensure_ascii=False)
121 
122 
123 
129 def hasSaveFailedManifest(pklKey,*,runtimePaths):
130  copyRoot=runtimePaths['copy_root']
131  manifestPath=os.path.join(copyRoot,'pkl',getFileName(pklKey,'.manifest.json'))
132  if not os.path.exists(manifestPath):
133  return False
134  try:
135  with open(manifestPath,'r',encoding='UTF-8') as fr:
136  manifest=json.load(fr)
137  except Exception:
138  return False
139  if not manifest.get('covered'):
140  return False
141  for candidate in manifest.get('candidates',[]):
142  if candidate.get('status')=='save_failed':
143  return True
144  return False
145 
146 
147 
148 
164 def dynamicMatch(callAPI,runCommand,runPath,projName,copyFile,version,virtualEnv,lock,errLst,curr=1,*,callKey,runtimePaths):
165  # pythonPath=f"{virtualEnv}/bin/python" #先指定python解释器的路径
166  pythonPath = resolvePythonExecutable(virtualEnv)
167  copyRoot=runtimePaths['copy_root']
168  dynamicRoot=runtimePaths['dynamic_root']
169  dataDir=runtimePaths['data_dir']
170  pklKey = callKey
171  pklFile=getFileName(pklKey,'.pkl')
172  # withitem调用优先尝试运行时对象,其次尝试还原表达式,最后尝试通用pkl候选
173  pklCandidateFiles=[pklFile[:-4]+'__object.pkl',pklFile[:-4]+'__expr.pkl',pklFile]
174 
175  #当runPath不在runCommand中时,需要切换到运行文件所在的目录执行命令
176  #而文件操作的相对路径就是相对于命令执行的路径
177  if runPath and runPath not in runCommand:
178  dynamic_cwd = os.path.join(dynamicRoot, projName, runPath)
179  dynamic_script = 'dynamicMatch.py'
180  else:
181  dynamic_cwd = os.path.join(dynamicRoot, projName)
182  dynamic_script = os.path.join(runPath, 'dynamicMatch.py') if runPath else 'dynamicMatch.py'
183  lastResult = None
184  lastDynamicMatchDict = None
185  existingPklFiles=[file for file in pklCandidateFiles if os.path.exists(os.path.join(copyRoot,'pkl',file))]
186  if not existingPklFiles:
187  if curr or not hasSaveFailedManifest(pklKey,runtimePaths=runtimePaths):
188  return False
189  lastResult=subprocess.CompletedProcess([],1,'','manifest save_failed')
190  else:
191  for candidatePklFile in existingPklFiles:
192  # 某个候选返回nullptr时继续尝试下一个候选,避免可inspect调用被提前判为static
193  pkl_arg = os.path.join(copyRoot, 'pkl', candidatePklFile)
194  matchResult = subprocess.run(
195  [pythonPath, dynamic_script, pkl_arg, callAPI, dataDir, pklKey],
196  cwd=dynamic_cwd, capture_output=True, text=True, encoding='utf-8'
197  )
198  lastResult = matchResult
199  if matchResult.returncode == 0:
200  fileName=getFileName(pklKey,'_dynamicMatch.json')
201  matchJsonPath=os.path.join(dataDir,fileName)
202  with open(matchJsonPath,'r',encoding='UTF-8') as fr:
203  try:
204  dynamicMatchDict=json.load(fr)
205  except Exception as e:
206  dynamicMatchDict=None
207  print(f"json load {matchJsonPath} failed: {e}\n")
208  lastDynamicMatchDict = dynamicMatchDict
209  if dynamicMatchDict and dynamicMatchDict.get('match') != 'nullptr':
210  saveDynamicMatchSnapshot(pklKey,version,curr,dynamicMatchDict,candidatePklFile,runtimePaths=runtimePaths)
211  return dynamicMatchDict
212  continue
213  continue
214 
215  if lastResult is None:
216  return False
217 
218  stdout = lastResult.stdout
219  stderr = lastResult.stderr
220  matchResult = lastResult
221 
222  if matchResult.returncode!=0:
223  if curr:
224  errLst.append(f"{callAPI}, Failed to load pkl in current version{version}: {matchResult.stderr}\n")
225  return False
226 
227  #若在新版本中无法加载旧版本的pkl文件,则尝试在新版本中重新生成
228  #什么情况下不需要在目标版本重新生成?什么情况下需要在mu
229  # elif 'Ran out of input' not in matchResult.stderr:
230  elif stderr and 'Ran out of input' not in stderr:
231  loadError=f"{callAPI}, Failed to load pkl in target version{version}: {matchResult.stderr}\n"
232  with lock:
233  shutil.copy2(copyFile,f"{copyFile}.bak")
234  addDictSingle(callAPI,copyFile,pklKey) #添加字典并运行,在当前文件中添加字典,在运行文件中
235  regeneratedPklFiles=[]
236  if runPath and runPath not in runCommand:
237  copy_cwd = os.path.join(copyRoot, projName, runPath)
238  else:
239  copy_cwd = os.path.join(copyRoot, projName)
240  # generateResult=subprocess.run(command,shell=True,executable='/bin/bash',stderr=subprocess.PIPE,text=True)
241  cmd=buildRunCommand(runCommand,virtualEnv)
242  generateResult = subprocess.run(
243  cmd, cwd=copy_cwd, capture_output=True, text=True, encoding='utf-8'
244  )
245  if generateResult.returncode==0:
246  # target环境重生成时保留object/expr候选顺序,避免退回混合老格式pkl
247  regeneratedCandidates=[
248  ('paraValue__object.pkl','new_'+pklFile[:-4]+'__object.pkl'),
249  ('paraValue__expr.pkl','new_'+pklFile[:-4]+'__expr.pkl'),
250  ('paraValue.pkl','new_'+pklFile),
251  ]
252  for sourceName,targetName in regeneratedCandidates:
253  sourcePath=os.path.join(copyRoot,'pkl',sourceName)
254  if os.path.exists(sourcePath):
255  targetPath=os.path.join(copyRoot,'pkl',targetName)
256  os.replace(sourcePath,targetPath)
257  regeneratedPklFiles.append(targetName)
258 
259  #插桩完后,再将备份后的文件进行还原
260  os.remove(copyFile)
261  shutil.move(f'{copyFile}.bak',copyFile)
262 
263  if generateResult.returncode!=0:
264  errLst.append(loadError)
265  errLst.append(f'[{version}]{callAPI}, generate new pkl failed: {generateResult.stderr}\n')
266  return False
267  else:
268  if not regeneratedPklFiles:
269  errLst.append(f'[{version}]{callAPI}, generate new pkl failed: no pkl file generated\n')
270  return False
271  for regeneratedPklFile in regeneratedPklFiles:
272  pkl_arg = os.path.join(copyRoot, 'pkl', regeneratedPklFile)
273  matchResult = subprocess.run(
274  [pythonPath, dynamic_script, pkl_arg, callAPI, dataDir, pklKey],
275  cwd=dynamic_cwd, capture_output=True, text=True, encoding='utf-8'
276  )
277  if matchResult.returncode!=0:
278  continue
279  fileName=getFileName(pklKey,'_dynamicMatch.json')
280  matchJsonPath=os.path.join(dataDir,fileName)
281  with open(matchJsonPath,'r',encoding='UTF-8') as fr:
282  try:
283  dynamicMatchDict=json.load(fr)
284  except Exception as e:
285  dynamicMatchDict=None
286  print(f"json load {matchJsonPath} failed: {e}\n")
287  if dynamicMatchDict and dynamicMatchDict.get('match') != 'nullptr':
288  saveDynamicMatchSnapshot(pklKey,version,curr,dynamicMatchDict,regeneratedPklFile,runtimePaths=runtimePaths)
289  return dynamicMatchDict
290  lastDynamicMatchDict=dynamicMatchDict
291  if lastDynamicMatchDict:
292  saveDynamicMatchSnapshot(pklKey,version,curr,lastDynamicMatchDict,runtimePaths=runtimePaths)
293  return lastDynamicMatchDict
294  errLst.append(f"[{version}]{callAPI}, load new pkl failed: {matchResult.stderr}\n")
295  return False
296 
297  else:
298  return False
299 
300  else:
301  if lastDynamicMatchDict:
302  saveDynamicMatchSnapshot(pklKey,version,curr,lastDynamicMatchDict,runtimePaths=runtimePaths)
303  return lastDynamicMatchDict
304 
305 
306 
307 
328 def mapAPI(callAPI,runCommand,runPath,formatAPI,projName,libName,copyFile,version,virtualEnv,lock,errLst,curr=1,*,callKey,runtimePaths):
329  dynamicMatchDict=dynamicMatch(callAPI,runCommand,runPath,projName,copyFile,version,virtualEnv,lock,errLst,curr,callKey=callKey,runtimePaths=runtimePaths)
330  ans={}
331  ans['format']=formatAPI
332  if dynamicMatchDict!=False: #若动态匹配成功,还要对动态匹配的结果进行检查
333  result=dynamicMatchDict['match']
334  ans['error']=dynamicMatchDict['error']
335  if 'internalPath' in dynamicMatchDict:
336  ans['internalPath']=dynamicMatchDict['internalPath']
337  else:
338  ans['internalPath']=formatAPI
339 
340  if 'builtin' in dynamicMatchDict['error']: #这里的内置不一定是库的内置,有可能是python内置,如何区分?
341  ans['match']=fuzzymatch(formatAPI,libName,version,1) #目前只发现pytorch中把内置记录到了.pyi中
342  ans['matchMethod']='static'
343  elif result=='nullptr': #若inspect失败
344  ans['match']=fuzzymatch(formatAPI,libName,version,0)
345  ans['matchMethod']='static'
346  else:
347  ans['match']=result
348  ans['matchMethod']='dynamic'
349  else:
350  ans['match']=fuzzymatch(formatAPI,libName,version,0)
351  ans['matchMethod']='static'
352 
353  return ans
Provide class definitions for statically mapping API parameter definitions.
Definition: fuzzyMatch.py:1
def loadLib(libName, version)
Load the extracted definitions of lib APIs for static signature mapping 把之前抽取出来的库API加载到字典中,在项目API与库AP...
Definition: loadData.py:30
def hasSaveFailedManifest(pklKey, *runtimePaths)
Check whether a callsite candidate manifest records pkl save failure 检查调用点候选清单是否记录了pkl保存失败
Definition: map.py:129
def saveDynamicMatchSnapshot(pklKey, version, curr, dynamicMatchDict, pklFile=None, *runtimePaths)
Save dynamic match result snapshot 保存动态匹配结果快照
Definition: map.py:97
def mapAPI(callAPI, runCommand, runPath, formatAPI, projName, libName, copyFile, version, virtualEnv, lock, errLst, curr=1, *callKey, runtimePaths)
Construct the mapping between the invoked API and the lib API to obtain its signature 建立invoked API与 ...
Definition: map.py:328
def isAlias(callApi, assignDict)
Check whether the last name in an API call is an alias 判断一个callAPI最后一个名字是否为库中的别名
Definition: map.py:34
def fuzzymatch(formatAPI, libName, version, builtinFlag)
Static mapping of API signatures API签名静态匹配
Definition: map.py:57
def dynamicMatch(callAPI, runCommand, runPath, projName, copyFile, version, virtualEnv, lock, errLst, curr=1, *callKey, runtimePaths)
Dynamic mapping of API signatures API签名动态匹配
Definition: map.py:164
def addDictSingle(callAPI, filePath, callKey)
Code instrumentation for a single API call within a source file 源文件单个API调用代码插桩
Definition: preprocess.py:436
def buildRunCommand(runCommand, envPath)
Build subprocess argv for project run command 为被测项目运行命令构造subprocess argv.
Definition: tool.py:779
def removeParameter(s, flag=0)
Remove parameter(s) from API call string 去掉API中的参数部分
Definition: tool.py:210
def getArtifactDisplayName(artifactId)
Return readable artifact display name 返回可读运行产物展示名
Definition: tool.py:45
def getFileName(fileName, extension)
Normalize file name 给文件取名字
Definition: tool.py:479
def resolvePythonExecutable(envPath)
Resolve Python executable from a virtual environment root 从虚拟环境根目录解析 Python 解释器路径
Definition: tool.py:856
def getArtifactHash(artifactId)
Return artifact hash from an artifact id 从运行产物id中提取hash.
Definition: tool.py:33