org.eclipse.xtext.xbase.testing.CompilationTestHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.eclipse.xtext.xbase.testing Show documentation
Show all versions of org.eclipse.xtext.xbase.testing Show documentation
Infrastructure for testing Xbase languages.
The newest version!
/*******************************************************************************
* Copyright (c) 2012, 2017, 2024 itemis AG (http://www.itemis.eu) and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.xtext.xbase.testing;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.diagnostics.Severity;
import org.eclipse.xtext.generator.GeneratorContext;
import org.eclipse.xtext.generator.GeneratorDelegate;
import org.eclipse.xtext.generator.IGenerator;
import org.eclipse.xtext.generator.IOutputConfigurationProvider;
import org.eclipse.xtext.generator.OutputConfiguration;
import org.eclipse.xtext.resource.FileExtensionProvider;
import org.eclipse.xtext.resource.IResourceDescription;
import org.eclipse.xtext.resource.IResourceServiceProvider;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.eclipse.xtext.resource.impl.ResourceDescriptionsData;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.util.Exceptions;
import org.eclipse.xtext.util.IAcceptor;
import org.eclipse.xtext.util.JavaVersion;
import org.eclipse.xtext.validation.CheckMode;
import org.eclipse.xtext.validation.Issue;
import org.eclipse.xtext.workspace.FileProjectConfig;
import org.eclipse.xtext.workspace.ProjectConfigAdapter;
import org.eclipse.xtext.xbase.compiler.GeneratorConfig;
import org.eclipse.xtext.xbase.compiler.GeneratorConfigProvider;
import org.eclipse.xtext.xbase.compiler.IGeneratorConfigProvider;
import org.eclipse.xtext.xbase.lib.Conversions;
import org.eclipse.xtext.xbase.lib.IterableExtensions;
import org.eclipse.xtext.xbase.lib.Pair;
import org.eclipse.xtext.xbase.testing.RegisteringFileSystemAccess.GeneratedFile;
import org.junit.Assert;
import com.google.inject.Inject;
import com.google.inject.Provider;
/**
* A utility class for testing Xtext languages that compile to Java code.
* It's designed to be used as an injected extension in unit tests written in Xtend.
*
* Example:
*
* @RunWith(XtextRunner)
* @InjectWith(MyLanguageInjectorProvider)
* class CompilerTest {
*
* @Rule @Inject public TemporaryFolder temporaryFolder
* @Inject extension CompilationTestHelper
*
* @Test def void myTest() {
* '''
* // DSL code
* Foo bla
* '''.assertCompilesTo('''
* class Foo {
* String bla
* }
* '''
* }
* }
*
*
* @author Sven Efftinge
* @since 2.7
*
* @noextend This class is not intended to be subclassed by clients.
*/
public class CompilationTestHelper {
private final static Logger LOG = Logger.getLogger(CompilationTestHelper.class);
public final static String PROJECT_NAME = "myProject";
@Inject private OnTheFlyJavaCompiler2 javaCompiler;
@Inject private Provider resourceSetProvider;
@Inject private FileExtensionProvider extensionProvider;
@Inject private IOutputConfigurationProvider outputConfigurationProvider;
@Inject private Provider resultProvider;
@Inject private IGeneratorConfigProvider generatorConfigProvider;
private TemporaryFolder temporaryFolder;
private File workspaceRoot;
private ClassLoader classpathUriContext;
/**
* creates a fresh temp directory and sets it as the workspace root.
*/
public void configureFreshWorkspace() {
workspaceRoot = createFreshTempDir();
}
@Inject
private void setTemporaryFolder(TemporaryFolder folder) {
this.temporaryFolder = folder;
configureFreshWorkspace();
}
protected String getSourceFolderPath() {
return "/"+PROJECT_NAME+"/src";
}
protected File createFreshTempDir() {
try {
return temporaryFolder.newFolder();
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Add the class path entries of the given classes to the java compiler's class path.
*/
public void setJavaCompilerClassPath(@SuppressWarnings("unused") Class> ...classes) {
LOG.warn("java compiler classpath setup is deprecated. Only classloader based classpathes are supported.");
}
/**
* @since 2.9
*/
public void setJavaCompilerClassPath(ClassLoader classLoader) {
this.javaCompiler = new OnTheFlyJavaCompiler2(classLoader, generatorConfigProvider.get(null).getJavaSourceVersion());
this.classpathUriContext = classLoader;
}
/**
* Sets the Java version both for the DSL generator, for example, Xbase
* compiler, and for the Java compiler.
*
* @since 2.11
*/
public void setJavaVersion(JavaVersion javaVersion) {
this.javaCompiler.setJavaVersion(javaVersion);
}
/**
* Asserts that the expected code is generated for the given source.
*
* @param source some valid source code written in the language under test
* @param expected the expected Java source code.
* @throws IOException if the resource loading fails
*/
public void assertCompilesTo(CharSequence source, final CharSequence expected) throws IOException {
final boolean[] called = {false};
compile(source, new IAcceptor() {
@Override
public void accept(Result r) {
Assert.assertEquals(expected.toString(), r.getSingleGeneratedCode());
called[0] = true;
}
});
Assert.assertTrue("Nothing was generated but the expectation was :\n"+expected, called[0]);
}
/**
* Parses, validates and compiles the given source. Calls the given acceptor for each
* resource which is generated from the source.
*
* @param source some code written in the language under test.
* @param acceptor gets called once for each file generated in {@link IGenerator}
* @throws IOException if the resource loading fails
*/
@SuppressWarnings("unchecked")
public void compile(CharSequence source, IAcceptor acceptor) throws IOException {
String fileName = "MyFile."+extensionProvider.getPrimaryFileExtension();
compile(resourceSet(new Pair(fileName, source)), acceptor);
}
/**
* Parses, validates and compiles the given sources. Calls the given acceptor for each
* resource which is generated from the source.
*
* @param sources some inputs written in the language under test.
* @param acceptor gets called once for each file generated in {@link IGenerator}
* @throws IOException if the resource loading fails
*
* @since 2.8
*/
@SuppressWarnings("unchecked")
public void compile(Iterable extends CharSequence> sources, IAcceptor acceptor) throws IOException {
int index = 0;
List> pairs = newArrayList();
for (CharSequence source : sources) {
String fileName = "MyFile" + (++index) + "." + extensionProvider.getPrimaryFileExtension();
pairs.add(new Pair(fileName, source));
}
compile(resourceSet(((Pair[])Conversions.unwrapArray(pairs, Pair.class))), acceptor);
}
/**
* Parses, validates and compiles the given source. Calls the given acceptor for each
* resource which is generated from the source.
*
* @param resourceSet - the {@link ResourceSet} to use
* @param acceptor gets called once for each file generated in {@link IGenerator}
*/
public void compile(final ResourceSet resourceSet, IAcceptor acceptor) {
try {
List resourcesToCheck = newArrayList(resourceSet.getResources());
if (generatorConfigProvider instanceof GeneratorConfigProvider) {
GeneratorConfigProvider configProvider = (GeneratorConfigProvider) generatorConfigProvider;
GeneratorConfig config = generatorConfigProvider.get(null);
config.setJavaSourceVersion(javaCompiler.getJavaVersion());
GeneratorConfig existent = configProvider.install(resourceSet, config);
if (existent != null) {
existent.setJavaSourceVersion(javaCompiler.getJavaVersion());
configProvider.install(resourceSet, existent);
}
}
Result result = resultProvider.get();
result.setJavaCompiler(javaCompiler);
result.setCheckMode(getCheckMode());
result.setResources(resourcesToCheck);
result.setResourceSet(resourceSet);
result.setOutputConfigurations(getOutputConfigurations());
result.doGenerate();
acceptor.accept(result);
} catch (Exception e) {
Exceptions.throwUncheckedException(e);
}
}
/**
* @since 2.8
*/
protected CheckMode getCheckMode() {
return CheckMode.NORMAL_AND_FAST;
}
protected Iterable extends OutputConfiguration> getOutputConfigurations() {
return outputConfigurationProvider.getOutputConfigurations();
}
/**
* creates a fresh resource set with the given resources
*
* @param resources - pairs of file names and their contents
* @return a ResourceSet, containing the given resources.
* @throws IOException if the resource loading fails
*/
public ResourceSet resourceSet(@SuppressWarnings("unchecked") Pair ...resources ) throws IOException {
XtextResourceSet result = newResourceSetWithUTF8Encoding();
FileProjectConfig projectConfig = new FileProjectConfig(new File(workspaceRoot,PROJECT_NAME), PROJECT_NAME);
projectConfig.addSourceFolder("src");
ProjectConfigAdapter.install(result, projectConfig);
for (Pair entry : resources) {
URI uri = copyToWorkspace(getSourceFolderPath()+"/"+entry.getKey(), entry.getValue());
Resource resource = result.createResource(uri);
if (resource == null)
throw new IllegalStateException("Couldn't create resource for URI "+uri+". Resource.Factory not registered?");
resource.load(result.getLoadOptions());
}
return result;
}
/**
* Physically copies the given files to the currently used workspace root (a temporary folder).
* Files are written with UTF-8 encoding.
* @param workspacefilePath the workspace relative path
* @param contents the file contents
*/
public URI copyToWorkspace(String workspacefilePath, CharSequence contents) {
File fullPath = new File(workspaceRoot.getAbsolutePath()+"/"+workspacefilePath);
if (fullPath.exists()) {
fullPath.delete();
} else {
mkDir(fullPath.getParentFile());
}
URI uri = URI.createFileURI(fullPath.getAbsolutePath());
writeFileWithUTF8(uri.toFileString(), contents.toString());
return uri;
}
private void writeFileWithUTF8(String filename, String content) {
try {
final File file = new File(filename);
com.google.common.io.Files.write(content.getBytes(StandardCharsets.UTF_8), file);
} catch (IOException e) {
throw new WrappedException(e);
}
}
private void mkDir(File file) {
if (!file.getParentFile().exists()) {
mkDir(file.getParentFile());
}
if (!file.exists()) {
file.mkdir();
}
}
/**
* same as {@link #resourceSet(Pair...)} but without actually loading the created resources.
*/
public ResourceSet unLoadedResourceSet(@SuppressWarnings("unchecked") Pair ...resources ) throws IOException {
XtextResourceSet result = newResourceSetWithUTF8Encoding();
for (Pair entry : resources) {
URI uri = copyToWorkspace(getSourceFolderPath()+"/"+entry.getKey(), entry.getValue());
Resource resource = result.createResource(uri);
if (resource == null)
throw new IllegalStateException("Couldn't create resource for URI "+uri+". Resource.Factory not registered?");
}
return result;
}
private XtextResourceSet newResourceSetWithUTF8Encoding() {
XtextResourceSet result = resourceSetProvider.get();
result.setClasspathURIContext(classpathUriContext);
result.getLoadOptions().put(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name());
return result;
}
/**
* A result contains information about various aspects of a compiled piece of code.
*
* @noextend This class is not intended to be subclassed by clients.
* @noinstantiate This class is not intended to be instantiated by clients.
*/
public static class Result {
@Inject private IResourceServiceProvider.Registry serviceRegistry;
@Inject private Provider fileSystemAccessProvider;
private OnTheFlyJavaCompiler2 javaCompiler;
private ResourceSet resourceSet;
private List sources;
private Map outputConfigurations;
private CheckMode checkMode;
protected void setResourceSet(ResourceSet resourceSet) {
this.resourceSet = resourceSet;
}
/**
* @since 2.8
*/
protected void setCheckMode(CheckMode checkMode) {
this.checkMode = checkMode;
}
protected void setResources(List sources) {
this.sources = sources;
}
/**
* @since 2.9
*/
protected void setJavaCompiler(OnTheFlyJavaCompiler2 javaCompiler) {
this.javaCompiler = javaCompiler;
}
protected void setOutputConfigurations(Iterable extends OutputConfiguration> outputConfiguration) {
this.outputConfigurations = newHashMap();
for (OutputConfiguration conf : outputConfiguration) {
outputConfigurations.put(conf.getName(), conf);
}
}
private ClassLoader classLoader;
private Map> compiledClasses;
private Map generatedCode;
private RegisteringFileSystemAccess access;
private ResourceDescriptionsData index;
private List allErrorsAndWarnings;
/**
* Ensures validation has happened and returns any errors and warnings
*
* @return errors and warnings contained in the currently processed sources
*/
public List getErrorsAndWarnings() {
doValidation();
return allErrorsAndWarnings;
}
/**
* Ensures that no error has been detected through the triggered validation.
*
* @since 2.35
*/
public void assertNoErrors() {
var errors = getErrorsAndWarnings().stream()
.filter(it -> it.getSeverity() == Severity.ERROR)
.collect(Collectors.toList());
if (!isEmpty(errors))
fail("Expected no errors, but got:\n" +
errors.stream().map(Object::toString).collect(Collectors.joining("\n")));
}
/**
* Ensures that no issue has been detected through the triggered validation.
*
* @since 2.35
*/
public void assertNoIssues() {
var issues = getErrorsAndWarnings();
if (!isEmpty(issues))
fail("Expected no issues, but got:\n" +
issues.stream().map(Object::toString).collect(Collectors.joining("\n")));
}
/**
* Ensures compilation has happened and returns any generated and compiled Java classes.
*
* @return the compiled Java classes
*/
public Map> getCompiledClasses() {
doCompile();
return compiledClasses;
}
/**
* Ensures compilation has happened and returns the class loader including compiled classes.
*
* @return the class loader after the compilation happend
*/
public ClassLoader getClassLoader() {
doCompile();
return classLoader;
}
/**
* Ensures generation happened and returns a map of the generated Java source files.
*
* @return a map of the generated Java source files, where the key is the qualified class name and the value the generated Java code.
*/
public Map getGeneratedCode() {
doGenerate();
return generatedCode;
}
/**
* convenience method. Same as getGeneratedCode().get(typeName)
*/
public String getGeneratedCode(String typeName) {
return getGeneratedCode().get(typeName);
}
/**
* Convenience method for the common case, that only one file is generated.
*/
public String getSingleGeneratedCode() {
doGenerate();
Set generatedFiles = access.getGeneratedFiles();
if (generatedFiles.size() == 1)
return generatedFiles.iterator().next().getContents().toString();
else if (generatedFiles.isEmpty())
return "NO FILE WAS GENERATED";
String separator = System.getProperty("line.separator");
if (separator == null)
separator = "\n";
List files = newArrayList(generatedFiles);
Collections.sort(files, new Comparator() {
@Override
public int compare(GeneratedFile o1,
GeneratedFile o2) {
return o1.getPath().toString().compareTo(o2.getPath().toString());
}
});
StringBuilder result = new StringBuilder("MULTIPLE FILES WERE GENERATED"+separator+separator);
int i = 1;
for (GeneratedFile file: files) {
result.append("File "+i+" : "+file.getPath().toString()+separator+separator);
result.append(file.getContents()).append(separator);
i++;
}
return result.toString();
}
/**
* @return the resource set used in this compilation process
*/
public ResourceSet getResourceSet() {
return resourceSet;
}
/**
* Convenience method for single generated Java classes
*/
public Class> getCompiledClass() {
return IterableExtensions.head(getCompiledClasses().values());
}
/**
* Convenience method for single generated Java classes
*/
public Class> getCompiledClass(String className) {
return getCompiledClasses().get(className);
}
/**
* @return all generated resources. the key is the file path and the value denotes the generated text.
*/
public Map getAllGeneratedResources() {
doGenerate();
Map result = newHashMap();
for (GeneratedFile f: access.getGeneratedFiles()) {
result.put(f.getPath(), f.getContents());
}
return result;
}
protected void doIndex() {
if (index == null) {
// indexing
List descriptions = newArrayList();
for (Resource resource : sources) {
IResourceServiceProvider serviceProvider = serviceRegistry.getResourceServiceProvider(resource.getURI());
IResourceDescription description = serviceProvider.getResourceDescriptionManager().getResourceDescription(resource);
descriptions.add(description);
}
index = new ResourceDescriptionsData(descriptions);
ResourceDescriptionsData.ResourceSetAdapter.installResourceDescriptionsData(resourceSet, index);
}
}
protected void doLinking() {
doIndex();
for (Resource resource : sources) {
EcoreUtil2.resolveLazyCrossReferences(resource, CancelIndicator.NullImpl);
}
}
protected void doValidation() {
if (allErrorsAndWarnings == null) {
doLinking();
allErrorsAndWarnings = newArrayList();
// validation
for (Resource resource : sources) {
if (resource instanceof XtextResource) {
XtextResource xtextResource = (XtextResource) resource;
List issues = xtextResource.getResourceServiceProvider().getResourceValidator().validate(xtextResource, checkMode, CancelIndicator.NullImpl);
for (Issue issue : issues) {
allErrorsAndWarnings.add(issue);
}
}
}
}
}
protected void doGenerate() {
if (access == null) {
doValidation();
access = fileSystemAccessProvider.get();
access.setOutputConfigurations(outputConfigurations);
for (Resource resource : sources) {
if (resource instanceof XtextResource) {
access.setProjectName(PROJECT_NAME);
XtextResource xtextResource = (XtextResource) resource;
IResourceServiceProvider resourceServiceProvider = xtextResource.getResourceServiceProvider();
GeneratorDelegate generator = resourceServiceProvider.get(GeneratorDelegate.class);
if (generator != null) {
GeneratorContext context = new GeneratorContext();
context.setCancelIndicator(CancelIndicator.NullImpl);
generator.generate(xtextResource, access, context);
}
}
}
generatedCode = newHashMap();
for (final GeneratedFile e : access.getGeneratedFiles()) {
if (e.getJavaClassName() != null) {
generatedCode.put(e.getJavaClassName(), e.getContents().toString());
}
}
}
}
protected void doCompile() {
if (compiledClasses == null || classLoader==null) {
doGenerate();
try {
Map> compilationResult = javaCompiler.compileToClasses(getGeneratedCode());
Iterator> values = compilationResult.values().iterator();
this.classLoader = values.hasNext() ? values.next().getClassLoader() : null;
this.compiledClasses = compilationResult;
} catch (IllegalArgumentException e) {
throw new AssertionError(e);
}
}
}
}
}