package ch.hsr.ifs.cdttesting.cdttest;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.ToolFactory;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompoundStatement;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTInitializerList;
import org.eclipse.cdt.core.formatter.CodeFormatter;
import org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModelUtil;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.ui.editor.CEditor;
import org.eclipse.cdt.internal.ui.util.ExternalEditorInput;
import org.eclipse.cdt.ui.testplugin.Accessor;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.NotEnabledException;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Event;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import ch.hsr.ifs.cdttesting.helpers.ExternalResourceHelper;
import ch.hsr.ifs.cdttesting.helpers.UIThreadSyncRunnable;
import ch.hsr.ifs.cdttesting.rts.junit4.RTSTestCases;
import ch.hsr.ifs.cdttesting.rts.junit4.RtsFileInfo;
import ch.hsr.ifs.cdttesting.rts.junit4.RtsTestSuite;
import ch.hsr.ifs.cdttesting.testsourcefile.TestSourceFile;
@SuppressWarnings("restriction")
@RunWith(RtsTestSuite.class)
public class CDTTestingTest extends CDTSourceFileTest {
public static final String NL = System.getProperty("line.separator");
private static final String INTROVIEW_ID = "org.eclipse.ui.internal.introview";
public CDTTestingTest() {
ExternalResourceHelper.copyPluginResourcesToTestingWorkspace(getClass());
}
private enum MatcherState {
skip, inTest, inSource, inExpectedResult
}
private static final String testRegex = "//!(.*)\\s*(\\w*)*$";
private static final String fileRegex = "//@(.*)\\s*(\\w*)*$";
private static final String resultRegex = "//=.*$";
protected static Map<String, ArrayList<TestSourceFile>> createTests(final BufferedReader inputReader)
throws Exception {
final Map<String, ArrayList<TestSourceFile>> testCases = new TreeMap<>();
String line;
ArrayList<TestSourceFile> files = new ArrayList<>();
TestSourceFile actFile = null;
MatcherState matcherState = MatcherState.skip;
String testName = null;
boolean beforeFirstTest = true;
while ((line = inputReader.readLine()) != null) {
if (lineMatchesBeginOfTest(line)) {
if (!beforeFirstTest) {
testCases.put(testName, files);
files = new ArrayList<>();
testName = null;
}
matcherState = MatcherState.inTest;
testName = getNameOfTest(line);
beforeFirstTest = false;
continue;
} else if (lineMatchesBeginOfResult(line)) {
matcherState = MatcherState.inExpectedResult;
if (actFile != null) {
actFile.initExpectedSource();
}
continue;
} else if (lineMatchesFileName(line)) {
matcherState = MatcherState.inSource;
actFile = new TestSourceFile(getFileName(line));
files.add(actFile);
continue;
}
switch (matcherState) {
case skip:
case inTest:
break;
case inSource:
if (actFile != null) {
actFile.addLineToSource(line);
}
break;
case inExpectedResult:
if (actFile != null) {
actFile.addLineToExpectedSource(line);
}
break;
}
}
testCases.put(testName, files);
return testCases;
}
private static String getFileName(final String line) {
final Matcher matcherBeginOfTest = createMatcherFromString(fileRegex, line);
if (matcherBeginOfTest.find()) {
return matcherBeginOfTest.group(1);
} else {
return null;
}
}
private static boolean lineMatchesBeginOfTest(final String line) {
return createMatcherFromString(testRegex, line).find();
}
private static boolean lineMatchesFileName(final String line) {
return createMatcherFromString(fileRegex, line).find();
}
private static Matcher createMatcherFromString(final String pattern, final String line) {
return Pattern.compile(pattern).matcher(line);
}
private static String getNameOfTest(final String line) {
final Matcher matcherBeginOfTest = createMatcherFromString(testRegex, line);
if (matcherBeginOfTest.find()) {
return matcherBeginOfTest.group(1);
} else {
return "Not Named";
}
}
private static boolean lineMatchesBeginOfResult(final String line) {
return createMatcherFromString(resultRegex, line).find();
}
protected void addReferencedProject(final String projectName, final String rtsFileName) throws Exception {
final RtsFileInfo rtsFileInfo = new RtsFileInfo(appendSubPackages(rtsFileName));
try {
final BufferedReader in = rtsFileInfo.getRtsFileReader();
final Map<String, ArrayList<TestSourceFile>> testCases = createTests(in);
if (testCases.isEmpty()) {
throw new Exception("Failed to add referenced project. RTS file " + rtsFileName
+ " does not contain any test-cases.");
} else if (testCases.size() > 1) {
throw new Exception("RTS files + " + rtsFileName
+ " which represents a referenced project must only contain a single test case.");
}
referencedProjectsToLoad.put(projectName, testCases.values().iterator().next());
} finally {
rtsFileInfo.closeReaderStream();
}
}
private String appendSubPackages(final String rtsFileName) {
final String testClassPackage = getClass().getPackage().getName();
return testClassPackage + "." + rtsFileName;
}
@RTSTestCases
public static Map<String, ArrayList<TestSourceFile>> testCases(final Class<? extends CDTTestingTest> testClass)
throws Exception {
final RtsFileInfo rtsFileInfo = new RtsFileInfo(testClass);
try {
final Map<String, ArrayList<TestSourceFile>> testCases = createTests(rtsFileInfo.getRtsFileReader());
return testCases;
} finally {
rtsFileInfo.closeReaderStream();
}
}
@Before
@Override
public void setUp() throws Exception {
super.setUp();
}
@After
@Override
public void tearDown() throws Exception {
FileHelper.clean();
super.tearDown();
}
protected void runEventLoop() {
while (getActiveWorkbenchWindow().getShell().getDisplay().readAndDispatch()) {
// do nothing
}
}
private IWorkbenchPage getActivePage() {
return getActiveWorkbenchWindow().getActivePage();
}
protected void closeEditorsWithoutSaving() throws Exception {
FileHelper.clean(); // make sure we are not holding any reference to the
// open IDocument anymore (otherwise, local changes
// in dirty editors
// won't get lost).
new UIThreadSyncRunnable() {
@Override
protected void runSave() throws Exception {
getActivePage().closeAllEditors(false);
}
}.runSyncOnUIThread();
}
protected void saveAllEditors() throws Exception {
new UIThreadSyncRunnable() {
@Override
protected void runSave() throws Exception {
getActivePage().saveAllEditors(false);
runEventLoop();
}
}.runSyncOnUIThread();
}
protected void openActiveFileInEditor() throws Exception {
openFileInEditor(activeFileName);
}
protected void openFileInEditor(final IFile file) throws Exception {
new UIThreadSyncRunnable() {
@Override
protected void runSave() throws Exception {
IDE.openEditor(getActivePage(), file);
setSelectionIfAvailable(file);
runEventLoop();
}
}.runSyncOnUIThread();
}
protected void openFileInEditor(final String fileName) throws Exception {
openFileInEditor(project.getFile(fileName));
}
public static void closeWelcomeScreen() throws Exception {
new UIThreadSyncRunnable() {
@Override
protected void runSave() throws Exception {
final IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
final IViewReference viewReference = page.findViewReference(INTROVIEW_ID);
page.hideView(viewReference);
}
}.runSyncOnUIThread();
}
protected void setSelectionIfAvailable(final IFile file) {
final TestSourceFile testSourceFile = fileMap.get(file.getProjectRelativePath().toString());
if (testSourceFile != null && testSourceFile.getSelection() != null) {
final ISelectionProvider selectionProvider = getActiveEditorSelectionProvider();
if (selectionProvider != null) {
selectionProvider.setSelection(testSourceFile.getSelection());
} else {
fail("no active editor found.");
}
}
}
protected AbstractTextEditor getActiveEditor() {
final IEditorPart editor = getActivePage().getActiveEditor();
return ((editor instanceof AbstractTextEditor) ? ((AbstractTextEditor) editor) : null);
}
protected ISelectionProvider getActiveEditorSelectionProvider() {
final AbstractTextEditor editor = getActiveEditor();
return (editor != null) ? editor.getSelectionProvider() : null;
}
protected void openExternalFileInEditor(final String absolutePath) throws Exception {
new UIThreadSyncRunnable() {
@Override
protected void runSave() throws Exception {
final ExternalEditorInput input = new ExternalEditorInput(FileHelper.stringToUri(absolutePath),
project);
IDE.openEditor(getActivePage(), input, "org.eclipse.cdt.ui.editor.CEditor", true);
runEventLoop();
}
}.runSyncOnUIThread();
}
protected IFile getActiveIFile() {
return getIFile(activeFileName);
}
protected IFile getIFile(final String relativePath) {
return project.getFile(relativePath);
}
protected IDocument getActiveDocument() throws Exception {
return getDocument(getActiveIFile());
}
protected IDocument getDocument(final IFile file) {
return FileHelper.getDocument(file);
}
protected IDocument getDocument(final String absoluteFilePath) {
final URI uri = FileHelper.stringToUri(absoluteFilePath);
return FileHelper.getDocument(uri);
}
protected String getCurrentSource() {
return getCurrentSource(activeFileName);
}
protected String getCurrentSource(final String relativeFilePath) {
final String absolutePath = makeProjectAbsolutePath(relativeFilePath);
return getCurrentSourceFromAbsolutePath(absolutePath);
}
protected String getCurrentSourceFromAbsolutePath(final String absoluteFilePath) {
return getDocument(absoluteFilePath).get();
}
@Override
protected String getExpectedSource() {
return getExpectedSource(activeFileName);
}
@Override
protected String getExpectedSource(final String relativeFilePath) {
final String absolutePath = makeProjectAbsolutePath(relativeFilePath, expectedProject);
return getExpectedSourceFromAbsolutePath(absolutePath);
}
protected String getExpectedSourceFromAbsolutePath(final String absoluteFilePath) {
final URI uri = FileHelper.stringToUri(absoluteFilePath);
final IDocument doc = FileHelper.getDocument(uri);
if (expectedCproject instanceof ICProject) {
final Map<String, Object> options = new HashMap<>(expectedCproject.getOptions(true));
try {
final ITranslationUnit tu = CoreModelUtil.findTranslationUnitForLocation(uri, expectedCproject);
options.put(DefaultCodeFormatterConstants.FORMATTER_TRANSLATION_UNIT, tu);
final CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
final TextEdit te = formatter.format(CodeFormatter.K_TRANSLATION_UNIT, absoluteFilePath, 0,
doc.getLength(), 0, NL);
te.apply(doc);
} catch (CModelException | MalformedTreeException | BadLocationException e) {
e.printStackTrace();
}
}
return doc.get();
}
protected void executeCommand(final String commandId)
throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException {
final IHandlerService hs = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getService(IHandlerService.class);
hs.executeCommand(commandId, null);
}
protected void insertUserTyping(final String text, int position)
throws MalformedTreeException, BadLocationException, IOException {
final String path = makeProjectAbsolutePath(activeFileName);
position = adaptExpectedOffsetOfCurrentDocument(path, position);
insertUserTyping(text, position, 0);
}
protected void insertUserTyping(final String text)
throws MalformedTreeException, BadLocationException, IOException {
final TextSelection selection = getCurrentEditorTextSelection();
if (selection != null) {
insertUserTyping(text, selection.getOffset(), selection.getLength());
return;
}
final int caretPos = getCurrentEditorCaretPosition();
insertUserTyping(text, caretPos, 0);
}
private TextSelection getCurrentEditorTextSelection() {
final ISelectionProvider selectionProvider = getActiveEditorSelectionProvider();
if (selectionProvider == null) {
return null;
}
final ISelection selection = selectionProvider.getSelection();
return (selection instanceof TextSelection) ? ((TextSelection) selection) : null;
}
private int getCurrentEditorCaretPosition() {
final ITextViewer viewer = (ITextViewer) getActiveEditor().getAdapter(ITextOperationTarget.class);
return JFaceTextUtil.getOffsetForCursorLocation(viewer);
}
protected void insertUserTyping(final String text, final int startPosition, final int length)
throws MalformedTreeException, BadLocationException, IOException {
final IDocument document = getDocument(getActiveIFile());
new ReplaceEdit(startPosition, length, text.replaceAll("\\n", NL)).apply(document);
}
/**
* This method can e.g. be used to jump to next linked-edit-group by sending
* c='\t' (tab)
*/
protected void invokeKeyEvent(final char c) {
final AbstractTextEditor abstractEditor = getActiveEditor();
if (!(abstractEditor instanceof CEditor)) {
fail("active editor is no ceditor.");
}
final StyledText textWidget = ((CEditor) abstractEditor).getViewer().getTextWidget();
assertNotNull(textWidget);
final Accessor accessor = new Accessor(textWidget, StyledText.class);
final Event event = new Event();
event.character = c;
event.keyCode = 0;
event.stateMask = 0;
accessor.invoke("handleKeyDown", new Object[] { event });
}
protected int adaptExpectedOffset(final String absoluteFilePath, final int offset) throws IOException {
if (NL.length() < 2) {
return offset;
}
final String expectedNewLine = "\n";
final String expectedSource = getTestSourceAbsolutePath(absoluteFilePath).replace(NL, expectedNewLine);
return offset + getOffsetAdaptionDelta(offset, expectedSource, expectedNewLine);
}
protected int adaptExpectedOffsetOfCurrentDocument(final String fileLocation, final int expectedOffset)
throws IOException {
if (NL.length() < 2) {
return expectedOffset;
}
final String expectedNewLine = "\n";
final String expectedSource = getCurrentSourceFromAbsolutePath(fileLocation).replace(NL, expectedNewLine);
return expectedOffset + getOffsetAdaptionDelta(expectedOffset, expectedSource, expectedNewLine);
}
protected int adaptActualOffset(final IASTFileLocation fileLocation) throws IOException {
return adaptActualOffset(fileLocation.getFileName(), fileLocation.getNodeOffset());
}
protected int adaptActualOffset(final String fileName, final int offset) throws IOException {
if (NL.length() < 2) {
return offset;
}
return offset - getOffsetAdaptionDelta(offset, getCurrentSourceFromAbsolutePath(fileName), NL);
}
private int getOffsetAdaptionDelta(final int offset, final String source, final String nl) throws IOException {
final int amountNewLines = countUpTo(source, nl, offset);
final int delta = (NL.length() - 1) * amountNewLines;
return delta;
}
protected Object adaptActualLength(final String fileName, final int length, final int offset) throws IOException {
if (NL.length() < 2) {
return length;
}
return length - getLengthAdaptionDelta(length, offset, getTestSourceAbsolutePath(fileName), NL);
}
private int getLengthAdaptionDelta(final int length, final int offset, final String source, final String nl) {
final int amountNewLines = countFromTo(source, nl, offset, offset + length);
final int delta = (NL.length() - 1) * amountNewLines;
return delta;
}
private int countFromTo(final String hayStack, final String needle, final int startAt, final int stopAt) {
int curOffset = startAt;
int matches = 0;
while ((curOffset = hayStack.indexOf(needle, curOffset)) < stopAt) {
if (curOffset == -1) {
break;
}
curOffset += needle.length();
matches++;
}
return matches;
}
private int countUpTo(final String hayStack, final String needle, final int stopAt) {
return countFromTo(hayStack, needle, 0, stopAt);
}
private String getTestSourceAbsolutePath(final String absoluteFilePath) throws IOException {
final IPath projectRelativePath = new Path(absoluteFilePath).makeRelativeTo(project.getLocation());
return getTestSource(projectRelativePath.toOSString());
}
/**
* Normalizes the passed {@link String} by removing all testeditor-comments,
* removing leading/trailing whitespace and line-breaks, replacing all
* remaining line-breaks by ↵ and reducing all groups of whitespace to a
* single space.
*
* @author tstauber
*
* @param in
* The {@link String} that should be normalized.
*
* @return A normalized copy of the parameter in.
**/
public static String normalize(final String in) {
//@formatter:off
return in.replaceAll("/\\*.*\\*/", "") //Remove all test-editor-comments
.replaceAll("(^((\\r?\\n)|\\s)*|((\\r?\\n)|\\s)*$)", "") //Remove all leading and trailing linebreaks/whitespace
.replaceAll("\\s*(\\r?\\n)+\\s*", "↵") //Replace all linebreaks with linebreak-symbol
.replaceAll("\\s+", " "); //Reduce all groups of whitespace to a single space
//@formatter:on
}
/**
* Performs an assertEquals on the passed parameters after using
* {@link normalize} on them.
*
* @author tstauber
*/
public static void assertEqualsNormalized(final String expected, final String actual) {
assertEquals(normalize(expected), normalize(actual));
}
/**
* Compares the {@link IASTTranslationUnit} from the code after the QuickFix
* was applied with the {@link IASTTranslationUnit} from the expected code.
* To use this method the flag {@code instantiateExpectedProject} has to be
* set to true.
*
* @author tstauber
*
*/
public void assertEqualsAST(final IASTTranslationUnit expectedAST, final IASTTranslationUnit currentAST) {
if (!instantiateExpectedProject) {
fail("To use the assertEqualsAST() method, the class must set instantiateExpectedProject=true ");
}
final Pair<ComparisonState, String[]> equals = equals(expectedAST, currentAST);
switch (equals.first) {
case EQUAL:
assertTrue(true);
break;
case DIFFERENT_AMOUNT_OF_CHILDREN:
assertEquals("Different amount of children. On line no: " + equals.second[2] + " -> ", equals.second[0],
equals.second[1]);
break;
case DIFFERENT_TYPE:
assertEquals("Different type. On line no: " + equals.second[2] + " -> ", equals.second[0],
equals.second[1]);
break;
case DIFFERENT_SIGNATURE:
assertEquals("Different normalized signatures. On line no: " + equals.second[2] + " -> ", equals.second[0],
equals.second[1]);
break;
}
}
/**
* Get the AST of the expected result
*
* @author tstauber
*
* @return The expected AST or null, if an exception occurred.
*/
public IASTTranslationUnit getExpectedAST() {
final String absoluteExpectedPath = makeProjectAbsolutePath(activeFileName, expectedProject);
final URI expectedURI = FileHelper.stringToUri(absoluteExpectedPath);
try {
return CoreModelUtil.findTranslationUnitForLocation(expectedURI, expectedCproject).getAST();
} catch (final CoreException ignored) {
return null;
}
}
/**
* Get the AST of the current result after the quickfix
*
* @author tstauber
*
* @return The current AST or null, if an exception occurred.
*/
public IASTTranslationUnit getCurrentAST() {
final String absoluteCurrentPath = makeProjectAbsolutePath(activeFileName);
final URI currentURI = FileHelper.stringToUri(absoluteCurrentPath);
try {
return CoreModelUtil.findTranslationUnitForLocation(currentURI, cproject).getAST();
} catch (final CoreException ignored) {
return null;
}
}
// TODO feature to enable failure if node of type CPPASTProblemId occurs
private Pair<ComparisonState, String[]> equals(final IASTNode expected, final IASTNode actual) {
final IASTNode[] lChilds = expected.getChildren();
final IASTNode[] rChilds = actual.getChildren();
final IASTFileLocation fileLocation = actual.getOriginalNode().getFileLocation();
final String lineNo = fileLocation == null ? "?" : String.valueOf(fileLocation.getStartingLineNumber());
final String[] description = new String[] { expected.getRawSignature(), actual.getRawSignature(), lineNo };
if (lChilds.length != rChilds.length) {
return new Pair<>(ComparisonState.DIFFERENT_AMOUNT_OF_CHILDREN, description);
} else if (!expected.getClass().equals(actual.getClass())) {
return new Pair<>(ComparisonState.DIFFERENT_TYPE, description);
} else if (lChilds.length != 0) {
for (int i = 0; i < lChilds.length; i++) {
final Pair<ComparisonState, String[]> childResult = equals(lChilds[i], rChilds[i]);
if (childResult.first != ComparisonState.EQUAL) {
return childResult;
}
}
} else if (expected instanceof ICPPASTCompoundStatement || expected instanceof ICPPASTInitializerList) {
return new Pair<>(ComparisonState.EQUAL, null);
} else if (!normalize(expected.getRawSignature()).equals(normalize(actual.getRawSignature()))) {
return new Pair<>(ComparisonState.DIFFERENT_SIGNATURE, description);
}
return new Pair<>(ComparisonState.EQUAL, null);
}
private enum ComparisonState {
DIFFERENT_TYPE, DIFFERENT_AMOUNT_OF_CHILDREN, DIFFERENT_SIGNATURE, EQUAL
}
private class Pair<T1, T2> {
public T1 first;
public T2 second;
public Pair(final T1 first, final T2 second) {
this.first = first;
this.second = second;
}
}
}