/*
* Copyright (C) 2014-2017 the original authors or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.sarl.lang.ui.tests.quickfix;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import javax.inject.Inject;
import org.arakhne.afc.text.TextUtil;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextStore;
import org.eclipse.jface.text.Region;
import org.eclipse.xtext.resource.IFragmentProvider;
import org.eclipse.xtext.resource.SaveOptions;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.model.IXtextDocument;
import org.eclipse.xtext.ui.editor.model.IXtextDocumentContentObserver;
import org.eclipse.xtext.ui.editor.model.IXtextModelListener;
import org.eclipse.xtext.ui.editor.model.edit.IModification;
import org.eclipse.xtext.ui.editor.model.edit.IModificationContext;
import org.eclipse.xtext.ui.editor.quickfix.IssueResolution;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;
import org.eclipse.xtext.validation.Issue;
import org.junit.ComparisonFailure;
import io.sarl.lang.sarl.SarlScript;
import io.sarl.lang.ui.quickfix.SARLQuickfixProvider;
import io.sarl.tests.api.AbstractSarlUiTest;
/** Abstract implementation for the quick fix tests.
*
* @author $Author: sgalland$
* @version $Name$ $Revision$ $Date$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
public abstract class AbstractSARLQuickfixTest extends AbstractSarlUiTest {
@Inject
private SARLQuickfixProvider quickfixProvider;
/** Create an object that permits to test the details of a quick fix.
*
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @return the quick fixes for assertions.
*/
protected QuickFixAsserts getQuickFixAsserts(
String issueCode,
String invalidCode) {
return getQuickFixAsserts(issueCode, invalidCode, true);
}
/** Create an object that permits to test the details of a quick fix.
*
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @param failOnErrorInCode - indicates if this function fails on error in the code.
* @return the quick fixes for assertions.
*/
protected QuickFixAsserts getQuickFixAsserts(
String issueCode,
String invalidCode,
boolean failOnErrorInCode) {
try {
String baseName = issueCode.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$//$NON-NLS-2$
String packageName = baseName.toLowerCase();
baseName = baseName.substring(0, 1).toUpperCase() + baseName.substring(1);
String filename = helper().generateFilename(
"io", "sarl", "tests", "quickfix", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
packageName, baseName);
SarlScript script = helper().sarlScript(filename, invalidCode, failOnErrorInCode);
Thread.yield();
assertNotNull(script);
Resource scriptResource = script.eResource();
Thread.yield();
List<Issue> issues = issues(script);
StringBuilder issueLabels = new StringBuilder();
Iterator<Issue> issueIterator = issues.iterator();
List<IssueResolution> resolutions = new ArrayList<>();
Issue issue = null;
while (issueIterator.hasNext()) {
Issue nextIssue = issueIterator.next();
if (issueLabels.length() > 0) {
issueLabels.append(","); //$NON-NLS-1$
issueLabels.append(getLineSeparator());
}
issueLabels.append(nextIssue.getCode());
issueLabels.append(" - \""); //$NON-NLS-1$
issueLabels.append(Strings.convertToJavaString(nextIssue.getMessage()));
issueLabels.append("\""); //$NON-NLS-1$
if (issueCode.equals(nextIssue.getCode())) {
issue = nextIssue;
resolutions.addAll(this.quickfixProvider.getResolutions(issue));
}
}
if (issueLabels.length() > 0) {
issueLabels.append("."); //$NON-NLS-1$
issueLabels.append(getLineSeparator());
}
if (issue == null) {
fail("The issue '" + issueCode //$NON-NLS-1$
+ "' was not found.\nAvailable issues are: " //$NON-NLS-1$
+ issueLabels.toString());
return null;
}
if (resolutions.isEmpty()) {
fail("No resolution found for the issue '" + issueCode //$NON-NLS-1$
+ "'."); //$NON-NLS-1$
return null;
}
QuickFixAsserts asserts = new QuickFixAsserts(
issueCode,
invalidCode,
scriptResource,
resolutions);
return asserts;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** Assert the quick fix is changing the document in the expected way.
*
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolution - the expected code after fixing.
* @param expectedResolution2 - the expected code after fixing.
* @return the matching resolved text.
*/
protected String assertQuickFix(
String issueCode,
String invalidCode,
String expectedLabel,
String expectedResolution,
String... expectedResolution2) {
return assertQuickFix(false, issueCode, invalidCode, expectedLabel, expectedResolution,
expectedResolution2);
}
/** Assert the quick fix is changing the document in the expected way.
*
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolution - the expected code after fixing.
* @param expectedResolution2 - the expected code after fixing.
* @return the matching resolved text.
*/
protected String assertQuickFixWithErrors(
String issueCode,
String invalidCode,
String expectedLabel,
String expectedResolution,
String... expectedResolution2) {
return assertQuickFixWithErrors(false, issueCode, invalidCode, expectedLabel, expectedResolution,
expectedResolution2);
}
/** Assert the quick fix is changing the document in the expected way.
*
* @param changeOriginalDocument - change the original document.
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolution - the expected code after fixing.
* @param expectedResolution2 - the expected code after fixing.
* @return the matching resolved text.
*/
protected String assertQuickFix(
boolean changeOriginalDocument,
String issueCode,
String invalidCode,
String expectedLabel,
String expectedResolution,
String... expectedResolution2) {
QuickFixAsserts asserts = getQuickFixAsserts(issueCode, invalidCode);
String[] expected = new String[expectedResolution2.length + 1];
expected[0] = expectedResolution;
System.arraycopy(expectedResolution2, 0, expected, 1, expectedResolution2.length);
return asserts.assertQuickFix(changeOriginalDocument, expectedLabel, expected);
}
/** Assert the quick fix is changing the document in the expected way.
*
* @param changeOriginalDocument - change the original document.
* @param issueCode - the code of the issue to test.
* @param invalidCode - the code that is generating the issue.
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolution - the expected code after fixing.
* @param expectedResolution2 - the expected code after fixing.
* @return the matching resolved text.
*/
protected String assertQuickFixWithErrors(
boolean changeOriginalDocument,
String issueCode,
String invalidCode,
String expectedLabel,
String expectedResolution,
String... expectedResolution2) {
QuickFixAsserts asserts = getQuickFixAsserts(issueCode, invalidCode, false);
String[] expected = new String[expectedResolution2.length + 1];
expected[0] = expectedResolution;
System.arraycopy(expectedResolution2, 0, expected, 1, expectedResolution2.length);
return asserts.assertQuickFix(changeOriginalDocument, expectedLabel, expected);
}
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
protected static class QuickFixAsserts {
private final String issueCode;
private final String invalidCode;
private final Resource scriptResource;
private final Collection<IssueResolution> resolutions = new LinkedList<>();
/**
* @param issueCode
* @param invalidCode
* @param scriptResource
* @param resolutions
*/
public QuickFixAsserts(
String issueCode,
String invalidCode,
Resource scriptResource,
Collection<IssueResolution> resolutions) {
this.issueCode = issueCode;
this.invalidCode = invalidCode;
this.scriptResource = scriptResource;
this.resolutions.addAll(resolutions);
}
/** Replies the resolution that corresponds to the given label.
*
* @param label - the label of the resolution to search for.
* @param failIfNotFound - fails if no resolution found.
* @param removeWhenFound - indicates if the resolution must be removed for the list of the resolutions
* when it was found.
* @return the resolution or <code>null</code>.
*/
public IssueResolution findResolution(String label, boolean failIfNotFound, boolean removeWhenFound) {
Iterator<IssueResolution> iterator = this.resolutions.iterator();
String close = null;
int distance = Integer.MAX_VALUE;
while (iterator.hasNext()) {
IssueResolution resolution = iterator.next();
String resolutionLabel = resolution.getLabel();
int d = TextUtil.getLevenshteinDistance(resolutionLabel, label);
if (d == 0) {
if (removeWhenFound) {
iterator.remove();
}
return resolution;
} else if (close == null || d < distance) {
distance = d;
close = resolutionLabel;
}
}
if (failIfNotFound) {
throw new ComparisonFailure(
"Quick fix not found for the issue '" + this.issueCode + "'.", //$NON-NLS-1$//$NON-NLS-2$
label, close);
}
return null;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
for(IssueResolution resolution : this.resolutions) {
if (buffer.length() > 0) {
buffer.append(", "); //$NON-NLS-1$
}
buffer.append("\""); //$NON-NLS-1$
buffer.append(Strings.convertToJavaString(resolution.getLabel()));
buffer.append("\""); //$NON-NLS-1$
}
return "[ " + buffer.toString() + " ]"; //$NON-NLS-1$ //$NON-NLS-2$
}
/** Replies the number of resolutions.
*
* @return the number of resolutions.
*/
public int getResolutionCount() {
return this.resolutions.size();
}
private static String unifiesNewLineCharacters(String content) {
String result = Strings.emptyIfNull(content);
result = result.replaceAll("\r\n", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
result = result.replaceAll("\r", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
return result.trim();
}
/** Test the existence of a valid quick fix.
*
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolutions - the expected codes after fixing.
* @return the matching resolved text.
*/
public String assertQuickFix(
String expectedLabel,
String... expectedResolutions) {
return assertQuickFix(false, expectedLabel, expectedResolutions);
}
/** Test the existence of a valid quick fix.
*
* @param changeOriginalDocument - change the original document
* @param expectedLabel - the expected label for the quick fix.
* @param expectedResolutions - the expected codes after fixing.
* @return the matching resolved text.
*/
public String assertQuickFix(
boolean changeOriginalDocument,
String expectedLabel,
String... expectedResolutions) {
try {
assert (expectedResolutions.length > 0);
IssueResolution resolution = findResolution(expectedLabel, true, true);
assertEquals(expectedLabel, resolution.getLabel());
String newContent;
if (changeOriginalDocument) {
resolution.apply();
IXtextDocument document = resolution.getModificationContext().getXtextDocument();
newContent = document.get();
} else {
XtextResource xtextResource = new XtextResource(this.scriptResource.getURI());
xtextResource.setFragmentProvider(new TestFragmentProvider(this.scriptResource));
TestXtextDocument document = new TestXtextDocument(xtextResource, this.invalidCode);
TestModificationContext modificationContext = new TestModificationContext(document);
String oldContent = Objects.toString(document);
final IModification modification = resolution.getModification();
modification.apply(modificationContext);
newContent = document.toString();
newContent = Objects.toString(newContent).trim();
if (Objects.equals(newContent, oldContent)) {
// Save the resource for ensuring EMF changes are comitted.
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
SaveOptions options = new SaveOptions(false, false) {
//
};
this.scriptResource.save(baos, options.toOptionsMap());
baos.flush();
newContent = baos.toString();
}
newContent = Objects.toString(newContent);
}
}
newContent = unifiesNewLineCharacters(newContent);
final Set<String> expected = new TreeSet<>();
for (final String expectedResolution : expectedResolutions) {
final String ex = unifiesNewLineCharacters(expectedResolution);
expected.add(ex);
}
String closeResolution = null;
int distance = Integer.MAX_VALUE;
for (String expectedResolution : expected) {
int d = TextUtil.getLevenshteinDistance(expectedResolution, newContent);
if (d == 0) {
return newContent;
}
if (closeResolution == null || d < distance) {
distance = d;
closeResolution = expectedResolution;
}
}
throw new ComparisonFailure(
"Invalid quick fix", //$NON-NLS-1$
closeResolution,
newContent);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** Test if there is no more quick fix.
*/
public void assertNoQuickFix() {
assertEquals("Expecting no quick fix, but got: " //$NON-NLS-1$
+ toString(),
0, this.resolutions.size());
}
} // class QuickFixAsserts
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
private static class TestModificationContext implements IModificationContext {
private final IXtextDocument document;
/**
* @param document
*/
public TestModificationContext(IXtextDocument document) {
this.document = document;
}
@Override
public IXtextDocument getXtextDocument() {
return this.document;
}
@Override
public IXtextDocument getXtextDocument(URI uri) {
return this.document;
}
} // class TestModificationContext
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
private static class TestFragmentProvider implements IFragmentProvider {
private final Resource scriptResource;
/**
* @param scriptResource
*/
public TestFragmentProvider(Resource scriptResource) {
this.scriptResource = scriptResource;
}
@Override
public String getFragment(EObject obj, Fallback fallback) {
throw new UnsupportedOperationException();
}
@Override
public EObject getEObject(Resource resource, String fragment, Fallback fallback) {
return this.scriptResource.getEObject(fragment);
}
} // class TestFragmentProvider
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
private static class TestTextStore implements ITextStore {
private final StringBuilder content = new StringBuilder();
/**
* @param initialContent - initialContent
*/
public TestTextStore(String initialContent) {
if (initialContent != null) {
this.content.append(initialContent);
}
}
@Override
public char get(int offset) {
return this.content.charAt(offset);
}
@Override
public String get(int offset, int length) {
return this.content.substring(offset, offset + length);
}
@Override
public int getLength() {
return this.content.length();
}
@Override
public void replace(int offset, int length, String text) {
this.content.replace(offset, offset + length, text);
}
@Override
public void set(String text) {
this.content.setLength(0);
this.content.append(text);
}
@Override
public String toString() {
return this.content.toString();
}
} // class TestTextStore
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
private static class TestLineTracker implements ILineTracker {
/** The predefined delimiters of this tracker */
private final static String[] STR_DELIMITERS = { System.getProperty("line.separator"), "\r", "\n" }; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
private final StringBuilder content = new StringBuilder();
private String[] lines = null;
private static String[] splitInLines(String text) {
StringBuilder delims = new StringBuilder();
for(String delim : STR_DELIMITERS) {
if (delims.length() > 0) {
delims.append("|"); //$NON-NLS-1$
}
delims.append("(" + Matcher.quoteReplacement(delim) + ")"); //$NON-NLS-1$//$NON-NLS-2$
}
return text.split(delims.toString());
}
private void ensureLines() {
if (this.lines == null) {
this.lines = splitInLines(this.content.toString());
}
}
/**
* @param initialContent
*/
public TestLineTracker(String initialContent) {
this.content.append(initialContent);
}
@Override
public String[] getLegalLineDelimiters() {
return STR_DELIMITERS;
}
@Override
public String getLineDelimiter(int line) throws BadLocationException {
ensureLines();
if (line < 0 || line >= this.lines.length) {
throw new BadLocationException();
}
String text = this.lines[line];
for(String delim : STR_DELIMITERS) {
if (text.endsWith(delim)) {
return delim;
}
}
return null;
}
@Override
public int computeNumberOfLines(String text) {
String[] lines = splitInLines(text);
return lines.length;
}
@Override
public int getNumberOfLines() {
ensureLines();
return this.lines.length;
}
@Override
public int getNumberOfLines(int offset, int length) throws BadLocationException {
String subText = this.content.substring(offset, offset + length);
return computeNumberOfLines(subText);
}
@Override
public int getLineOffset(int line) throws BadLocationException {
ensureLines();
if (line < 0 || line >= this.lines.length) {
throw new BadLocationException();
}
int offset = 0;
for(int i=0; i < this.lines.length && i < line; ++i) {
offset += this.lines[i].length();
}
return offset;
}
@Override
public int getLineLength(int line) throws BadLocationException {
ensureLines();
if (line < 0 || line >= this.lines.length) {
throw new BadLocationException();
}
return this.lines[line].length();
}
@Override
public int getLineNumberOfOffset(int offset) throws BadLocationException {
if (offset < 0) {
throw new BadLocationException();
}
ensureLines();
int tmpOffset = 0;
for(int i=0; i < this.lines.length; ++i) {
tmpOffset += this.lines[i].length();
if (offset < tmpOffset) {
return i;
}
}
throw new BadLocationException();
}
@Override
public IRegion getLineInformationOfOffset(int offset) throws BadLocationException {
if (offset < 0) {
throw new BadLocationException();
}
ensureLines();
int tmpOffset = 0;
int previousOffset;
for(int i = 0; i < this.lines.length; ++i) {
previousOffset = tmpOffset;
tmpOffset += this.lines[i].length();
if (offset < tmpOffset) {
return new Region(previousOffset, this.lines[i].length());
}
}
throw new BadLocationException();
}
@Override
public IRegion getLineInformation(int line) throws BadLocationException {
if (line < 0 || line >= this.lines.length) {
throw new BadLocationException();
}
ensureLines();
int offset = 0;
for(int i = 0; i < (line - 1); ++i) {
offset += this.lines[i].length();
}
return new Region(offset, this.lines[line].length());
}
@Override
public void replace(int offset, int length, String text) throws BadLocationException {
this.lines = null;
this.content.replace(offset, offset + length, text);
}
@Override
public void set(String text) {
this.lines = null;
this.content.setLength(0);
this.content.append(text);
}
@Override
public String toString() {
return this.content.toString();
}
} // class TestLineTracker
/**
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
private static class TestXtextDocument extends Document implements IXtextDocument {
private final XtextResource resource;
private final TestTextStore textStore;
/**
* @param resource
* @param initialText
*/
public TestXtextDocument(XtextResource resource, String initialText) {
this.resource = resource;
this.textStore = new TestTextStore(initialText);
setTextStore(this.textStore);
setLineTracker(new TestLineTracker(initialText));
}
@Override
public String toString() {
return this.textStore.toString();
}
@Override
public <T> T readOnly(IUnitOfWork<T, XtextResource> work) {
throw new UnsupportedOperationException();
}
@Override
public <T> T priorityReadOnly(IUnitOfWork<T, XtextResource> work) {
throw new UnsupportedOperationException();
}
@Override
public <T> T modify(IUnitOfWork<T, XtextResource> work) {
try {
return work.exec(this.resource);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** {@inheritDoc}
*/
@Override
public <T> T getAdapter(Class<T> adapterType) {
throw new UnsupportedOperationException();
}
/** {@inheritDoc}
*/
@Override
public void addModelListener(IXtextModelListener listener) {
throw new UnsupportedOperationException();
}
/** {@inheritDoc}
*/
@Override
public void removeModelListener(IXtextModelListener listener) {
throw new UnsupportedOperationException();
}
/** {@inheritDoc}
*/
@Override
public void addXtextDocumentContentObserver(IXtextDocumentContentObserver listener) {
throw new UnsupportedOperationException();
}
/** {@inheritDoc}
*/
@Override
public void removeXtextDocumentContentObserver(IXtextDocumentContentObserver listener) {
throw new UnsupportedOperationException();
}
} // class TestXtextDocument
}