PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
main.py
Go to the documentation of this file.
1 
10 
11 
12 
13 import argparse
14 import os
15 import json
16 import time
17 import shutil
18 import subprocess
19 from Path.getPath import *
20 from Map.map import mapAPI
21 from multiprocessing import Pool
22 from multiprocessing import Manager
23 from Extract.getCall import getCallFunction
24 from Extract.pcresolveBridge import buildCallsiteLookup
25 from Preprocess.preprocess import codeProcess
26 from Repair.repair import repairTask,validateByRun
27 from Tool.tool import getAst,save2txt,loadConfig,removeParameter,buildRunCommand,resolveConfigFilePath,resolveConfigValuePath
28 from Tool.workspace import cleanupRunWorkspace,createRunWorkspace,exportRunReport,getRepoRoot,getRuntimePaths,workspaceCwd
29 from Change.changeAnalyze import isCompatible,addValueForAPI,updateSharedDict,querySharedDict,updateErrorLst
30 
31 
32 
38 def backwardTask(args):
39  ansDict={} #保存每个文件处理的情况
40  if len(args)==13:
41  projName,libName,file,currentVersion,currentEnv,targetVersion,targetEnv,runCommand,runPath,lock,sharedDict,coverSet,runtimePaths=args
42  pcresolveLookup=None
43  else:
44  projName,libName,file,currentVersion,currentEnv,targetVersion,targetEnv,runCommand,runPath,lock,sharedDict,coverSet,runtimePaths,pcresolveLookup=args
45  copyRoot=runtimePaths['copy_root']
46  dataDir=runtimePaths['data_dir']
47  reportDir=runtimePaths['report_dir']
48  # fileName=file.split('/')[-1][0:-3]
49  fileName = os.path.basename(file)[:-3]
50 
51  #step1:将源代码文件映射到Copy目录中
52  # tempLst=file.split('/')
53  normalized_file = file.replace('\\', '/')
54  tempLst = normalized_file.split('/')
55  pos=tempLst.index(projName)
56  realProjPath='/'.join(tempLst[0:pos+1])
57  fileRelativePath='/'.join(tempLst[pos:])
58  copyFile = os.path.join(copyRoot, *fileRelativePath.split('/'))
59  #step2:先把当前文件中指定的第三方库的API抽取出来
60  callAPIDict,_=getCallFunction(file,libName,realProjPath,pcresolveLookup=pcresolveLookup) #key是artifact id,value是结构化调用点记录
61  os.makedirs(dataDir, exist_ok=True)
62  with open(os.path.join(dataDir, f'{fileName}_callAPIDict.json'), 'w', encoding='utf-8') as fw:
63  json.dump(callAPIDict, fw, indent=4, ensure_ascii=False)
64  root=None
65  astError=None
66  try:
67  root=getAst(file) #获取当前文件的AST,便于修复使用
68  except Exception as e:
69  astError=e
70  invokedAPINum=len(callAPIDict)
71  errorLog = os.path.join(reportDir, f'{projName}_fixed_log.txt')
72  for key,record in callAPIDict.items():
73  errLst=[] #记录错误信息
74  ansDict[key]={}
75  callAPI=record['call_text']
76  lineNum=record['lineno']
77  formatAPI=record['format_api']
78  callKey=record['id']
79  ansDict[key]['Invoked API']=callAPI
80  ansDict[key]['Location']=f"At Line {lineNum} in {fileRelativePath}"
81 
82  if callKey not in coverSet:
83  ansDict[key]['Coverage']='No'
84  continue
85 
86  ansDict[key]['Coverage']='Yes'
87  formatAPI=removeParameter(formatAPI)
88  #step3:将项目中的API与库API进行匹配,获得参数定义
89  #首先判断一下这个API是否匹配过,若之前匹配过了,就不用再匹配了
90  with lock:
91  matchDict=querySharedDict(callKey,sharedDict) #当查询操作发生在更新操作之前,可能会查询失败
92  if len(matchDict)>0:
93  currentMatch=matchDict['current']
94  targetMatch=matchDict['target']
95  else:
96  currentMatch=mapAPI(callAPI,runCommand,runPath,formatAPI,projName,libName,copyFile,currentVersion,currentEnv,lock,errLst,callKey=callKey,runtimePaths=runtimePaths)
97  targetMatch=mapAPI(callAPI,runCommand,runPath,formatAPI,projName,libName,copyFile,targetVersion,targetEnv,lock,errLst,curr=0,callKey=callKey,runtimePaths=runtimePaths)
98  with lock:
99  updateSharedDict(callKey,currentMatch,targetMatch,sharedDict)#更新sharedDict
100 
101 
102  ansDict[key][f"Definition @{currentVersion} <{currentMatch['matchMethod']}>"]=str(currentMatch['match'])
103  ansDict[key][f"Definition @{targetVersion} <{targetMatch['matchMethod']}>"]=str(targetMatch['match'])
104 
105  #step4:变更分析,若不兼容则返回需要修复的操作
106  repairLst=isCompatible(currentMatch,targetMatch) #repairLst中每个元素都是tuple
107  if repairLst==None:
108  ansDict[key]['Compatible']="Unknown"
109  if len(errLst)>0:
110  errorMsg = f"Error occurred, please check the {projName}_fixed_log.txt"
111  with lock:
112  updateErrorLst(errorLog,errLst)
113  continue
114 
115  if len(repairLst)==0: #若返回修复字典的个数为零,则一定是兼容的
116  ansDict[key]['Compatible']='Yes'
117  else:
118  if root is None:
119  ansDict[key]['Compatible']='Unknown'
120  ansDict[key]['Repair <Unknown>']='AST parse failed'
121  errLst.append(f"{callAPI}, AST parse failed in {fileRelativePath}: {astError}\n")
122  else:
123  apiWithValue=addValueForAPI(callAPI,projName,runPath,runCommand,currentEnv,targetEnv,errLst,callKey=callKey,runtimePaths=runtimePaths) #apiWithValue为空表示添加参数失败
124  fixedAPI,compatibilityLabel,repairStatus=repairTask(root,callAPI,apiWithValue,projName,runPath,runCommand,repairLst,targetEnv,errLst,callKey=callKey,runtimePaths=runtimePaths)
125  if compatibilityLabel=='Compatible':
126  ansDict[key]['Compatible']='Yes'
127  else:
128  if compatibilityLabel=='Incompatible':
129  ansDict[key]['Compatible']='No'
130  else:
131  ansDict[key]['Compatible']='Unknown'
132 
133  if repairStatus=='Successful':
134  ansDict[key]['Repair <Successful>']=f"{fixedAPI}"
135  elif repairStatus=='Failed':
136  ansDict[key]['Repair <Failed>']=f"{fixedAPI}"
137  else:
138  ansDict[key]['Repair <Unknown>']=f"{fixedAPI}"
139 
140 
141  if len(errLst)>0:
142  errorMsg = f"Error occurred, please check the {projName}_fixed_log.txt"
143  with lock:
144  updateErrorLst(errorLog,errLst)
145 
146 
147  #将修改操作更新到代码源文件
148  # with open(f"{file.rsplit('/',1)[0]}/new_{fileName}.py",'w') as fw:
149  # repairCode=ast.unparse(root)
150  # fw.write(repairCode+'\n')
151  return ansDict,fileRelativePath,invokedAPINum
152 
153 
154 
155 
168 def backward(projPath,libName,currentVersion,currentEnv,targetVersion,targetEnv,runCommand,runPath,workspace,pcresolveLookup=None):
169  runtimePaths=getRuntimePaths(workspace)
170  copyRoot=runtimePaths['copy_root']
171  dataDir=runtimePaths['data_dir']
172  tempDir=runtimePaths['temp_dir']
173  reportDir=runtimePaths['report_dir']
174  pathObj=Path('DF')
175  pathObj.getPath(projPath)
176  filePath=[it for it in pathObj.path if it.endswith('py')] #保留项目中的.py文件
177  projName=os.path.basename(projPath)
178  errorLog = os.path.join(reportDir, f'{projName}_fixed_log.txt')
179  if os.path.exists(errorLog):
180  os.remove(errorLog)
181 
182  #先在起始版本中生成每个API的pkl
183  # cwd 自动适配:使用 subprocess cwd 参数替代 shell cd
184  if runPath and runPath not in runCommand:
185  cwd = os.path.join(copyRoot, projName, runPath)
186  else:
187  cwd = os.path.join(copyRoot, projName)
188  print('Running the project...')
189  cmd=buildRunCommand(runCommand,currentEnv)
190  createResult = subprocess.run(
191  cmd, cwd=cwd, capture_output=True, text=True, encoding='utf-8'
192  )
193  if createResult.returncode!=0:
194  print(f'Failure to generate PKL in current version')
195  print(createResult.stderr)
196  return False
197  print("Running complete")
198 
199  #生成pkl成功后,将项目恢复成原样,便于之后对其中某个API单独插桩
200  os.makedirs(tempDir,exist_ok=True)
201  tempProjPath=os.path.join(tempDir,projName)
202  if os.path.exists(tempProjPath):
203  shutil.rmtree(tempProjPath)
204  shutil.move(os.path.join(copyRoot, projName), tempProjPath)
205  shutil.move(os.path.join(copyRoot, f'bak_{projName}'), os.path.join(copyRoot, projName))
206  shutil.move(tempProjPath, os.path.join(copyRoot, f'bak_{projName}'))
207 
208 
209  #用PCResolve进行全项目API调用识别,结果在所有任务间复用
210  if pcresolveLookup is None:
211  pcresolveLookup=buildCallsiteLookup(projPath,libName)
212 
213  #这里用进程池同时处理多个任务,但对于torch库可能会报错RuntimeError:CUDA out or memory
214  #对数据库的读写需要加锁
215  coverSet=set()
216  coverSet_path = os.path.join(copyRoot, 'pkl', 'coverSet')
217  if os.path.exists(coverSet_path):
218  with open(coverSet_path, 'r', encoding='utf-8') as fr:
219  tempLst=fr.readlines()
220  for it in tempLst:
221  it=it.rstrip('\n').replace(' ','')
222  coverSet.add(it)
223  manager=Manager()
224  lock=manager.Lock() #创建一个共享锁
225  sharedDict=manager.dict() #创建一个共享字典
226  tasks=[(projName,libName,file,currentVersion,currentEnv,targetVersion,targetEnv,runCommand,runPath,lock,sharedDict,coverSet,runtimePaths,pcresolveLookup) for file in filePath]
227  pool=Pool(processes=1)
228  resultLst=pool.map(backwardTask,tasks)
229  pool.close() #关闭进程池,使其不再接受新的任务
230  pool.join() #等待进程池中所有的任务执行完,否则主进程可能继续往下执行提前结束,而导致部分任务没有执行完
231  save2txt(resultLst, libName, runCommand, os.path.join(reportDir, f'{projName}.txt'))
232  return True
233 
234 
235 
246 def run(config,cleanWorkspace=False):
247  repoRoot=getRepoRoot()
248  configPath=resolveConfigFilePath(config,repoRoot)
249 
250  #加载配置
251  projPath,runCommand,runPath,libName,currentVersion,targetVersion,currentEnv,targetEnv=loadConfig(configPath)
252  projPath=resolveConfigValuePath(repoRoot,projPath)
253  currentEnv=resolveConfigValuePath(repoRoot,currentEnv)
254  targetEnv=resolveConfigValuePath(repoRoot,targetEnv)
255 
256  workspace=createRunWorkspace(
257  repoRoot,
258  projPath,
259  runCommand,
260  runPath,
261  libName,
262  currentVersion,
263  targetVersion,
264  currentEnv,
265  targetEnv,
266  )
267  print(f"Run workspace: {workspace.workspace_root}")
268  print("Code preprocessing...")
269 
270  pcresolveLookup=buildCallsiteLookup(projPath,libName)
271 
272  with workspaceCwd(workspace.workspace_root):
273  #首先对代码进行预处理
274  codeProcess(projPath,runCommand,runPath,libName,workspace=workspace,pcresolveLookup=pcresolveLookup)
275  print("Code preprocess complete")
276 
277  #执行主逻辑
278  succeeded=backward(projPath,libName,currentVersion,currentEnv,targetVersion,targetEnv,runCommand,runPath,workspace=workspace,pcresolveLookup=pcresolveLookup)
279 
280  exportRunReport(workspace)
281  print(f"Report output: {workspace.report_root}")
282  if cleanWorkspace and succeeded:
283  cleanupRunWorkspace(workspace)
284  print(f"Run workspace removed: {workspace.run_root}")
285  return workspace
286 
287 
288 
290 def main():
291  parser=argparse.ArgumentParser(
292  description='Python API compatibility analysis and repair tool'
293  )
294  parser.add_argument(
295  '-cfg',
296  dest='config',
297  required=True,
298  help='Configuration file path or file name under Configure',
299  )
300  parser.add_argument(
301  '--clean-workspace',
302  action='store_true',
303  help='Remove the run workspace after successful report export',
304  )
305  args=parser.parse_args()
306 
307  start=time.time()
308 
309  run(args.config,cleanWorkspace=args.clean_workspace)
310 
311  end=time.time()
312  print(f"Total run time={int(end-start)}s")
313 
314 
315 if __name__=='__main__':
316  main()
def isCompatible(current, target)
Determine the compatibility of APIs from current and target versions 判断起始版本和目标版本API的兼容性
def updateErrorLst(errorLog, errorLst)
Save error messages 保存错误信息
def updateSharedDict(callAPI, currentDict, targetDict, sharedDict)
Update API mapping dictionary: add and revise 更新API映射字典: 添加和修改
def querySharedDict(callAPI, sharedDict)
Query API mapping dictionary 查询API映射字典
def addValueForAPI(callAPI, projName, runPath, runCommand, currentEnv, targetEnv, errLst, callKey, *runtimePaths)
Add values stored by pkl file for API parameters 为API参数添加保存至pkl文件中的值
def getCallFunction(filePath, libName, projPath=None, pcresolveLookup=None)
Extract all API calls from a given .py file 每次传进来一个.py文件,抽取所有的调用API.
Definition: getCall.py:205
Definition: main.py:1
def backwardTask(args)
One process handles one file 一个进程处理一个文件
Definition: main.py:38
def run(config, cleanWorkspace=False)
Run PCART with an isolated workspace 在隔离工作区中运行PCART.
Definition: main.py:246
def backward(projPath, libName, currentVersion, currentEnv, targetVersion, targetEnv, runCommand, runPath, workspace, pcresolveLookup=None)
Generate pkl files and perform detection and repair tasks 生成项目调用API的pkl文件以及执行检测与修复任务
Definition: main.py:168
def main()
Main function of PCART PCART主函数
Definition: main.py:290
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 buildCallsiteLookup(projPath, libName)
Build a CallsiteRecord lookup table from PCResolve analysis 从PCResolve分析结果构建CallsiteRecord查找表
def codeProcess(projPath, runCommand, runPath, libName, workspace, pcresolveLookup=None)
Code processing 代码预处理
Definition: preprocess.py:1094
def repairTask(root, callAPI, apiWithValue, projName, runPath, runCommand, repairLst, virtualEnv, errLst, callKey, *runtimePaths)
Task of repairing parameter compatibility issues 参数兼容性问题修复任务
Definition: repair.py:383
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 save2txt(lst, libName, runCommand, savePath)
Save PCART report 保存PCART报告
Definition: tool.py:533
def getAst(filePath, strFlag=0)
Get AST for code 将代码转化为Ast树
Definition: tool.py:173
def resolveConfigValuePath(repoRoot, path)
Resolve path value loaded from PCART config 解析PCART配置字段中的路径值
Definition: tool.py:657
def resolveConfigFilePath(config, repoRoot)
Resolve PCART config file path 解析PCART配置文件路径
Definition: tool.py:643
def loadConfig(configPath)
Load PCART's configuration file 加载PCART配置文件
Definition: tool.py:627
def getRepoRoot()
Get PCART repository root path 获取PCART仓库根目录路径
Definition: workspace.py:52
def cleanupRunWorkspace(workspace)
Remove one completed PCART run workspace 删除一次已完成的PCART运行工作区
Definition: workspace.py:264
def exportRunReport(workspace)
Export internal report files to user-visible report directory 将工作区内部报告导出到用户可见报告目录
Definition: workspace.py:239
def workspaceCwd(path)
Definition: workspace.py:225
def createRunWorkspace(repoRoot, projPath, runCommand, runPath, libName, currentVersion, targetVersion, currentEnv, targetEnv, commandId='cmd-001', timestamp=None)
Create an isolated workspace for one PCART run command 为一次PCART运行命令创建隔离工作区
Definition: workspace.py:125
def getRuntimePaths(workspace)
Return runtime artifact paths for the current execution 返回当前执行使用的运行产物路径
Definition: workspace.py:207