blob: 5fe92d9187dfb582701323033f819fbca6c02ebf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2016 Wind River Systems, Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Markus Schorn - initial API and implementation
* Andrew Ferguson (Symbian)
* Sergey Prigogin (Google)
*******************************************************************************/
package org.eclipse.cdt.core.testplugin.util;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ILinkage;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexFileLocation;
import org.eclipse.cdt.core.index.IndexLocationFactory;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.testplugin.CTestPlugin;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.junit.Assert;
import org.osgi.framework.Bundle;
/**
* Utilities for reading test source code from plug-in .java sources
*/
public class TestSourceReader {
private final Bundle bundle;
private final String srcRoot;
private final Class clazz;
private final int numSections;
/**
* @param bundle the bundle containing the source, if {@code null} can try to load using
* classpath (source folder has to be in the classpath for this to work)
* @param srcRoot the directory inside the bundle containing the packages
* @param clazz the name of the class containing the test
*/
public TestSourceReader(Bundle bundle, String srcRoot, Class clazz) {
this(bundle, srcRoot, clazz, 0);
}
/**
* @param bundle the bundle containing the source, if {@code null} can try to load using
* classpath (source folder has to be in the classpath for this to work)
* @param srcRoot the directory inside the bundle containing the packages
* @param clazz the name of the class containing the test
* @param numSections the number of comment sections preceding the named test to return.
* Pass zero to get all available sections.
*/
public TestSourceReader(Bundle bundle, String srcRoot, Class clazz, int numSections) {
this.bundle = bundle;
this.srcRoot = srcRoot;
this.clazz = clazz;
this.numSections = numSections;
}
public StringBuilder[] getContentsForTest(final String testName) throws IOException {
return getContentsForTest(bundle, srcRoot, clazz, testName, numSections);
}
public String readTaggedComment(String tag) throws IOException {
return readTaggedComment(bundle, tag, clazz, tag);
}
/**
* Returns an array of StringBuilder objects for each comment section found preceding the named
* test in the source code.
*
* @param bundle the bundle containing the source, if {@code null} can try to load using
* classpath (source folder has to be in the classpath for this to work)
* @param srcRoot the directory inside the bundle containing the packages
* @param clazz the name of the class containing the test
* @param testName the name of the test
* @param numSections the number of comment sections preceding the named test to return.
* Pass zero to get all available sections.
* @return an array of StringBuilder objects for each comment section found preceding the named
* test in the source code.
* @throws IOException
*/
public static StringBuilder[] getContentsForTest(Bundle bundle, String srcRoot, Class clazz, final String testName,
int numSections) throws IOException {
// Walk up the class inheritance chain until we find the test method.
try {
while (clazz.getMethod(testName).getDeclaringClass() != clazz) {
clazz = clazz.getSuperclass();
}
} catch (SecurityException e) {
Assert.fail(e.getMessage());
} catch (NoSuchMethodException e) {
Assert.fail(e.getMessage());
}
while (true) {
// Find and open the .java file for the class clazz.
String fqn = clazz.getName().replace('.', '/');
fqn = fqn.indexOf("$") == -1 ? fqn : fqn.substring(0, fqn.indexOf("$"));
String classFile = fqn + ".java";
IPath filePath = new Path(srcRoot + '/' + classFile);
InputStream in;
Class superclass = clazz.getSuperclass();
try {
if (bundle != null) {
in = FileLocator.openStream(bundle, filePath, false);
} else {
in = clazz.getResourceAsStream('/' + classFile);
}
} catch (IOException e) {
if (superclass == null || !superclass.getPackage().equals(clazz.getPackage())) {
throw e;
}
clazz = superclass;
continue;
}
BufferedReader br = new BufferedReader(new InputStreamReader(in));
try {
// Read the java file collecting comments until we encounter the test method.
List<StringBuilder> contents = new ArrayList<>();
StringBuilder content = new StringBuilder();
for (String line = br.readLine(); line != null; line = br.readLine()) {
line = line.replaceFirst("^\\s*", ""); // Replace leading whitespace, preserve trailing
// Trailing whitespace can be removed by editor/clean-up actions. To enforce whitespace
// at end of line, use ${whitspace_eol}, which will be removed, but cause the
// whitespace to the left of it to be preserved.
line = line.replace("${whitespace_eol}", "");
if (line.startsWith("//")) {
content.append(line.substring(2)).append('\n');
} else {
if (!line.startsWith("@") && content.length() > 0) {
contents.add(content);
if (numSections > 0 && contents.size() == numSections + 1)
contents.remove(0);
content = new StringBuilder();
}
if (line.length() > 0 && !contents.isEmpty()) {
int idx = line.indexOf(testName);
if (idx != -1 && !Character.isJavaIdentifierPart(line.charAt(idx + testName.length()))) {
return contents.toArray(new StringBuilder[contents.size()]);
}
if (!line.startsWith("@")) {
contents.clear();
}
}
}
}
} finally {
br.close();
}
if (superclass == null || !superclass.getPackage().equals(clazz.getPackage())) {
throw new IOException("Test data not found for " + clazz.getName() + "." + testName);
}
clazz = superclass;
}
}
/**
* Searches for the offset of the first occurrence of a string in a workspace file.
* @param lookfor string to be searched for
* @param fullPath full path of the workspace file
* @return the offset or -1
* @throws Exception
* @throws UnsupportedEncodingException
* @since 4.0
*/
public static int indexOfInFile(String lookfor, Path fullPath) throws Exception {
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
Reader reader = new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset()));
Assert.assertTrue(lookfor.indexOf('\n') == -1);
try {
int c = 0;
int offset = 0;
StringBuilder buf = new StringBuilder();
while ((c = reader.read()) >= 0) {
buf.append((char) c);
if (c == '\n') {
int idx = buf.indexOf(lookfor);
if (idx >= 0) {
return idx + offset;
}
offset += buf.length();
buf.setLength(0);
}
}
int idx = buf.indexOf(lookfor);
if (idx >= 0) {
return idx + offset;
}
return -1;
} finally {
reader.close();
}
}
public static int getLineNumber(int offset, Path fullPath) throws Exception {
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
Reader reader = new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset()));
try {
int line = 1;
for (int i = 0; i < offset; i++) {
int c = reader.read();
Assert.assertTrue(c >= 0);
if (c == '\n')
line++;
}
return line;
} finally {
reader.close();
}
}
/**
* Reads a section in comments form the source of the given class. The section
* is started with '// {tag}' and ends with the first line not started by '//'
* @since 4.0
*/
public static String readTaggedComment(Bundle bundle, String srcRoot, Class clazz, final String tag)
throws IOException {
IPath filePath = new Path(srcRoot + '/' + clazz.getName().replace('.', '/') + ".java");
InputStream in = FileLocator.openStream(bundle, filePath, false);
LineNumberReader reader = new LineNumberReader(new InputStreamReader(in));
boolean found = false;
final StringBuilder content = new StringBuilder();
try {
String line = reader.readLine();
while (line != null) {
line = line.trim();
if (line.startsWith("//")) {
line = line.substring(2);
if (found) {
content.append(line);
content.append('\n');
} else {
line = line.trim();
if (line.startsWith("{" + tag)) {
if (line.length() == tag.length() + 1
|| !Character.isJavaIdentifierPart(line.charAt(tag.length() + 1))) {
found = true;
}
}
}
} else if (found) {
break;
}
line = reader.readLine();
}
} finally {
reader.close();
}
Assert.assertTrue("Tag '" + tag + "' is not defined inside of '" + filePath + "'.", found);
return content.toString();
}
/**
* Creates a file with content at the given path inside the given container.
* If the file exists its content is replaced.
* @param container a container to create the file in
* @param filePath the path relative to the container to create the file at
* @param contents the content for the file
* @return a file object.
* @throws CoreException
* @since 4.0
*/
public static IFile createFile(final IContainer container, final IPath filePath, final CharSequence contents)
throws CoreException {
final IWorkspace ws = ResourcesPlugin.getWorkspace();
final IFile result[] = new IFile[1];
ws.run(new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor monitor) throws CoreException {
// Obtain file handle
IFile file = container.getFile(filePath);
InputStream stream;
try {
stream = new ByteArrayInputStream(contents.toString().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new CoreException(new Status(IStatus.ERROR, CTestPlugin.PLUGIN_ID, null, e));
}
// Create file input stream
if (file.exists()) {
long timestamp = file.getLocalTimeStamp();
file.setContents(stream, false, false, new NullProgressMonitor());
if (file.getLocalTimeStamp() == timestamp) {
file.setLocalTimeStamp(timestamp + 1000);
}
} else {
createFolders(file);
file.create(stream, true, new NullProgressMonitor());
}
result[0] = file;
}
private void createFolders(IResource res) throws CoreException {
IContainer container = res.getParent();
if (!container.exists() && container instanceof IFolder) {
createFolders(container);
((IFolder) container).create(true, true, new NullProgressMonitor());
}
}
}, null);
return result[0];
}
/**
* Creates a file with content at the given path inside the given container.
* If the file exists its content is replaced.
* @param container a container to create the file in
* @param filePath the path relative to the container to create the file at
* @param contents the content for the file
* @return a file object.
* @since 4.0
*/
public static IFile createFile(IContainer container, String filePath, String contents) throws CoreException {
return createFile(container, new Path(filePath), contents);
}
/**
* Waits until the given file is indexed. Fails if this does not happen within the
* given time.
* @param file
* @param maxmillis
* @throws Exception
* @since 4.0
*/
public static void waitUntilFileIsIndexed(IIndex index, IFile file, int maxmillis) throws Exception {
long fileTimestamp = file.getLocalTimeStamp();
IIndexFileLocation indexFileLocation = IndexLocationFactory.getWorkspaceIFL(file);
long endTime = System.currentTimeMillis() + maxmillis;
int timeLeft = maxmillis;
while (timeLeft >= 0) {
Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
index.acquireReadLock();
try {
IIndexFile[] files = index.getFiles(ILinkage.CPP_LINKAGE_ID, indexFileLocation);
if (files.length > 0 && areAllFilesNotOlderThan(files, fileTimestamp)) {
Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
return;
}
files = index.getFiles(ILinkage.C_LINKAGE_ID, indexFileLocation);
if (files.length > 0 && areAllFilesNotOlderThan(files, fileTimestamp)) {
Assert.assertTrue(CCorePlugin.getIndexManager().joinIndexer(timeLeft, new NullProgressMonitor()));
return;
}
} finally {
index.releaseReadLock();
}
Thread.sleep(50);
timeLeft = (int) (endTime - System.currentTimeMillis());
}
Assert.fail("Indexing of " + file.getFullPath() + " did not complete in " + maxmillis / 1000. + " sec");
}
private static boolean areAllFilesNotOlderThan(IIndexFile[] files, long timestamp) throws CoreException {
for (IIndexFile file : files) {
if (file.getTimestamp() < timestamp) {
return false;
}
}
return true;
}
public static IASTTranslationUnit createIndexBasedAST(IIndex index, ICProject project, IFile file)
throws CModelException, CoreException {
ICElement elem = project.findElement(file.getFullPath());
if (elem instanceof ITranslationUnit) {
ITranslationUnit tu = (ITranslationUnit) elem;
return tu.getAST(index, ITranslationUnit.AST_SKIP_INDEXED_HEADERS);
}
Assert.fail("Could not create AST for " + file.getFullPath());
return null;
}
}