From bd5dd6d117dbc5d2576a426486742b1f927263b2 Mon Sep 17 00:00:00 2001 From: U004458 Date: Thu, 6 Nov 2025 20:37:49 +0800 Subject: [PATCH] feat: Migrate com.avaloq.tools.ddk.xtext.format.test to Junit5 also made a junit5 copy of the AbstractXtextTestUtil and realign the recently migrated junit5 tests to use this instead, since the old one still uses the junti4 assertions --- .../test/export/util/ExportTestUtil.java | 4 +- .../META-INF/MANIFEST.MF | 3 +- .../ddk/xtext/format/FormatParsingTest.xtend | 17 +- .../builder/FormatBuilderParticipantTest.java | 20 +- .../formatting/FormatFormattingTest.java | 2 +- .../format/scoping/FormatScopingTest.java | 10 +- .../validation/FormatValidationTest.java | 6 +- .../xtext/test/format/FormatTestSuite.java | 10 +- .../test/format/util/FormatTestUtil.java | 4 +- .../test/util/GeneratorTestUtil.java | 4 +- .../test/jupiter/AbstractQuickFixTest.java | 455 ++++++++++++++++++ .../test/jupiter/AbstractXtextEditorTest.java | 203 ++++++++ .../xtext/test/jupiter/AbstractXtextTest.java | 1 - .../test/jupiter/AbstractXtextTestUtil.java | 408 ++++++++++++++++ 14 files changed, 1107 insertions(+), 40 deletions(-) create mode 100644 com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractQuickFixTest.java create mode 100644 com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextEditorTest.java create mode 100644 com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTestUtil.java diff --git a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java index b320d00455..a9be0cb950 100644 --- a/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java +++ b/com.avaloq.tools.ddk.xtext.export.test/src/com/avaloq/tools/ddk/xtext/test/export/util/ExportTestUtil.java @@ -12,7 +12,7 @@ import com.avaloq.tools.ddk.xtext.export.ExportConstants; import com.avaloq.tools.ddk.xtext.export.ui.internal.ExportActivator; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; import com.google.inject.Injector; @@ -29,7 +29,7 @@ private static final class InstanceHolder { private static final ExportTestUtil INSTANCE = new ExportTestUtil(); public static ExportTestUtil get() { - return INSTANCE; + return INSTANCE; } } diff --git a/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF index 34505f8c1c..9f1780e8ff 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF @@ -10,7 +10,6 @@ Fragment-Host: com.avaloq.tools.ddk.xtext.format.ui Require-Bundle: com.avaloq.tools.ddk.xtext.format, com.google.inject, com.avaloq.tools.ddk.xtext.test.core, - org.junit, org.mockito.mockito-core, org.eclipse.xtext.xbase.lib, org.eclipse.xtext.xbase.testing, @@ -18,7 +17,7 @@ Require-Bundle: com.avaloq.tools.ddk.xtext.format, org.eclipse.xtext.testing, junit-jupiter-api, junit-jupiter-engine, - junit-vintage-engine + junit-platform-suite-api Export-Package: com.avaloq.tools.ddk.xtext.test.format;x-internal=true, com.avaloq.tools.ddk.xtext.format;x-internal=true Automatic-Module-Name: com.avaloq.tools.ddk.xtext.format.test diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatParsingTest.xtend b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatParsingTest.xtend index a196c100c7..24562e7ab2 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatParsingTest.xtend +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatParsingTest.xtend @@ -6,13 +6,16 @@ package com.avaloq.tools.ddk.xtext.format import com.avaloq.tools.ddk.xtext.format.format.FormatConfiguration import com.google.inject.Inject import org.eclipse.xtext.testing.InjectWith -import org.eclipse.xtext.testing.XtextRunner import org.eclipse.xtext.testing.util.ParseHelper -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -@RunWith(XtextRunner) +import org.junit.jupiter.api.^extension.ExtendWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.junit.jupiter.api.Test +import static org.junit.jupiter.api.Assertions.assertNotNull +import static org.junit.jupiter.api.Assertions.assertTrue + + +@ExtendWith(InjectionExtension) @InjectWith(FormatInjectorProvider) class FormatParsingTest { @Inject @@ -23,8 +26,8 @@ class FormatParsingTest { val result = parseHelper.parse(''' Hello Xtext! ''') - Assert.assertNotNull(result) + assertNotNull(result) val errors = result.eResource.errors - Assert.assertTrue('''Unexpected errors: «errors.join(", ")»''', errors.isEmpty) + assertTrue(errors.isEmpty, '''Unexpected errors: «errors.join(", ")»''') } } diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/builder/FormatBuilderParticipantTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/builder/FormatBuilderParticipantTest.java index 179f866d41..a6bfd8bc35 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/builder/FormatBuilderParticipantTest.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/builder/FormatBuilderParticipantTest.java @@ -10,8 +10,8 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.format.builder; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -21,12 +21,12 @@ import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.resource.IResourceDescription.Delta; import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; import com.avaloq.tools.ddk.xtext.format.ui.builder.FormatBuilderParticipant; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTest; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; import com.google.common.collect.ImmutableList; import com.google.inject.Guice; import com.google.inject.Injector; @@ -71,8 +71,8 @@ protected void beforeAllTests() { public void hasCorrectExtensionTest() { IResourceServiceProvider resourceServiceProvider = mock(IResourceServiceProvider.class); when(resourceServiceProvider.canHandle(argThat(new IsUri()))).thenReturn(true, false); - assertTrue("Check if the delta resource has correct extension", participant.hasCorrectExtension(delta, resourceServiceProvider)); - assertFalse("Check if the delta resource has incorrect extension", participant.hasCorrectExtension(delta, resourceServiceProvider)); + assertTrue(participant.hasCorrectExtension(delta, resourceServiceProvider), "Check if the delta resource has correct extension"); + assertFalse(participant.hasCorrectExtension(delta, resourceServiceProvider), "Check if the delta resource has incorrect extension"); } /** @@ -81,11 +81,11 @@ public void hasCorrectExtensionTest() { @Test public void isSourceOriginatedTest() { when(uriCorrect.segments()).thenReturn(CORRECT_URI_SEGMENTS); - assertTrue("Check if the delta resource has correct URI and comes from SRC directory", participant.isSourceOriginated(delta)); + assertTrue(participant.isSourceOriginated(delta), "Check if the delta resource has correct URI and comes from SRC directory"); when(uriCorrect.segments()).thenReturn(BIN_URI_SEGMENTS); - assertFalse("Check if the delta resource has correct URI and does not come from SRC directory", participant.isSourceOriginated(delta)); + assertFalse(participant.isSourceOriginated(delta), "Check if the delta resource has correct URI and does not come from SRC directory"); when(uriCorrect.segments()).thenReturn(INCORRECT_URI_SEGMENTS); - assertFalse("Check if the delta resource has incorrect URI and comes from SRC directory", participant.isSourceOriginated(delta)); + assertFalse(participant.isSourceOriginated(delta), "Check if the delta resource has incorrect URI and comes from SRC directory"); } @Override diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/formatting/FormatFormattingTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/formatting/FormatFormattingTest.java index 2f14dc5670..0da7073bd3 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/formatting/FormatFormattingTest.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/formatting/FormatFormattingTest.java @@ -12,7 +12,7 @@ import com.avaloq.tools.ddk.xtext.test.TestSource; import com.avaloq.tools.ddk.xtext.test.format.util.FormatTestUtil; -import com.avaloq.tools.ddk.xtext.test.formatting.AbstractFormattingTest; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractFormattingTest; public class FormatFormattingTest extends AbstractFormattingTest { diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/scoping/FormatScopingTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/scoping/FormatScopingTest.java index d236fee5bc..d088a6a148 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/scoping/FormatScopingTest.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/scoping/FormatScopingTest.java @@ -10,7 +10,7 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.format.scoping; -import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.List; import java.util.Set; @@ -27,13 +27,13 @@ import org.eclipse.xtext.nodemodel.ICompositeNode; import org.eclipse.xtext.nodemodel.ILeafNode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.avaloq.tools.ddk.xtext.format.format.FormatConfiguration; import com.avaloq.tools.ddk.xtext.format.format.FormatPackage; import com.avaloq.tools.ddk.xtext.format.format.GroupBlock; import com.avaloq.tools.ddk.xtext.test.format.util.FormatTestUtil; -import com.avaloq.tools.ddk.xtext.test.scoping.AbstractScopingTest; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractScopingTest; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -112,7 +112,7 @@ public void allGrammarsScoped() { public void keywordScoped() { AbstractRule parserRuleA = grammarA.getRules().get(0); Set keywordURIs = Sets.newHashSet(Iterables.transform(GrammarUtil.containedKeywords(parserRuleA), TO_URI)); - assertFalse("No keywords found", keywordURIs.isEmpty()); + assertFalse(keywordURIs.isEmpty(), "No keywords found"); assertScope(formatC.getRules().get(0), FormatPackage.Literals.GRAMMAR_ELEMENT_REFERENCE__KEYWORD, keywordURIs); AbstractRule parserRuleC = grammarC.getRules().get(0); assertScope(formatA.getRules().get(0), FormatPackage.Literals.GRAMMAR_ELEMENT_REFERENCE__KEYWORD, keywordURIs); @@ -128,7 +128,7 @@ public void keywordScoped() { public void assignmentScoped() { AbstractRule parserRuleA = grammarA.getRules().get(0); Set assignmentURIs = Sets.newHashSet(Iterables.transform(GrammarUtil.containedAssignments(parserRuleA), TO_URI)); - assertFalse("No assignments found", assignmentURIs.isEmpty()); + assertFalse(assignmentURIs.isEmpty(), "No assignments found"); assertScope(formatC.getRules().get(0), FormatPackage.Literals.GRAMMAR_ELEMENT_REFERENCE__ASSIGNMENT, assignmentURIs); assertScope(formatA.getRules().get(0), FormatPackage.Literals.GRAMMAR_ELEMENT_REFERENCE__ASSIGNMENT, assignmentURIs); } diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/validation/FormatValidationTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/validation/FormatValidationTest.java index 8fcc918abb..62a80a86b3 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/validation/FormatValidationTest.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/validation/FormatValidationTest.java @@ -10,16 +10,16 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.format.validation; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.avaloq.tools.ddk.xtext.format.format.FormatConfiguration; import com.avaloq.tools.ddk.xtext.test.format.util.FormatTestUtil; -import com.avaloq.tools.ddk.xtext.test.validation.AbstractValidationTest; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractValidationTest; import com.google.common.collect.Lists; diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java index 6de2e292f3..4baf7095fe 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java @@ -10,8 +10,8 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.test.format; -import org.junit.runner.RunWith; -import org.junit.runners.Suite; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; import com.avaloq.tools.ddk.xtext.format.builder.FormatBuilderParticipantTest; import com.avaloq.tools.ddk.xtext.format.formatting.FormatFormattingTest; @@ -20,10 +20,10 @@ /** - * Empty class serving only as holder for JUnit4 annotations. + * Empty class serving only as holder for JUnit5 annotations. */ -@RunWith(Suite.class) -@Suite.SuiteClasses({FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class}) +@Suite +@SelectClasses({FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class}) public class FormatTestSuite { } diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/util/FormatTestUtil.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/util/FormatTestUtil.java index 57cd3a75f0..dee3a3a51a 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/util/FormatTestUtil.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/util/FormatTestUtil.java @@ -10,7 +10,7 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.test.format.util; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; @@ -20,9 +20,9 @@ import com.avaloq.tools.ddk.xtext.format.FormatConstants; import com.avaloq.tools.ddk.xtext.format.format.FormatConfiguration; import com.avaloq.tools.ddk.xtext.format.ui.internal.FormatActivator; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTestUtil; import com.avaloq.tools.ddk.xtext.test.ITestProjectManager; import com.avaloq.tools.ddk.xtext.test.PluginTestProjectManager; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; import com.google.inject.Injector; diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/util/GeneratorTestUtil.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/util/GeneratorTestUtil.java index e50409d268..4ff3726b6f 100644 --- a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/util/GeneratorTestUtil.java +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/util/GeneratorTestUtil.java @@ -11,7 +11,7 @@ package com.avaloq.tools.ddk.xtext.generator.test.util; import com.avaloq.tools.ddk.xtext.expression.ExpressionStandaloneSetup; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; import com.google.inject.Injector; @@ -28,7 +28,7 @@ private static final class InstanceHolder { private static final GeneratorTestUtil INSTANCE = new GeneratorTestUtil(); public static GeneratorTestUtil get() { - return INSTANCE; + return INSTANCE; } } diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractQuickFixTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractQuickFixTest.java new file mode 100644 index 0000000000..a082548ee8 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractQuickFixTest.java @@ -0,0 +1,455 @@ +/******************************************************************************* + * Copyright (c) 2025 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.test.jupiter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.xtext.resource.XtextSyntaxDiagnostic; +import org.eclipse.xtext.ui.editor.model.edit.IModificationContext; +import org.eclipse.xtext.ui.editor.model.edit.IssueModificationContext; +import org.eclipse.xtext.ui.editor.quickfix.IssueResolution; +import org.eclipse.xtext.ui.editor.quickfix.IssueResolutionProvider; +import org.eclipse.xtext.validation.Issue; +import org.hamcrest.MatcherAssert; +import org.hamcrest.text.IsEqualCompressingWhiteSpace; + +import com.avaloq.tools.ddk.check.runtime.quickfix.ICoreModificationContext; +import com.avaloq.tools.ddk.check.runtime.ui.quickfix.CoreIssueModificationContext; +import com.avaloq.tools.ddk.check.runtime.ui.quickfix.IssueResolutionWrapper; +import com.avaloq.tools.ddk.xtext.ui.util.UiThreadDispatcher; +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; + + +/** + * Test existence and application of QuickFixes for a given language. + * Subclasses provide a validation test file which contains the model the test is based on. + * The name of the quick fix test file must be provided in the method getQuickFixFileName(). + * Besides overriding the abstract method getQuickFixFileName(), subclasses have to implement + * the test method itself which tests for existence and resolutions of the diagnostic issues. + */ +@SuppressWarnings({"PMD.UseObjectForClearerAPI", "nls"}) +public abstract class AbstractQuickFixTest extends AbstractXtextEditorTest { + + private IssueResolutionProvider getIssueResolutionProvider() { + return getXtextTestUtil().get(IssueResolutionProvider.class); + } + + /** + * Results of the diagnostic, a list of Issue. + * + * @return the list of issues + */ + private List getIssueList() { + return getXtextTestUtil().getIssues(getDocument()); + } + + /** + * Set up test by opening a text editor with the validation test file and triggering validation. + */ + @Override + protected void beforeAllTests() { + super.beforeAllTests(); + closeEditor(getEditor(), false); + } + + @Override + protected void beforeEachTest() { + super.beforeEachTest(); + if (getTestSource() != null) { + openEditor(getTestSourceFileName()); + } + } + + @Override + protected void afterEachTest() { + super.afterEachTest(); + closeEditor(getEditor(), false); + } + + /** + * Assert that the diagnostic result (issueList) contains an Issue of the given issueCode. + * + * @param issueCode + * the code of the expected issue, may be {@code null} + */ + protected void assertHasIssue(final String issueCode) { + assertFalse(issuesWith(issueCode).isEmpty(), "Issue " + issueCode + " is empty"); + } + + /** + * Assert that diagnostic result (issueList) contains a QuickFix of the given issueCode. + * + * @param issueCode + * the code of the issue for which a quickfix is expected to exist, may be {@code null} + */ + protected void assertHasQuickFix(final String issueCode) { + assertFalse(resolutionsFor(issueCode).isEmpty(), "No resolutions found for issue " + issueCode); + } + + /** + * Assert that diagnostic result (issueList) contains a QuickFix of the given issueCode. + * + * @param issueCode + * the code of the issue for which a quickfix is expected to exist, may be {@code null} + * @param quickfixLabel + * the label of the quickfix, may be {@code null} + */ + protected void assertHasQuickFix(final String issueCode, final String quickfixLabel) { + assertFalse(resolutionsFor(issueCode, quickfixLabel).isEmpty(), "No resolutions found for issue " + issueCode); + } + + /** + * Assert that diagnostic result (issueList) contains the exact number of the given quickfix label in the proposal of the given issueCode. + * + * @param issueCode + * the code of the issue for which a quickfix is expected to exist, may be {@code null} + * @param quickfixLabel + * the label of the quickfix, may be {@code null} + * @param numberOfQuickfixProposal + * the number of expected quickfix proposal, must not be {@code null} + */ + protected void assertHasQuickFix(final String issueCode, final String quickfixLabel, final int numberOfQuickfixProposal) { + assertEquals(resolutionsFor(issueCode, quickfixLabel).size(), numberOfQuickfixProposal, "Number of resolutions found for issue " + issueCode + + " does not match the expected number of quickfix proposal"); + } + + /** + * Assert that diagnostic result (issueList) of a given source does not contain a QuickFix of the given issueCode. + * + * @param sourceFileName + * the source file name, must not be {@code null} + * @param sourceFileContent + * the source file content, must not be {@code null} + * @param issueCode + * the issue code for which no QuickFix must exist, must not be {@code null} + */ + protected void assertNoQuickFix(final String sourceFileName, final String sourceFileContent, final String issueCode) { + assertNoQuickFix(sourceFileName, sourceFileContent, issueCode, null); + } + + /** + * Assert that diagnostic result (issueList) of a given source does not contain a QuickFix of the given issueCode. + * + * @param sourceFileName + * the source file name, must not be {@code null} + * @param sourceFileContent + * the source file content, must not be {@code null} + * @param issueCode + * the issue code for which no QuickFix must exist, must not be {@code null} + * @param quickfixLabel + * the quickfix label, may be {@code null} + */ + protected void assertNoQuickFix(final String sourceFileName, final String sourceFileContent, final String issueCode, final String quickfixLabel) { + createTestSource(sourceFileName, sourceFileContent); + openEditor(sourceFileName); + try { + assertTrue(resolutionsFor(issueCode, quickfixLabel).isEmpty(), "No resolutions expected for issue " + issueCode + " on source " + sourceFileName); + } finally { + closeEditor(getEditor(), false); + } + } + + /** + * Assert that application of the quick fixes for the given issueCode resolve the problem. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + */ + protected void assertQuickFixSuccessful(final String issueCode) { + assertQuickFixSuccessful(issueCode, null); + } + + /** + * Assert that application of the quick fixes for the given issueCode and label resolve the problem. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quickfix, may be {@code null} + */ + protected void assertQuickFixSuccessful(final String issueCode, final String quickfixLabel) { + for (final IssueResolution issueResolution : sortResolutionsByOffsetDecreasing(resolutionsFor(issueCode, quickfixLabel))) { + UiThreadDispatcher.dispatchAndWait(new Runnable() { + @Override + public void run() { + issueResolution.apply(); + } + }); + } + waitForValidation(); + assertTrue(resolutionsFor(issueCode, quickfixLabel).isEmpty(), "Resolutions for issue " + issueCode + " with quickfix " + quickfixLabel + "are not empty"); + } + + /** + * Sort issue resolutions by offset in document decreasing. + * + * @param resolutions + * resolutions to sort + * @return a copy of {@code resolutions} sorted by offset in document decreasing + */ + protected List sortResolutionsByOffsetDecreasing(final List resolutions) { + + final Function getLocationFunction = new Function() { + + @Override + public Integer apply(final IssueResolution from) { + if (from != null) { + if (from instanceof IssueResolutionWrapper) { + ICoreModificationContext context = ((IssueResolutionWrapper) from).getCoreModificationContext(); + if (context instanceof CoreIssueModificationContext) { + return ((CoreIssueModificationContext) context).getIssue().getOffset(); + } + } else { + IModificationContext context = from.getModificationContext(); + if (context instanceof IssueModificationContext) { + return ((IssueModificationContext) context).getIssue().getOffset(); + } + } + } + return Integer.MIN_VALUE; + } + }; + Ordering ordering = Ordering.natural().onResultOf(getLocationFunction).reverse(); + return new ArrayList(ordering.sortedCopy(resolutions)); + } + + /** + * Assert that the test source has no syntax error. + */ + protected void assertNoSyntaxError() { + assertFalse(Iterables.any(getTestSource().getXtextResource().getErrors(), Predicates.instanceOf(XtextSyntaxDiagnostic.class)), "The source has syntax errors"); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text. + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param sourceFileName + * the name of the source being tested + * @param sourceContent + * the content of the source being tested + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + */ + protected void assertQuickFixExistsAndSuccessfulInKernelSource(final String issueCode, final String quickfixLabel, final String sourceFileName, final String sourceContent, final String expectedContent) { + assertQuickFixExistsAndSuccessful(issueCode, quickfixLabel, sourceFileName, sourceContent, expectedContent, false); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text (ignoring formatting). + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param sourceFileName + * the name of the source being tested + * @param sourceContent + * the content of the source being tested + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + */ + protected void assertQuickFixExistsAndSuccessfulInKernelSourceIgnoreFormatting(final String issueCode, final String quickfixLabel, final String sourceFileName, final String sourceContent, final String expectedContent) { + assertQuickFixExistsAndSuccessful(issueCode, quickfixLabel, sourceFileName, sourceContent, expectedContent, true); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text. + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param sourceFileName + * the name of the source being tested + * @param sourceContent + * the content of the source being tested + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + */ + protected void assertQuickFixExistsAndSuccessfulInCustomerSource(final String issueCode, final String quickfixLabel, final String sourceFileName, final String sourceContent, final String expectedContent) { + assertQuickFixExistsAndSuccessful(issueCode, quickfixLabel, CUSTOMER_SOURCE_PREFIX.concat(sourceFileName), sourceContent, expectedContent, false); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text (ignoring formatting). + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param sourceFileName + * the name of the source being tested + * @param sourceContent + * the content of the source being tested + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + */ + protected void assertQuickFixExistsAndSuccessfulInCustomerSourceIgnoreFormatting(final String issueCode, final String quickfixLabel, final String sourceFileName, final String sourceContent, final String expectedContent) { + assertQuickFixExistsAndSuccessful(issueCode, quickfixLabel, CUSTOMER_SOURCE_PREFIX.concat(sourceFileName), sourceContent, expectedContent, true); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text. + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param sourceFileName + * the name of the source being tested + * @param sourceContent + * the content of the source being tested + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + * @param ignoreFormatting + * ignore formatting + */ + private void assertQuickFixExistsAndSuccessful(final String issueCode, final String quickfixLabel, final String sourceFileName, final String sourceContent, final String expectedContent, final boolean ignoreFormatting) { + createTestSource(sourceFileName, sourceContent); + openEditor(sourceFileName); + assertQuickFixExistsAndSuccessful(issueCode, quickfixLabel, expectedContent, ignoreFormatting); + closeEditor(getEditor(), false); + } + + /** + * Assert that application of the target quickfix was successful and the text of the resulting document equals the expected text. + * The method ensures that there is one and only one quickfix for the given issue code with the given label. + * + * @param issueCode + * the code of the issue that should have been fixed, may be {@code null} + * @param quickfixLabel + * the label of the quick fix that should be applied, may be {@code null} + * @param expectedContent + * the name of the file containing the expected result after applying the quick fix + * @param ignoreFormatting + * ignore formatting + */ + private void assertQuickFixExistsAndSuccessful(final String issueCode, final String quickfixLabel, final String expectedContent, final boolean ignoreFormatting) { + // Assert amount of quickfixes + int resolutionCount = resolutionsFor(issueCode, quickfixLabel).size(); + assertEquals(resolutionCount, 1, String.format("There must be exactly one quickfix with label '%s' for issue '%s', but found '%d'.", quickfixLabel, issueCode, resolutionCount)); + // Apply quickfix + UiThreadDispatcher.dispatchAndWait(new Runnable() { + @Override + public void run() { + List resolutions = resolutionsFor(issueCode, quickfixLabel); + if (!resolutions.isEmpty()) { + resolutions.get(0).apply(); + } + } + }); + waitForValidation(); + assertTrue(resolutionsFor(issueCode, quickfixLabel).isEmpty(), "Resolutions for issue " + issueCode + " with quickfix " + quickfixLabel + "are not empty"); + String actualContent = getDocument().get(); + assertQuickFixProducesExpectedOutput(expectedContent, actualContent, ignoreFormatting); + } + + /** + * Assert that quick fix produces expected output. + * + * @param expectedContent + * the expected content + * @param actualContent + * the actual content + * @param ignoreFormatting + * the ignore formatting + */ + private void assertQuickFixProducesExpectedOutput(final String expectedContent, final String actualContent, final boolean ignoreFormatting) { + String message = "Quickfix didn't produce the expected output."; + String expected = expectedContent.replaceAll(CR_LF, LF); + String actual = actualContent.replaceAll(CR_LF, LF); + if (ignoreFormatting) { + MatcherAssert.assertThat(message, actual, IsEqualCompressingWhiteSpace.equalToCompressingWhiteSpace(expected)); + } else { + assertEquals(message, expected, actual); + } + } + + /** + * Finds all issues with a specific issue code. + * + * @param issueCode + * to filter for, may be {@code null} + * @return {@link List} of issues with a specific code + */ + private List issuesWith(final String issueCode) { + List issues = new ArrayList(); + if (issueCode == null) { + return issues; + } + for (Issue issue : getIssueList()) { + if (issueCode.equals(issue.getCode())) { + issues.add(issue); + } + } + return issues; + } + + /** + * Finds all resolutions for issues with a specific issue code. + * + * @param issueCode + * to find resolutions for, may be {@code null} + * @return {@link List} of resolutions for issues with a specific code + */ + private List resolutionsFor(final String issueCode) { + return resolutionsFor(issueCode, null); + } + + /** + * Finds all resolutions for issues with a specific issue code. + * + * @param issueCode + * to find resolutions for, may be {@code null} + * @param quickfixLabel + * to find resolutions for, may be {@code null} + * @return {@link List} of resolutions for issues with a specific code + */ + private List resolutionsFor(final String issueCode, final String quickfixLabel) { + final List resolutions = new ArrayList(); + + for (final Issue issue : issuesWith(issueCode)) { + UiThreadDispatcher.dispatchAndWait(new Runnable() { + @Override + public void run() { + if (quickfixLabel == null) { + resolutions.addAll(getIssueResolutionProvider().getResolutions(issue)); + } else { + for (IssueResolution r : getIssueResolutionProvider().getResolutions(issue)) { + if (quickfixLabel.equals(r.getLabel())) { + resolutions.add(r); + } + } + } + } + }); + } + + return resolutions; + } +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextEditorTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextEditorTest.java new file mode 100644 index 0000000000..0b443eb511 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextEditorTest.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * Copyright (c) 2025 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.test.jupiter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.eclipse.emf.common.util.WrappedException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.ui.editor.XtextEditor; +import org.eclipse.xtext.ui.editor.XtextSourceViewer; +import org.eclipse.xtext.ui.editor.model.IXtextDocument; +import org.eclipse.xtext.ui.editor.reconciler.XtextReconciler; +import org.eclipse.xtext.util.concurrent.IUnitOfWork; + +import com.avaloq.tools.ddk.xtext.test.TestSource; +import com.avaloq.tools.ddk.xtext.test.XtextTestSource; + + +/** + * AbstractXtextEditorTest provides convenient setup and access functionality for tests that require an xtext editor. + */ +@SuppressWarnings("nls") +public abstract class AbstractXtextEditorTest extends AbstractXtextMarkerBasedTest { + + private static final String EDITOR_MUST_NOT_BE_DIRTY = "Editor must not be dirty - this indicates state carried over"; + private static final String EDITOR_HAS_NO_DOCUMENT = "Editor has no document"; + private static final String EDITOR_COULD_NOT_BE_OPENED_WITH_URI = "Editor could not be opened with URI: "; + + protected static final String CR_LF = "\r\n"; + protected static final String LF = "\n"; + + @Override + protected void beforeAllTests() { + super.beforeAllTests(); + // For Xtend-based tests there is no default test source associated with the test class + TestSource testSource = getTestSource(); + if (testSource != null) { + openEditor(testSource); + } + } + + @Override + protected void afterAllTests() { + closeOpenEditor(); + super.afterAllTests(); + } + + @Override + protected void beforeApplyAssertions(final XtextTestSource testSource) { + super.beforeApplyAssertions(testSource); + openEditor(testSource); + } + + @Override + protected void afterValidate() { + closeOpenEditor(); + super.afterValidate(); + } + + /** + * Opens the editor with the given test source. + * + * @param testSource + * the test source to open, not {@code null} + */ + private void openEditor(final TestSource testSource) { + // if openEditor returns NULL, then one possible cause might be that the Activator + // has not been set correctly in the presentation plug-in MANIFEST of that grammar. + XtextEditor editor = getXtextTestUtil().openEditor(testSource.getUri()); + assertNotNull(editor, EDITOR_COULD_NOT_BE_OPENED_WITH_URI + testSource.getUri()); + getTestInformation().putTestObject(XtextEditor.class, editor); + assertNotNull(getDocument(), EDITOR_HAS_NO_DOCUMENT); + assertFalse(getEditor().isDirty(), EDITOR_MUST_NOT_BE_DIRTY); + } + + /** + * Open editor of the test source with a given file name. + * + * @param fileName + * file name of the source to open editor for, must not be {@code null} + */ + protected void openEditor(final String fileName) { + openEditor(getTestSource(fileName)); + } + + /** + * Closes the currently open editor. + */ + private void closeOpenEditor() { + final XtextEditor editor = getEditor(); + if (editor != null) { + closeEditor(editor, false); + } + } + + /** + * Returns the editor. + * + * @return the editor + */ + protected XtextEditor getEditor() { + return (XtextEditor) getTestInformation().getTestObject(XtextEditor.class); + } + + /** + * Closes the given editor-part - contrary to {@link org.eclipse.ui.texteditor.AbstractTextEditor#close(boolean)} this call is blocking! + * + * @param editor + * the editor to close + * @param save + * true if should save before close, false otherwise + */ + protected void closeEditor(final IEditorPart editor, final boolean save) { + Object editorJobs = getTestUtil().getEditorJobFamily(editor); + PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().closeEditor(editor, save); + } + }); + if (editorJobs != null) { + waitForJobsOfFamily(editorJobs); + } + } + + /** + * Returns the viewer. + * + * @return the viewer + */ + protected XtextSourceViewer getViewer() { + return (XtextSourceViewer) getEditor().getInternalSourceViewer(); + } + + /** + * Returns the document. + * + * @return the document + */ + protected IXtextDocument getDocument() { + return getEditor().getDocument(); + } + + /** + * Insert the given value at offset in {@link #getDocument()}. + * + * @param offset + * the offset + * @param value + * the value + */ + protected void insertAtOffset(final int offset, final String value) { + PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + // the modify() is invoked to ensure a (fast-only) re-validation of the document is triggered + getDocument().modify(new IUnitOfWork() { + @Override + public java.lang.Void exec(final XtextResource state) { + try { + getDocument().replace(offset, 0, value); + } catch (BadLocationException e) { + throw new WrappedException("Could not insert \"" + value + "\" at affset " + offset, e); + } + return null; + } + }); + } + }); + } + + /** + * Test that the editor does not allow "Save as...". + * + * @deprecated Provide this test method in an appropriate test class for the editor under test, if the editor shall not allow "Save as...". + */ + @Deprecated + public void testSaveAsDisallowed() { + final XtextEditor editor = getEditor(); + assertFalse(editor.isSaveAsAllowed(), "Editor must not allow 'Save as...'"); + } + + @Override + protected void waitForValidation() { + // Editor tests frequently work by modifying the document. We first need to wait for the reconciler to run, otherwise we may + // actually get results from before a document change is reflected in the document's resource, leading to spurious errors. + // Note that the XtextReconciler runs with a delay of 500ms. + waitForJobsOfFamily(XtextReconciler.class.getName()); + super.waitForValidation(); + } +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTest.java index 01a43815b9..5f843bc4e3 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTest.java @@ -13,7 +13,6 @@ import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.resource.XtextResource; -import com.avaloq.tools.ddk.xtext.test.AbstractXtextTestUtil; import com.avaloq.tools.ddk.xtext.test.XtextTestSource; diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTestUtil.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTestUtil.java new file mode 100644 index 0000000000..7a28733d39 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextTestUtil.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * Copyright (c) 2025 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.test.jupiter; //NOPMD + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import org.eclipse.emf.common.util.Diagnostic; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.common.util.WrappedException; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.util.Diagnostician; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.intro.IIntroPart; +import org.eclipse.xtext.diagnostics.AbstractDiagnostic; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.ILeafNode; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.FileExtensionProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.serializer.ISerializer; +import org.eclipse.xtext.ui.editor.GlobalURIEditorOpener; +import org.eclipse.xtext.ui.editor.XtextEditor; +import org.eclipse.xtext.ui.editor.model.IXtextDocument; +import org.eclipse.xtext.ui.testing.util.ResourceLoadHelper; +import org.eclipse.xtext.util.StringInputStream; +import org.eclipse.xtext.validation.AbstractValidationDiagnostic; +import org.eclipse.xtext.validation.FeatureBasedDiagnostic; +import org.eclipse.xtext.validation.Issue; +import org.eclipse.xtext.validation.RangeBasedDiagnostic; +import org.eclipse.xtext.xbase.lib.Pair; + +import com.avaloq.tools.ddk.xtext.test.AbstractTestUtil; +import com.avaloq.tools.ddk.xtext.test.model.ModelUtil; +import com.avaloq.tools.ddk.xtext.test.validation.ValidationHelper; +import com.avaloq.tools.ddk.xtext.ui.util.Function; +import com.avaloq.tools.ddk.xtext.ui.util.UiThreadDispatcher; +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.Injector; + + +/** + * Utility for Xtext tests. + */ +@SuppressWarnings("nls") +public abstract class AbstractXtextTestUtil extends AbstractTestUtil implements ResourceLoadHelper /* , IInjectorProvider */ { + + private final ModelUtil modelUtil = new ModelUtil(); + private final ValidationHelper validationHelper = new ValidationHelper(); + + private static final String ERROR = "ERROR %d: "; + private static final String ERROR_MARKER = ""; + private static final String SPLITTER = "------------------------\n"; + + /** + * Returns the current injector used in this plugin. + * + * @return the current injector for this language + * @deprecated this method should be implemented only and not called directly; use {@link #get(Class)} instead + */ + @Deprecated + protected abstract Injector getInjector(); + + /** + * Opens an editor for a specific {@link URI}. + * + * @param uri + * to open editor for + * @return editor opened + */ + public XtextEditor openEditor(final URI uri) { + XtextEditor editor = UiThreadDispatcher.dispatchAndWait(new Function() { + @Override + public XtextEditor run() { + closeWelcomePage(); + return (XtextEditor) get(GlobalURIEditorOpener.class).open(uri, false); + } + }); + waitForEditorJobs(editor); + return editor; + } + + /** + * Parses the given instance and returns the model representation. + * + * @param sourceFileName + * to associate the source with, used to determine the source content type + * @param content + * String representation of the content to parse + * @return model representation + * @throws IOException + * may be thrown if the instance cannot be parsed + */ + public final EObject getModel(final String sourceFileName, final String content) throws IOException { + XtextResource resource = getResource(getTestProjectManager().createTestSourceUri(sourceFileName), content); + return resource.getParseResult().getRootASTElement(); + } + + /** + * Creates a resource with the given URI and content. + * + * @param uri + * to associate the model with + * @param content + * String representation of the create a resource from + * @return {@link XtextResource} created + * @throws IOException + * may be thrown when trying to load the given content + */ + protected final XtextResource getResource(final URI uri, final String content) throws IOException { + StringInputStream instanceStream = new StringInputStream(content); + XtextResourceSet rs = getResourceSet(); + XtextResource resource = (XtextResource) rs.createResource(uri); + rs.getResources().add(resource); + resource.load(instanceStream, null); + EcoreUtil.resolveAll(resource); + return resource; + } + + public String getDefaultSourceName() { + return "mytestmodel"; + } + + @Override + public XtextResource getResourceFor(final InputStream stream) { + XtextResourceSet rs = getResourceSet(); + XtextResource resource = (XtextResource) rs.createResource(getTestProjectManager().createTestSourceUri(getDefaultSourceName() + '.' + getFileExtension())); + rs.getResources().add(resource); + try { + resource.load(stream, null); + } catch (IOException e) { + throw new WrappedException("Could not create XtextResource from input stream.", e); + } + EcoreUtil.resolveAll(resource); + return resource; + } + + /** + * Validates the provided document and returns a list of issues found. + * + * @param document + * to validate + * @return list of issues found + */ + public List getIssues(final IXtextDocument document) { + return validationHelper.getIssues(document); + } + + /** + * Use current injector to get a class instance. + * + * @param + * Class type + * @param clazz + * class of object to get. + * @return object of given class + */ + public T get(final Class clazz) { + return getInjector().getInstance(clazz); + } + + /** + * File extension associated with the instance's grammar. + * + * @return grammar specific file extension + */ + public String getFileExtension() { + return get(FileExtensionProvider.class).getPrimaryFileExtension(); + } + + /** + * Creates a grammar specific instance of {@link XtextResourceSet}. + * + * @return grammar specific {@link XtextResourceSet} + */ + public XtextResourceSet getResourceSet() { + return get(XtextResourceSet.class); + } + + /** + * Creates a grammar specific instance of {@link ISerializer}. + * + * @return grammar specific {@link ISerializer} + */ + public ISerializer getSerializer() { + return get(ISerializer.class); + } + + /** + * Creates a grammar specific instance of {@link Diagnostician}. + * + * @return grammar specific {@link Diagnostician} + */ + public Diagnostician getDiagnostician() { + return get(Diagnostician.class); + } + + /** + * Closes the welcome page if it is open. + */ + protected void closeWelcomePage() { + UiThreadDispatcher.dispatchAndWait(new Runnable() { + @Override + public void run() { + IIntroPart intro = PlatformUI.getWorkbench().getIntroManager().getIntro(); + if (intro != null) { + PlatformUI.getWorkbench().getIntroManager().closeIntro(intro); + } + } + }); + } + + /** + * Gets the first instance of given type containing a given structural feature with given value using a given context object. + * + * @param + * the generic type + * @param context + * the context object + * @param type + * the type + * @return the first instance found or null if none found matching given criteria + */ + public T getFirstInstanceOf(final EObject context, final Class type) { + return modelUtil.getFirstInstanceOf(context, type); + } + + /** + * Gets the all instances of given type type having given value value on structural feature feature. + * + * @param + * the generic type + * @param context + * the context + * @param type + * the type + * @param feature + * the feature + * @param value + * the value + * @return the all instances of + */ + public Iterable getAllInstancesOf(final EObject context, final Class type, final EStructuralFeature feature, final Object value) { + return modelUtil.getAllInstancesOf(context, type, feature, value); + } + + /** + * Gets the first instance of given type containing a given structural feature with given value using a given context object. + * + * @param + * the generic type + * @param context + * the context object + * @param type + * the type + * @param feature + * the structural feature + * @param value + * the value + * @return the first instance found or null if none found matching given criteria + */ + public T getFirstInstanceOf(final EObject context, final Class type, final EStructuralFeature feature, final Object value) { + return modelUtil.getFirstInstanceOf(context, type, feature, value); + } + + /** + * Validates a source with a given name and content. + * + * @param sourceFileName + * source name + * @param sourceContent + * content + */ + public void validateSource(final String sourceFileName, final CharSequence sourceContent) { + String sourceContentAsString = sourceContent.toString(); + EObject root; + try { + root = getModel(sourceFileName, sourceContentAsString); + } catch (IOException e) { + fail("Model creation failed: " + e.getMessage()); + return; + } + + // Store all the validation errors + Set errors = Sets.newHashSet(); + errors.addAll(Collections2.filter(getDiagnostician().validate(root).getChildren(), new Predicate() { + @Override + public boolean apply(final Diagnostic input) { + return input.getSeverity() == Diagnostic.ERROR; + } + })); + + Map errorMessages = Maps.newHashMap(); + for (Diagnostic diagnostic : errors) { + Pair result = processDiagnostic(diagnostic); + if (result != null) { + errorMessages.put(result.getKey(), result.getValue()); + } + } + + // Store all the resource errors + for (AbstractDiagnostic diagnostic : Iterables.filter(root.eResource().getErrors(), AbstractDiagnostic.class)) { + errorMessages.put(diagnostic.getOffset(), diagnostic.getMessage()); + } + + List offsets = Lists.newArrayListWithExpectedSize(errorMessages.size()); + offsets.addAll(errorMessages.keySet()); + Collections.sort(offsets); + + // Append all the error messages to the end of the source + int errorNumber = 1; + ListIterator offsetIterator = offsets.listIterator(); + StringBuilder sourceContentWithErrors = new StringBuilder(sourceContent); + while (offsetIterator.hasNext()) { + sourceContentWithErrors.append(SPLITTER); + sourceContentWithErrors.append(String.format(ERROR, errorNumber++)); + sourceContentWithErrors.append(errorMessages.get(offsetIterator.next())).append('\n'); + } + if (errorNumber > 1) { + sourceContentWithErrors.append(SPLITTER); + } + + // Insert all the error markers () to the source + // Done in inverse order to avoid the confusion with offset adjustments + while (offsetIterator.hasPrevious()) { + sourceContentWithErrors.insert(offsetIterator.previous(), String.format(ERROR_MARKER, --errorNumber)); + } + + assertEquals(sourceContentAsString, sourceContentWithErrors.toString(), "Errors found: "); + } + + /** + * Gets the offset and the error message for a given {@link Diagnostic}. + * + * @param diagnostic + * instance of {@link Diagnostic} + * @return + * offset and error message + */ + private Pair processDiagnostic(final Diagnostic diagnostic) { + // CHECKSTYLE:OFF MagicNumber + StringBuilder errorMessage = new StringBuilder(50); + // CHECKSTYLE:ON + if (diagnostic instanceof AbstractValidationDiagnostic) { + AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; + errorMessage.append("Unexpected issue found. Code '"); + errorMessage.append(avd.getIssueCode()).append("'\n"); + errorMessage.append(avd.getMessage()); + if (avd instanceof FeatureBasedDiagnostic && ((FeatureBasedDiagnostic) avd).getFeature() != null) { + List nodes = NodeModelUtils.findNodesForFeature(avd.getSourceEObject(), ((FeatureBasedDiagnostic) avd).getFeature()); + if (nodes != null && !nodes.isEmpty()) { + return new Pair(findFirstNonHiddenLeafNode(nodes.get(0)).getTotalOffset(), errorMessage.toString()); + } + } else if (avd instanceof RangeBasedDiagnostic) { + return new Pair(((RangeBasedDiagnostic) avd).getOffset(), errorMessage.toString()); + } else { + return new Pair(NodeModelUtils.getNode(avd.getSourceEObject()).getTotalOffset(), errorMessage.toString()); + } + } + return null; + } + + /** + * Given an AST node, find the first non-hidden leaf node among child nodes using deep search. + * For the sake of compatibility the method can handle LeafNodes and CompositeNodes. + * In case of a LeafNode the result is the input node itself. + * + * @param node + * entry point + * @return + * first node for which isHidden() is false or the original node + */ + public INode findFirstNonHiddenLeafNode(final INode node) { + if (node instanceof ICompositeNode) { + for (ILeafNode leaf : node.getLeafNodes()) { + if (!leaf.isHidden()) { + return leaf; + } + } + } + return node; + } +}