All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.xtext.xbase.testing.CompilationTestHelper Maven / Gradle / Ivy

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 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 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 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); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy