From a5e5a2e37d7815570b76bda634d84e1ccc4004de Mon Sep 17 00:00:00 2001 From: Simeon Andreev Date: Thu, 23 May 2024 13:27:44 +0200 Subject: [PATCH] Provide JUnit 5 equivalent of XpectRunner This change provides a JUnit 5 alternative of the JUnit 4 XpectRunner. To summarize the JUnit 4 specific code for Xpect tests, it looks like this: * XpectRunner takes the @XpectTestFiles test DSLs and generates a test case for each DSL test file, i.e. it generates test cases. * XpectFileRunner takes a DSL test file and generates test methods for a test case, these are coming from the XPECT statements (XpectInvocation or XjcTestMethod, I'm not sure which is what in the Xpect tests. * XpectTestRunner generates a test method for a XpectInvocation. * TestRunner generates a test method for a XjmTestMethod. The code JUnit 4 is "transformed" as follows: XpectRunner -> XpectDynamicTestFactory XpectFileRunner -> XpectDynamicTestCase XpectTestRunner -> XpectInvocationDynamicTest TestRunner -> XpectDynamicTest There is lots of supporting code that is not JUnit 4 specific, it remains functional and unchanged. The API proposal is to add a marker interface org.eclipse.xpect.dynamic.IXpectDynamicTestFactory, that is implemented by a test case in order to enable Xpect tests (based on JUnit 5) for that test case. There is also org.eclipse.xpect.dynamic.XpectDynamicTestCase, which has setUp() and tearDown() callbacks, called before reps. after each test method. We require these, since the JUnit 5 construct for generating tests, dynamic tests, doesn't have before/after callbacks (junit-team/junit5#1292 (comment), see also warning here: https://junit.org/junit5/docs/current/user-guide/#writing-tests-dynamic-tests). XpectRunner itself generates tests based on annotation values via JUnit 4 runners. XpectDynamicTestCase can be used by extending it and specifying the annotation @XpectReplace(XpectDynamicTestCase.class) at the extending class. Then importing the extended class at the client test case via e.g.: ... @XpectImport({MyXpectDynamicTestCase.class ...}) public class MyXpectTestCase implements IXpectDynamicTestFactory { ... Fixes: #262 --- .../xpect/ui/util/XpectFileAccess.java | 5 +- .../org/eclipse/xpect/ui/util/XpectUtil.java | 5 +- org.eclipse.xpect/META-INF/MANIFEST.MF | 2 + .../dynamic/IXpectDynamicTestFactory.java | 17 ++ .../xpect/dynamic/XpectDynamicTest.java | 64 ++++++ .../xpect/dynamic/XpectDynamicTestCase.java | 174 +++++++++++++++ .../dynamic/XpectDynamicTestFactory.java | 202 ++++++++++++++++++ .../dynamic/XpectInvocationDynamicTest.java | 96 +++++++++ .../xpect/model/XpectJavaModelImplCustom.java | 5 +- .../org/eclipse/xpect/runner/XpectRunner.java | 6 + .../xpect/runner/XpectTestGlobalState.java | 38 ++++ .../XtResourceServiceProviderProvider.java | 5 +- .../org/eclipse/xpect/util/ClasspathUtil.java | 5 +- .../eclipse/xpect/util/EnvironmentUtil.java | 4 +- 14 files changed, 616 insertions(+), 12 deletions(-) create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/dynamic/IXpectDynamicTestFactory.java create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTest.java create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestCase.java create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestFactory.java create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectInvocationDynamicTest.java create mode 100644 org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectTestGlobalState.java diff --git a/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectFileAccess.java b/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectFileAccess.java index 2edf2a91..5cfff814 100644 --- a/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectFileAccess.java +++ b/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectFileAccess.java @@ -30,6 +30,7 @@ import org.eclipse.xpect.XpectFile; import org.eclipse.xpect.registry.ILanguageInfo; import org.eclipse.xpect.runner.XpectRunner; +import org.eclipse.xpect.runner.XpectTestGlobalState; import com.google.inject.Injector; @@ -67,8 +68,8 @@ protected static ResourceSet cloneResourceSet(ResourceSet rs) { // need delegation or nothing because of "java" protocol // result.setResourceFactoryRegistry(rs.getResourceFactoryRegistry()); result.setURIConverter(rs.getURIConverter()); - if (XpectRunner.testClassloader != null) { - result.setClasspathURIContext(XpectRunner.testClassloader); + if (XpectTestGlobalState.INSTANCE.testClass() != null) { + result.setClasspathURIContext(XpectTestGlobalState.INSTANCE.testClass().getClassLoader()); result.setClasspathUriResolver(new ClassloaderClasspathUriResolver()); } else if (rs instanceof XtextResourceSet) { XtextResourceSet xrs = (XtextResourceSet) rs; diff --git a/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectUtil.java b/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectUtil.java index deed9c45..2486165a 100644 --- a/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectUtil.java +++ b/org.eclipse.xpect.ui/src/org/eclipse/xpect/ui/util/XpectUtil.java @@ -28,6 +28,7 @@ import org.eclipse.xpect.XpectFile; import org.eclipse.xpect.XpectJavaModel; import org.eclipse.xpect.runner.XpectRunner; +import org.eclipse.xpect.runner.XpectTestGlobalState; import org.eclipse.xpect.ui.internal.XpectActivator; import com.google.inject.Injector; @@ -40,8 +41,8 @@ public static XpectFile loadFile(IFile file) { Injector injector = XpectActivator.getInstance().getInjector(XpectActivator.ORG_ECLIPSE_XPECT_XPECT); XtextResourceSet rs = new XtextResourceSet(); IJavaProject javaProject = JavaCore.create(file.getProject()); - if (XpectRunner.testClassloader != null) { - rs.setClasspathURIContext(XpectRunner.testClassloader); + if (XpectTestGlobalState.INSTANCE.testClass() != null) { + rs.setClasspathURIContext(XpectTestGlobalState.INSTANCE.testClass().getClassLoader()); rs.setClasspathUriResolver(new ClassloaderClasspathUriResolver()); } else if (javaProject != null && javaProject.exists()) { rs.setClasspathURIContext(javaProject); diff --git a/org.eclipse.xpect/META-INF/MANIFEST.MF b/org.eclipse.xpect/META-INF/MANIFEST.MF index d374b142..5be66a2b 100644 --- a/org.eclipse.xpect/META-INF/MANIFEST.MF +++ b/org.eclipse.xpect/META-INF/MANIFEST.MF @@ -19,11 +19,13 @@ Require-Bundle: org.eclipse.xtext, org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", org.apache.log4j;bundle-version="1.2.24", org.junit;bundle-version="4.11.0";visibility:=reexport, + junit-jupiter-api;bundle-version="5.9.1";visibility:=reexport, org.eclipse.xtext.common.types;visibility:=reexport, org.apache.log4j;bundle-version="1.2.0";visibility:=reexport, org.objectweb.asm;bundle-version="[9.5.0,10.0.0)";resolution:=optional Bundle-RequiredExecutionEnvironment: JavaSE-11 Export-Package: org.eclipse.xpect, + org.eclipse.xpect.dynamic, org.eclipse.xpect.expectation, org.eclipse.xpect.expectation.impl; x-friends:="org.eclipse.xpect.xtext.lib, diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/IXpectDynamicTestFactory.java b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/IXpectDynamicTestFactory.java new file mode 100644 index 00000000..674c398b --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/IXpectDynamicTestFactory.java @@ -0,0 +1,17 @@ +package org.eclipse.xpect.dynamic; + +import java.util.stream.Stream; + +import org.eclipse.xpect.XpectImport; +import org.eclipse.xpect.runner.TestTitleProvider; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; + +@XpectImport(TestTitleProvider.class) +public interface IXpectDynamicTestFactory { + + @TestFactory + default Stream tests() { + return XpectDynamicTestFactory.xpectTests(getClass()); + } +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTest.java b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTest.java new file mode 100644 index 00000000..f7cc2d84 --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTest.java @@ -0,0 +1,64 @@ +package org.eclipse.xpect.dynamic; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.xpect.XjmTestMethod; +import org.eclipse.xpect.runner.ValidatingSetup; +import org.eclipse.xpect.runner.XpectTestGlobalState; +import org.eclipse.xpect.setup.ThisTestObject; +import org.eclipse.xpect.state.Creates; +import org.eclipse.xpect.state.StateContainer; +import org.junit.jupiter.api.DynamicTest; + +import com.google.common.base.Preconditions; + +public class XpectDynamicTest { + + private final String className; + private XjmTestMethod method; + private final StateContainer state; + + public XpectDynamicTest(StateContainer state, XjmTestMethod method) { + Preconditions.checkNotNull(method); + this.className = XpectTestGlobalState.INSTANCE.testClass().getName(); + this.method = method; + this.state = state; + } + + @Creates + public XpectDynamicTest create() { + return this; + } + + public XjmTestMethod getMethod() { + return method; + } + + public StateContainer getState() { + return state; + } + + public DynamicTest test() { + String testName = getName(); + return DynamicTest.dynamicTest(testName, () -> runInternal()); + } + + public String getName() { + String testName = formatDisplayName(method.getName(), className); + return testName; + } + + protected void runInternal() throws Throwable { + Object test = state.get(Object.class, ThisTestObject.class).get(); + try { + method.getJavaMethod().invoke(test); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private static String formatDisplayName(String name, String className) { + return String.format("%s(%s)", name, className); + } + +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestCase.java b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestCase.java new file mode 100644 index 00000000..6e5c7d94 --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestCase.java @@ -0,0 +1,174 @@ +package org.eclipse.xpect.dynamic; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xpect.XjmTestMethod; +import org.eclipse.xpect.XpectFile; +import org.eclipse.xpect.XpectInvocation; +import org.eclipse.xpect.XpectJavaModel; +import org.eclipse.xpect.runner.IXpectURIProvider; +import org.eclipse.xpect.runner.TestExecutor; +import org.eclipse.xpect.runner.ValidatingSetup; +import org.eclipse.xpect.setup.ThisRootTestClass; +import org.eclipse.xpect.state.Creates; +import org.eclipse.xpect.state.StateContainer; +import org.junit.jupiter.api.DynamicTest; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class XpectDynamicTestCase { + private List children; + private final StateContainer state; + private final XpectFile xpectFile; + + public XpectDynamicTestCase(StateContainer state, XpectFile file) { + this.xpectFile = file; + this.state = state; + } + + @Creates + public XpectDynamicTestCase create() { + return this; + } + + /** + *

+ * To run code before an Xpect test begins, extend this class and annotate the extending class with: + * {@code @org.eclipse.xpect.XpectReplace(org.eclipse.xpect.dynamic.XpectDynamicTestCase)} + *

+ *

+ * The extended class must then be included in the values of the annotation of the test case: + * {@code @org.eclipse.xpect.XpectImport({...class})} + *

+ */ + public void setUp() throws Throwable { + // nothing to set up + } + + /** + *

+ * To run code after an Xpect test finishes, extend this class and annotate the extending class with: + * {@code @org.eclipse.xpect.XpectReplace(org.eclipse.xpect.dynamic.XpectDynamicTestCase)} + *

+ *

+ * The extended class must then be included in the values of the annotation of the test case: + * {@code @org.eclipse.xpect.XpectImport({...class})} + *

+ */ + public void tearDown() throws Throwable { + // nothing to tear down + } + + protected List createChildren() { + List tests = Lists.newArrayList(); + if (xpectFile != null) { + /* + * With JUnit 4 runners, we can do setup validation before children run and state clean-up after children are done. + * With JUnit 5 we cannot. + * So the first test does setup validation and the last test does clean-up. + * Meaning their execution times will likely increase notably, when compared to the JUnit 4 reported time. + * Alternatively we can add a first and last test, for setup validation resp. clean-up. This means extra test nodes in the result, as well as extra test results. We then also have the problem of running tests if the setup validation failed. + * XXX: does JUnit 5 offer anything for dynamic tests, to improve this situation? + * As of writing this code, @BeforeEach and @AfterEach are only called before and after the factory method runs. + * We have a factory method with every URL we must run an Xpect test for, as those URLs are known only via an annotation. + */ + AtomicBoolean setupValidated = new AtomicBoolean(false); + AtomicBoolean setupValidationFailed = new AtomicBoolean(false); + AtomicInteger finishedChildren = new AtomicInteger(0); + XpectJavaModel xjm = xpectFile.getJavaModel(); + XjmTestMethod[] methods = xjm.getMethods().values().stream().filter(m -> m instanceof XjmTestMethod).toArray(XjmTestMethod[]::new); + XpectInvocation[] invocations = Iterables.toArray(xpectFile.getInvocations(), XpectInvocation.class); + int childrenCount = methods.length + invocations.length; + for (XjmTestMethod method : methods) { + DynamicTest test = createDynamicTest(method); + tests.add(wrapTest(test, childrenCount, setupValidated, setupValidationFailed, finishedChildren)); + } + for (XpectInvocation inv : invocations) { + DynamicTest test = createDynamicTest(inv); + tests.add(wrapTest(test, childrenCount, setupValidated, setupValidationFailed, finishedChildren)); + } + } + return tests; + } + + protected DynamicTest createDynamicTest(XjmTestMethod method) { + StateContainer childState = TestExecutor.createState(state, TestExecutor.createTestConfiguration(method)); + return childState.get(XpectDynamicTest.class).get().test(); + } + + protected DynamicTest createDynamicTest(XpectInvocation invocation) { + StateContainer childState = TestExecutor.createState(state, TestExecutor.createXpectConfiguration(invocation)); + DynamicTest test = childState.get(XpectInvocationDynamicTest.class).get().test(); + return test; + } + + protected DynamicTest wrapTest(DynamicTest test, final int childrenCount, AtomicBoolean validatedSetup, AtomicBoolean setupValidationFailed, AtomicInteger finishedChildren) { + return DynamicTest.dynamicTest(test.getDisplayName(), () -> { + try { + if (!validatedSetup.getAndSet(true)) { + // first test is running, validate setup + try { + state.get(ValidatingSetup.class).get().validate(); + } catch (Throwable t) { + setupValidationFailed.set(true); + throw t; + } + } + if (setupValidationFailed.get()) { + throw new AssertionError("Setup validation failed"); + } + try { + setUp(); + test.getExecutable().execute(); + } finally { + tearDown(); + } + } finally { + int finished = finishedChildren.incrementAndGet(); + if (finished >= childrenCount) { + // last test is done, do clean-up + state.invalidate(); + } + } + }); + } + + protected List getChildren() { + if (children == null) + children = createChildren(); + return children; + } + + public Class getJavaTestClass() { + return state.get(Class.class, ThisRootTestClass.class).get(); + } + + public IXpectURIProvider getURIProvider() { + return state.get(IXpectURIProvider.class).get(); + } + + public StateContainer getState() { + return state; + } + + public URI getUri() { + return xpectFile.eResource().getURI(); + } + + public XpectFile getXpectFile() { + return xpectFile; + } + + public String getName() { + IXpectURIProvider uriProvider = getURIProvider(); + URI uri = getUri(); + URI deresolved = uriProvider.deresolveToProject(uri); + String pathInProject = deresolved.trimSegments(1).toString(); + String fileName = deresolved.lastSegment(); + return fileName + ": " + pathInProject; + } +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestFactory.java b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestFactory.java new file mode 100644 index 00000000..837a8df7 --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectDynamicTestFactory.java @@ -0,0 +1,202 @@ +package org.eclipse.xpect.dynamic; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.plugin.EcorePlugin; +import org.eclipse.xpect.XpectFile; +import org.eclipse.xpect.XpectJavaModel; +import org.eclipse.xpect.XpectStandaloneSetup; +import org.eclipse.xpect.registry.ITestSuiteInfo; +import org.eclipse.xpect.runner.IXpectURIProvider; +import org.eclipse.xpect.runner.TestExecutor; +import org.eclipse.xpect.runner.ValidatingSetup; +import org.eclipse.xpect.runner.XpectTestFiles; +import org.eclipse.xpect.runner.XpectTestGlobalState; +import org.eclipse.xpect.runner.XpectURIProvider; +import org.eclipse.xpect.runner.XpectTestFiles.Builder; +import org.eclipse.xpect.runner.XpectTestFiles.FileRoot; +import org.eclipse.xpect.state.Configuration; +import org.eclipse.xpect.state.ResolvedConfiguration; +import org.eclipse.xpect.state.StateContainer; +import org.eclipse.xpect.util.AnnotationUtil; +import org.eclipse.xpect.util.IssueVisualizer; +import org.eclipse.xpect.util.XpectJavaModelManager; +import org.eclipse.xtext.resource.IResourceServiceProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceFactory; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.util.Strings; +import org.eclipse.xtext.validation.CheckMode; +import org.eclipse.xtext.validation.IResourceValidator; +import org.eclipse.xtext.validation.Issue; +import org.junit.ComparisonFailure; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; + +import com.google.inject.Injector; + +// a lot of copy-pasting from XpectRunner, however XpectRunner has protected methods that can use different model, injector, etc. via overriding getters, so its hard to extract code without causing damage +class XpectDynamicTestFactory { + + public static Stream xpectTests(Class testClass) { + XpectDynamicTestFactory factory = new XpectDynamicTestFactory(testClass); + return factory.getChildren(); + } + + private Stream children; + private Collection files; + private final Class testClass; + private final StateContainer state; + private final IXpectURIProvider uriProvider; + private final Injector xpectInjector; + private final XpectJavaModel xpectJavaModel; + + public XpectDynamicTestFactory(Class testClass) { + this.testClass = testClass; + this.uriProvider = findUriProvider(testClass); + this.xpectInjector = findXpectInjector(); + this.xpectJavaModel = XpectJavaModelManager.createJavaModel(testClass); + /* + * NOTE: + * Do this before the state creation, otherwise the parts that depend on + * the singleton won't initialize properly and test will fail to run! + */ + XpectTestGlobalState.INSTANCE.set(xpectJavaModel, testClass); + this.state = TestExecutor.createState(createRootConfiguration()); + } + + protected DynamicNode createChild(URI uri) { + try { + XtextResource resource = loadXpectResource(uri); + XpectFile file = loadXpectFile(resource); + Configuration cfg = createChildConfiguration(file); + StateContainer childState = new StateContainer(state, new ResolvedConfiguration(state.getConfiguration(), cfg)); + XpectDynamicTestCase testCase = childState.get(XpectDynamicTestCase.class).get(); + List tests = testCase.getChildren(); + if (tests.isEmpty()) { + // if there are no tests in the test case, we only validate the setup + return DynamicTest.dynamicTest(testCase.getName(), () -> { + try { + testCase.getState().get(ValidatingSetup.class).get().validate(); + testCase.setUp(); + } finally { + try { + tearDown(); + } finally { + testCase.getState().invalidate(); + } + } + }); + } + return DynamicContainer.dynamicContainer(testCase.getName(), tests); + } catch (IOException e) { + throw new AssertionError("Failed to create Xpect tests for URI: " + uri, e); + } + } + + protected Configuration createChildConfiguration(XpectFile file) { + return TestExecutor.createFileConfiguration(file); + } + + protected Stream createChildren(Class clazz) { + Collection fileUris = getFiles(); + // lazy stream + Stream tests = fileUris.stream().map(uri -> createChild(uri)); + return tests; + } + + protected Configuration createRootConfiguration() { + Configuration config = TestExecutor.createRootConfiguration(this.xpectJavaModel); + config.addDefaultValue(this); + config.addDefaultValue(IXpectURIProvider.class, this.uriProvider); + config.addFactory(XpectDynamicTestCase.class); + config.addFactory(XpectInvocationDynamicTest.class); + config.addFactory(XpectDynamicTest.class); + return config; + } + + protected IXpectURIProvider findUriProvider(Class clazz) { + String baseDir = System.getProperty("xpectBaseDir"); + String files = System.getProperty("xpectFiles"); + if (!Strings.isEmpty(baseDir) || !Strings.isEmpty(files)) { + Builder builder = new XpectTestFiles.Builder().relativeTo(FileRoot.PROJECT); + if (!Strings.isEmpty(baseDir)) + builder.withBaseDir(baseDir); + if (files != null) + for (String file : files.split(";")) { + String trimmed = file.trim(); + if (!"".equals(trimmed)) + builder.addFile(trimmed); + } + return builder.create(clazz); + + } + IXpectURIProvider provider = AnnotationUtil.newInstanceViaMetaAnnotation(clazz, XpectURIProvider.class, IXpectURIProvider.class); + if (provider != null) + return provider; + return new XpectTestFiles.Builder().relativeTo(FileRoot.CLASS).create(clazz); + } + + protected Injector findXpectInjector() { + IResourceServiceProvider rssp = IResourceServiceProvider.Registry.INSTANCE.getResourceServiceProvider(URI.createURI("foo.xpect")); + if (rssp != null) + return rssp.get(Injector.class); + if (!EcorePlugin.IS_ECLIPSE_RUNNING) + return new XpectStandaloneSetup().createInjectorAndDoEMFRegistration(); + throw new IllegalStateException("The language *.xpect is not activated"); + } + + // lazy stream, since otherwise preparation for each test runs as the tests are generated, not when each test eventually runs + public Stream getChildren() { + if (children == null) + children = createChildren(testClass); + return children; + } + + protected Collection getFiles() { + if (files == null) + files = uriProvider.getAllURIs(); + return files; + } + + protected XpectFile loadXpectFile(XtextResource res) throws IOException { + XpectFile file = !res.getContents().isEmpty() ? (XpectFile) res.getContents().get(0) : null; + if (file == null) + throw new IllegalStateException("Resource for " + res.getURI() + " is empty."); + validate(file); + validate(res); + return file; + } + + protected XtextResource loadXpectResource(URI uri) throws IOException { + XtextResourceFactory xtextResourceFactory = xpectInjector.getInstance(XtextResourceFactory.class); + XtextResource resource = (XtextResource) xtextResourceFactory.createResource(uri); + xpectJavaModel.eResource().getResourceSet().getResources().add(resource); + resource.load(null); + return resource; + } + + protected void validate(XpectFile file) { + XpectJavaModel model = file.getJavaModel(); + if (model == null || model.eIsProxy()) { + String fileName = file.eResource().getURI().lastSegment(); + String registry = ITestSuiteInfo.Registry.INSTANCE.toString(); + throw new IllegalStateException("Could not find test suite for " + fileName + ". Registry:\n" + registry); + } + } + + protected void validate(XtextResource res) { + IResourceValidator validator = res.getResourceServiceProvider().get(IResourceValidator.class); + List issues = validator.validate(res, CheckMode.ALL, CancelIndicator.NullImpl); + if (!issues.isEmpty()) { + String document = res.getParseResult().getRootNode().getText(); + String errors = new IssueVisualizer().visualize(document, issues); + throw new ComparisonFailure("Errors in " + res.getURI(), document.trim(), errors.trim()); + } + } +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectInvocationDynamicTest.java b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectInvocationDynamicTest.java new file mode 100644 index 00000000..ed99f50d --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/dynamic/XpectInvocationDynamicTest.java @@ -0,0 +1,96 @@ +package org.eclipse.xpect.dynamic; + +import java.util.List; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xpect.XjmXpectMethod; +import org.eclipse.xpect.XpectInvocation; +import org.eclipse.xpect.runner.DescriptionFactory; +import org.eclipse.xpect.runner.IXpectURIProvider; +import org.eclipse.xpect.runner.TestExecutor; +import org.eclipse.xpect.runner.XpectTestGlobalState; +import org.eclipse.xpect.state.Creates; +import org.eclipse.xpect.state.StateContainer; +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.DynamicTest; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +public class XpectInvocationDynamicTest { + + private final Class testClass; + private final XpectInvocation invocation; + private final StateContainer state; + + public XpectInvocationDynamicTest(StateContainer state, XpectInvocation invocation) { + Preconditions.checkNotNull(invocation); + this.testClass = XpectTestGlobalState.INSTANCE.testClass(); + this.invocation = invocation; + this.state = state; + } + + @Creates + public XpectInvocationDynamicTest create() { + return this; + } + + public DynamicTest test() { + String testName = getName(); + return DynamicTest.dynamicTest(testName, () -> runInternal()); + } + + public XpectInvocation getInvocation() { + return invocation; + } + + public XjmXpectMethod getMethod() { + return invocation.getMethod(); + } + + public StateContainer getState() { + return state; + } + + protected boolean isIgnore() { + return invocation.getFile().isIgnore() || invocation.isIgnore(); + } + + protected void runInternal() throws Throwable { + if (isIgnore()) { + throw new AssumptionViolatedException("Test is ignored"); + } + TestExecutor.runTest(state, invocation); + } + + public IXpectURIProvider getURIProvider() { + return state.get(IXpectURIProvider.class).get(); + } + + public String getName() { + IXpectURIProvider uriProvider = getURIProvider(); + String testClassName = testClass.getName(); + return getTestNameForInvocation(uriProvider, testClassName, invocation); + } + + private static String getTestNameForInvocation(IXpectURIProvider uriProvider, String testClassName, XpectInvocation invocation) { + URI uri = uriProvider.deresolveToProject(EcoreUtil.getURI(invocation)); + String title = DescriptionFactory.getTitle(invocation); + List ret = DescriptionFactory.extractXpectMethodNameAndResourceURI(uri.toString()); + String fragmentToXpectMethod = ret.get(0); + String pathToResource = ret.get(1); + + String text = fragmentToXpectMethod; + + if (!Strings.isNullOrEmpty(title)) + text = text + ": " + title; + // The test name has the following format + // errors~0: This is a comment 〔path/to/file.xt〕 + return formatDisplayName(text + " \u3014" + pathToResource + "\u3015", testClassName); + } + + private static String formatDisplayName(String name, String className) { + return String.format("%s(%s)", name, className); + } +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/model/XpectJavaModelImplCustom.java b/org.eclipse.xpect/src/org/eclipse/xpect/model/XpectJavaModelImplCustom.java index bbaf31ea..171e7431 100644 --- a/org.eclipse.xpect/src/org/eclipse/xpect/model/XpectJavaModelImplCustom.java +++ b/org.eclipse.xpect/src/org/eclipse/xpect/model/XpectJavaModelImplCustom.java @@ -39,7 +39,6 @@ import org.eclipse.xtext.common.types.JvmVisibility; import org.eclipse.xtext.util.Pair; import org.eclipse.xtext.util.Tuples; -import org.junit.Test; import org.junit.runner.RunWith; import org.eclipse.xpect.Environment; import org.eclipse.xpect.XjmClass; @@ -245,7 +244,9 @@ private void initTestClassMethods() { if (feature instanceof JvmOperation && feature.getVisibility() == JvmVisibility.PUBLIC) { if (JvmAnnotationUtil.isAnnotatedWith(feature, Xpect.class)) xpectMethods.put(Tuples.create(true, feature.getSimpleName()), Tuples.create(type, (JvmOperation) feature)); - if (JvmAnnotationUtil.isAnnotatedWith(feature, Test.class)) + if (JvmAnnotationUtil.isAnnotatedWith(feature, org.junit.Test.class)) + xpectMethods.put(Tuples.create(false, feature.getSimpleName()), Tuples.create(type, (JvmOperation) feature)); + if (JvmAnnotationUtil.isAnnotatedWith(feature, org.junit.jupiter.api.Test.class)) xpectMethods.put(Tuples.create(false, feature.getSimpleName()), Tuples.create(type, (JvmOperation) feature)); } diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectRunner.java b/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectRunner.java index f92b5d27..b9cd1f95 100644 --- a/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectRunner.java +++ b/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectRunner.java @@ -72,6 +72,12 @@ public XpectRunner(Class testClass) throws InitializationError { this.uriProvider = findUriProvider(testClass); this.xpectInjector = findXpectInjector(); this.xpectJavaModel = XpectJavaModelManager.createJavaModel(testClass); + /* + * NOTE: + * Do this before the state creation, otherwise the parts that depend on + * the singleton won't initialize properly and tests will fail to run! + */ + XpectTestGlobalState.INSTANCE.set(xpectJavaModel, testClass); this.state = TestExecutor.createState(createRootConfiguration()); } diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectTestGlobalState.java b/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectTestGlobalState.java new file mode 100644 index 00000000..a589541f --- /dev/null +++ b/org.eclipse.xpect/src/org/eclipse/xpect/runner/XpectTestGlobalState.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2024 Simeon Andreev 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 https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - Initial contribution and API + *******************************************************************************/ +package org.eclipse.xpect.runner; + +import org.eclipse.xpect.XpectJavaModel; + +/** + * @author Simeon Andreev - Initial contribution and API + */ +public class XpectTestGlobalState { + + public static final XpectTestGlobalState INSTANCE = new XpectTestGlobalState(); + + private XpectJavaModel model; + private Class testClass; + + public void set(XpectJavaModel model, Class testClass) { + this.model = model; + this.testClass = testClass; + } + + public XpectJavaModel model() { + return model; + } + + public Class testClass() { + return testClass; + } +} diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/services/XtResourceServiceProviderProvider.java b/org.eclipse.xpect/src/org/eclipse/xpect/services/XtResourceServiceProviderProvider.java index 9f769082..d954886c 100644 --- a/org.eclipse.xpect/src/org/eclipse/xpect/services/XtResourceServiceProviderProvider.java +++ b/org.eclipse.xpect/src/org/eclipse/xpect/services/XtResourceServiceProviderProvider.java @@ -17,6 +17,7 @@ import org.eclipse.xpect.XpectConstants; import org.eclipse.xpect.registry.ILanguageInfo; import org.eclipse.xpect.runner.XpectRunner; +import org.eclipse.xpect.runner.XpectTestGlobalState; import org.eclipse.xpect.util.IXtInjectorProvider; import com.google.inject.Injector; @@ -32,8 +33,8 @@ private XtResourceServiceProviderProvider() { } public IResourceServiceProvider get(URI uri, String contentType) { - if (XpectRunner.INSTANCE != null) { - Injector injector = IXtInjectorProvider.INSTANCE.getInjector(XpectRunner.INSTANCE.getXpectJavaModel(), uri); + if (XpectTestGlobalState.INSTANCE.model() != null) { + Injector injector = IXtInjectorProvider.INSTANCE.getInjector(XpectTestGlobalState.INSTANCE.model(), uri); if (injector != null) return injector.getInstance(IResourceServiceProvider.class); } diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/util/ClasspathUtil.java b/org.eclipse.xpect/src/org/eclipse/xpect/util/ClasspathUtil.java index e032b258..ff278d5d 100644 --- a/org.eclipse.xpect/src/org/eclipse/xpect/util/ClasspathUtil.java +++ b/org.eclipse.xpect/src/org/eclipse/xpect/util/ClasspathUtil.java @@ -26,6 +26,7 @@ import org.apache.log4j.Logger; import org.eclipse.xpect.runner.XpectRunner; +import org.eclipse.xpect.runner.XpectTestGlobalState; import com.google.common.base.Joiner; import com.google.common.collect.Sets; @@ -81,8 +82,8 @@ public static Collection findResources(String... fileNames) { } } // for some reason, ucl.getURLs() doesn't catch the current project in standalone maven surefire - if (XpectRunner.INSTANCE != null) { - Class clazz = XpectRunner.INSTANCE.getTestClass().getJavaClass(); + if (XpectTestGlobalState.INSTANCE.testClass() != null) { + Class clazz = XpectTestGlobalState.INSTANCE.testClass(); String[] segments = clazz.getName().split("\\."); String fileName = Joiner.on('/').join(segments) + ".class"; URL resource = clazz.getClassLoader().getResource(fileName); diff --git a/org.eclipse.xpect/src/org/eclipse/xpect/util/EnvironmentUtil.java b/org.eclipse.xpect/src/org/eclipse/xpect/util/EnvironmentUtil.java index 66e6807f..e07583e7 100644 --- a/org.eclipse.xpect/src/org/eclipse/xpect/util/EnvironmentUtil.java +++ b/org.eclipse.xpect/src/org/eclipse/xpect/util/EnvironmentUtil.java @@ -14,7 +14,7 @@ import org.eclipse.emf.ecore.plugin.EcorePlugin; import org.eclipse.xpect.Environment; -import org.eclipse.xpect.runner.XpectRunner; +import org.eclipse.xpect.runner.XpectTestGlobalState; import com.google.common.base.Joiner; @@ -22,7 +22,7 @@ public class EnvironmentUtil { public static final Environment ENVIRONMENT = detectEnvironement(); private static Environment detectEnvironement() { - if (XpectRunner.testClassloader != null) { + if (XpectTestGlobalState.INSTANCE.testClass() != null) { if (EcorePlugin.IS_ECLIPSE_RUNNING) return Environment.PLUGIN_TEST; else