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