/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ /* * Created on Oct 7, 2004 * * @author Fabio Zadrozny */ package org.python.pydev.debug.codecoverage; import java.io.File; import java.io.OutputStream; import java.util.Iterator; import java.util.List; import org.eclipse.core.resources.IContainer; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.python.pydev.core.docutils.StringUtils; import org.python.pydev.core.log.Log; import org.python.pydev.debug.core.PydevDebugPlugin; import org.python.pydev.debug.ui.launching.PythonRunnerConfig; import org.python.pydev.plugin.nature.PythonNature; import org.python.pydev.runners.UniversalRunner; import org.python.pydev.runners.UniversalRunner.AbstractRunner; import org.python.pydev.ui.filetypes.FileTypesPreferencesPage; import org.python.pydev.utils.PyFileListing; import org.python.pydev.utils.PyFileListing.PyFileInfo; import com.aptana.shared_core.io.FileUtils; import com.aptana.shared_core.io.ThreadStreamReader; import com.aptana.shared_core.string.FastStringBuffer; import com.aptana.shared_core.structure.Tuple; /** * This class is used to make the code coverage. * * It works in this way: when the user requests the coverage for the execution of a module, we create a python process and execute the * module using the code coverage module that is packed with pydev. * * Other options are: * - Erasing the results obtained; * - Getting the results when requested (cached in this class). * * @author Fabio Zadrozny */ public class PyCoverage { public CoverageCache cache = new CoverageCache(); /** * This method contacts the python server so that we get the information on the files that are below the directory passed as a parameter * and stores the information needed on the cache. * * @param file * should be the root folder from where we want cache info. */ public void refreshCoverageInfo(IContainer container, IProgressMonitor monitor) { cache.clear(); if (container == null) { return; } try { if (!container.exists()) { throw new RuntimeException("The directory passed: " + container + " no longer exists."); } File file = container.getLocation().toFile(); PyFileListing pyFilesBelow = new PyFileListing(); if (file.exists()) { pyFilesBelow = PyFileListing.getPyFilesBelow(file, monitor, true, false); } if (pyFilesBelow.getFoundPyFileInfos().size() == 0) { //no files return; } //add the folders to the cache boolean added = false; for (Iterator<File> it = pyFilesBelow.getFoundFolders().iterator(); it.hasNext();) { File f = it.next(); if (!added) { cache.addFolder(f); added = true; } else { cache.addFolder(f, f.getParentFile()); } } PythonNature nature = PythonNature.getPythonNature(container); if (nature == null) { throw new RuntimeException("The directory passed: " + container + " does not have an associated nature."); } AbstractRunner runner = UniversalRunner.getRunner(nature); //First, combine the results of the many runs we may have. Tuple<String, String> output = runner.runScriptAndGetOutput(PythonRunnerConfig.getCoverageScript(), new String[] { "combine" }, getCoverageDirLocation(), monitor); if (output.o1 != null && output.o1.length() > 0) { Log.logInfo(output.o1); } if (output.o2 != null && output.o2.length() > 0) { if (output.o2.startsWith("Coverage.py warning:")) { Log.logInfo(output.o2); } else { Log.log(output.o2); } } //we have to make a process to execute the script. it should look // like: //coverage.py -r [-m] FILE1 FILE2 ... //Report on the statement coverage for the given files. With the -m //option, show line numbers of the statements that weren't // executed. //python coverage.py -r -m files.... monitor.setTaskName("Starting shell to get info..."); monitor.worked(1); Process p = null; try { // Tuple<Process, String> tup = runner.createProcess( // PythonRunnerConfig.getCoverageScript(), new String[]{ // "-r", "-m", "--include", ".*"}, getCoverageDirLocation(), monitor); Tuple<Process, String> tup = runner.createProcess(PythonRunnerConfig.getCoverageScript(), new String[] { "--pydev-analyze" }, getCoverageDirLocation(), monitor); p = tup.o1; try { p.exitValue(); throw new RuntimeException("Some error happened... the process could not be created."); } catch (Exception e) { //that's ok } String files = ""; for (Iterator<PyFileInfo> iter = pyFilesBelow.getFoundPyFileInfos().iterator(); iter.hasNext();) { String fStr = iter.next().getFile().toString(); files += fStr + "|"; } files += "\r"; monitor.setTaskName("Writing to shell..."); //No need to synchronize as we'll waitFor() the process before getting the contents. ThreadStreamReader inputStream = new ThreadStreamReader(p.getInputStream(), false); inputStream.start(); ThreadStreamReader errorStream = new ThreadStreamReader(p.getErrorStream(), false); errorStream.start(); monitor.worked(1); OutputStream outputStream = p.getOutputStream(); outputStream.write(files.getBytes()); outputStream.close(); //We'll read something in the format below: //Name Stmts Miss Cover Missing //------------------------------------------------------------------------------------------------------- //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\__init__ 0 0 100% //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\a 10 3 70% 4-6 //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\hello 3 3 0% 1-4 //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\mod2\__init__ 5 5 0% 2-8 //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\mod2\hello2 33 33 0% 1-43 //------------------------------------------------------------------------------------------------------- //TOTAL 57 50 12% monitor.setTaskName("Waiting for process to finish..."); monitor.worked(1); while (true) { try { p.exitValue(); break; //process finished } catch (IllegalThreadStateException e) { //not finished } try { Thread.sleep(50); } catch (InterruptedException e) { //ignore } monitor.worked(1); if (monitor.isCanceled()) { try { p.destroy(); } catch (Exception e) { Log.log(e); } break; } } String stdOut = inputStream.getAndClearContents(); String stdErr = errorStream.getAndClearContents().trim(); if (stdErr.length() > 0) { Log.log(stdErr); } monitor.setTaskName("Getting coverage info...(please wait, this could take a while)"); monitor.worked(1); FastStringBuffer tempBuf = new FastStringBuffer(); for (String str : StringUtils.splitInLines(stdOut)) { analyzeReadLine(monitor, str.trim(), tempBuf); } monitor.setTaskName("Finished"); } catch (Exception e) { if (p != null) { p.destroy(); } Log.log(e); } } catch (Exception e1) { Log.log(e1); throw new RuntimeException(e1); } } /** * @param monitor * @param str * @param tempBuf */ private void analyzeReadLine(IProgressMonitor monitor, String str, FastStringBuffer tempBuf) { //The line we're interested in is something as //D:\workspaces\temp\test_workspace\pytesting1\src\mod1\a 10 3 70% 4-6, 18, 19 //with the last part (missing) optional. boolean added = false; List<String> strings = StringUtils.split(str, ' ', 5); String[] dottedValidSourceFiles = FileTypesPreferencesPage.getDottedValidSourceFiles(); File f = null; int nTokens = strings.size(); if (nTokens == 5 || nTokens == 4) { try { if (!strings.get(1).equalsIgnoreCase("stmts") && !strings.get(0).equalsIgnoreCase("total")) { //information in the format: D:\workspaces\temp\test_workspace\pytesting1\src\mod1\a 10 3 70% 4-6, 18 String fileStr = strings.get(0); boolean found = false; for (String ext : dottedValidSourceFiles) { if (fileStr.endsWith(ext)) { found = true; break; } } if (!found) { //Add the extension and see if it matches tempBuf.clear().append(fileStr); for (String ext : dottedValidSourceFiles) { f = new File(tempBuf.append(ext).toString()); if (f.exists()) { found = true; break; } tempBuf.deleteLastChars(ext.length()); } } if (!found) { return; } int stmts = Integer.parseInt(strings.get(1)); int miss = Integer.parseInt(strings.get(2)); if (nTokens == 4) { cache.addFile(f, f.getParentFile(), stmts, miss, ""); added = true; } else { String missing = strings.get(4); cache.addFile(f, f.getParentFile(), stmts, miss, missing); added = true; } String[] strs = f.toString().replaceAll("/", " ").replaceAll("\\\\", " ").split(" "); if (strs.length > 1) { monitor.setTaskName("Getting coverage info..." + strs[strs.length - 1]); } else { monitor.setTaskName("Getting coverage info..." + f.toString()); } monitor.worked(1); } } catch (RuntimeException e2) { //maybe there is something similar, but isn't quite the same, so, parse int could give us some problems... Log.log(IStatus.INFO, "Code-coverage: ignored line: " + str, null); } } //we may have gotten an error in the following format: //X:\coilib30\python\coilib\geom\Box3D.py exceptions.IndentationError: unindent does not match any outer indentation level (line // 97) //X:\coilib30\python\coilib\x3d\layers\cacherenderer.py exceptions.SyntaxError: invalid syntax (line 95) // //that is: file errorClass desc. if (added == false && f != null) { try { if (f.exists() && f.isFile()) { //this is probably an error... if (!f.getName().startsWith(".coverage")) { //System.out.println("Adding file:"+f); cache.addFile(f, f.getParentFile(), getError(strings)); } } } catch (Exception e) { Log.log(e); } } } /** * @param strings * @return string concatenating all but first elements from passed argument * separated by space */ private String getError(List<String> strings) { StringBuffer ret = new StringBuffer(); int len = strings.size(); for (int i = 1; i < len; i++) { ret.append(strings.get(i)).append(' '); } return ret.toString(); } /** * */ public void clearInfo() { cache.clear(); File dir = getCoverageDirLocation(); try { //Clear the files we created when running the coverages. FileUtils.clearTempFilesAt(dir, ".coverage."); } catch (Exception e) { Log.log(e); } try { //We also need to remove the file that consolidates all the info new File(dir, ".coverage").delete(); } catch (Exception e) { Log.log(e); } } private static PyCoverage pyCoverage; /** * @return Returns the pyCoverage. */ public static PyCoverage getPyCoverage() { if (pyCoverage == null) { pyCoverage = new PyCoverage(); } return pyCoverage; } public static File getCoverageDirLocation() { IPath stateLocation = PydevDebugPlugin.getDefault().getStateLocation(); stateLocation = stateLocation.append("coverage"); String loc = FileUtils.getFileAbsolutePath(stateLocation.toFile()); File dir = new File(loc); try { dir.mkdirs(); } catch (Exception e) { Log.log(e); } if (!dir.exists()) { throw new RuntimeException("The directory: " + loc + " could not be created."); } if (!dir.isDirectory()) { throw new RuntimeException("Expected the path: " + loc + " to be a directory."); } return dir; } /** * @return */ public static File getCoverageFileLocation() { return FileUtils.getTempFileAt(getCoverageDirLocation(), ".coverage."); } }