Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,35 @@
package com.avaloq.tools.ddk.xtext.export.jvmmodel;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.impl.ResourceImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.xtext.common.types.JvmDeclaredType;
import org.eclipse.xtext.common.types.JvmGenericType;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.eclipse.xtext.xbase.jvmmodel.AbstractModelInferrer;
import org.eclipse.xtext.xbase.jvmmodel.IJvmDeclaredTypeAcceptor;
import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder;
import org.eclipse.xtext.xbase.lib.Procedures.Procedure1;
import org.junit.jupiter.api.Test;

import com.avaloq.tools.ddk.xtext.export.export.ExportModel;
import com.avaloq.tools.ddk.xtext.expression.generator.GenModelUtilX;
import com.avaloq.tools.ddk.xtext.test.export.util.ExportTestUtil;
import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest;

Expand All @@ -37,6 +49,11 @@
* <p>
* That header is optional, so {@code ExportModel.name} is {@code null} for such models. The documentation of the inferred
* resource description manager must then be rendered with an empty name rather than with the string {@code "null"}.
* </p>
* <p>
* The inferrer must also leave the {@link GenModelUtilX} context as it found it, since a context left set keeps the
* model's resource reachable from the builder thread.
* </p>
*/
@SuppressWarnings("nls")
public class ExportJvmModelInferrerTest extends AbstractXtextTest {
Expand Down Expand Up @@ -78,6 +95,73 @@ public void testResourceDescriptionManagerDocumentationOfHeaderlessModel() throw
"A null export model name must render as an empty string, not as \"null\".");
}

@Test
public void testInferenceRestoresGenModelContext() throws IOException, ReflectiveOperationException {
final Resource resource = parseHeaderlessModel();
final ExportModel model = (ExportModel) resource.getContents().get(0);
final ExportJvmModelInferrer inferrer = getXtextTestUtil().get(ExportJvmModelInferrer.class);
final GenModelUtilX genModelUtil = genModelUtilOf(inferrer);
final Resource previousContext = new ResourceImpl(URI.createURI("memory:/previous-context"));
genModelUtil.setResource(previousContext);
try {
final RecordingAcceptor acceptor = new RecordingAcceptor(resource);
// as the JVM model associator does before inferring; setContext is package-private
final Method setContext = AbstractModelInferrer.class.getDeclaredMethod("setContext", Resource.class);
setContext.setAccessible(true);
setContext.invoke(inferrer, resource);
inferrer.infer(model, acceptor, false);
assertSame(previousContext, genModelUtil.getContext(), "Inference must restore the previous GenModelUtilX context.");
assertFalse(acceptor.initializers.isEmpty(), "The inferrer must register type initializers.");
acceptor.initializers.forEach(Runnable::run);
assertSame(previousContext, genModelUtil.getContext(), "Type initializers must restore the previous GenModelUtilX context.");
} finally {
genModelUtil.setResource(null);
}
}

/**
* Returns the {@link GenModelUtilX} the given inferrer sets its context on.
*
* @param inferrer
* the inferrer, must not be {@code null}
* @return the inferrer's utility, never {@code null}
* @throws ReflectiveOperationException
* if the field cannot be read
*/
private static GenModelUtilX genModelUtilOf(final ExportJvmModelInferrer inferrer) throws ReflectiveOperationException {
final Field field = ExportJvmModelInferrer.class.getDeclaredField("genModelUtil");
field.setAccessible(true);
return (GenModelUtilX) field.get(inferrer);
}

/**
* Adds the accepted types to the resource, as the JVM model associator does, and records their initializers so that the
* test can run them after inference.
*/
private static final class RecordingAcceptor implements IJvmDeclaredTypeAcceptor {

private final Resource resource;
private final List<Runnable> initializers = new ArrayList<>();

RecordingAcceptor(final Resource resource) {
this.resource = resource;
}

@Override
public <T extends JvmDeclaredType> IPostIndexingInitializing<T> accept(final T type) {
resource.getContents().add(type);
return initializer -> initializers.add(() -> initializer.apply(type));
}

@Override
public <T extends JvmDeclaredType> void accept(final T type, final Procedure1<? super T> lateInitialization) {
resource.getContents().add(type);
if (lateInitialization != null) {
initializers.add(() -> lateInitialization.apply(type));
}
}
}

/**
* Parses the header-less export model and installs its derived state.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,12 @@ protected void _infer(final ExportModel model, final IJvmDeclaredTypeAcceptor ac
* Runs the given operation with the model's resource as the {@link GenModelUtilX} context, restoring the previous
* context afterwards.
* <p>
* The context is a thread-local on a shared {@link GenModelUtilX}. Leaving it set after inference would keep the
* model's resource, and through it the builder's whole resource set, reachable from a pooled builder thread until
* that thread happens to infer another export model.
* The context is a thread-local of this inferrer's own {@link GenModelUtilX}; the utility is not a singleton, so every
* inferrer instance has a separate thread-local. Left set after inference, the context would keep the model's resource,
* and through it the builder's whole resource set, reachable from a pooled builder thread for as long as that thread
* lives: the resource's inferred types hold body closures referencing this inferrer, so the thread-local entry keeps its
* own weakly referenced key reachable and is never cleared. Inferring another export model does not overwrite it either,
* since that happens through another inferrer and thread-local.
* </p>
*
* @param model
Expand Down Expand Up @@ -207,10 +210,10 @@ private Procedure1<JvmGenericType> inModelContext(final ExportModel model, final
* the inferrer's invocation; running them from inside a deferred body closure delays that work past the
* Xtext linking phase, by which time the grammar's parser rules and the model's import packages are linked.
* <p>
* The injected {@link GenModelUtilX} is shared across resources and carries the current model's resource as
* mutable state; the inferrer sets it on entry, but by the time deferred bodies fire another resource may have
* inferred and clobbered the field. Re-bind it to this body's model resource so the GenPackage lookup runs in
* the right context.
* Deferred bodies run during generation, after {@link #_infer} has restored the previous context, so the model's resource
* is set as the context of this inferrer's {@link GenModelUtilX} while the producer runs, see
* {@link #withModelContext(ExportModel, Runnable)}. Only that instance carries the context: the expression compiler and
* translator inject their own instances, which look GenPackages up in the element's resource set instead.
*
* @param model
* the export model the body belongs to, must not be {@code null}
Expand Down