diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/jvmmodel/CheckJvmModelInferrerUtil.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/jvmmodel/CheckJvmModelInferrerUtil.java index 1619bcc8e6..d80dd6854f 100644 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/jvmmodel/CheckJvmModelInferrerUtil.java +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/jvmmodel/CheckJvmModelInferrerUtil.java @@ -1,32 +1,32 @@ -/******************************************************************************* - * Copyright (c) 2016 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 Evolution AG - initial API and implementation - *******************************************************************************/ - -package com.avaloq.tools.ddk.check.jvmmodel; - -/** - * A class to avoid multi-line strings in Xtend which can be constant strings: See https://github.com/eclipse/xtext/issues/3091. - */ -public final class CheckJvmModelInferrerUtil { - - private CheckJvmModelInferrerUtil() { - // empty - } - - /** - * The documentation for the Get Message Method. - */ - public static final String GET_MESSAGE_DOCUMENTATION = """ - Gets the message associated with a violation of this check. - - @param bindings - the message bindings - @return the message associated with a violation of this check"""; //$NON-NLS-1$ -} +/******************************************************************************* + * Copyright (c) 2016 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 Evolution AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.check.jvmmodel; + +/** + * A class to avoid multi-line strings in Xtend which can be constant strings: See https://github.com/eclipse/xtext/issues/3091. + */ +public final class CheckJvmModelInferrerUtil { + + private CheckJvmModelInferrerUtil() { + // empty + } + + /** + * The documentation for the Get Message Method. + */ + public static final String GET_MESSAGE_DOCUMENTATION = """ + Gets the message associated with a violation of this check. + + @param bindings + the message bindings + @return the message associated with a violation of this check"""; //$NON-NLS-1$ +} diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/BugTestAwareRule.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/BugTestAwareRule.java index cacdde4aa2..eaafe5d0f2 100644 --- a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/BugTestAwareRule.java +++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/BugTestAwareRule.java @@ -1,100 +1,100 @@ -/******************************************************************************* - * 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.test.core.jupiter; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.InvocationInterceptor; -import org.junit.jupiter.api.extension.ReflectiveInvocationContext; - -import com.avaloq.tools.ddk.test.core.BugTest; - - -/** - * This {@link InvocationInterceptor} implementation changes the behavior for unresolved bug tests. - *

- * The behavior for at test that is annotated with {@link BugTest(unresolved=true)} is the following: - *

- *

- *

- * Example for a bug test: - * - *

- * public class TestClass {
- *
- *   @Rule
- *   public BugTestAwareRule rule = BugTestAwareRule.getInstance();
- *
- *   @org.junit.Test
- *   @com.avaloq.tools.ddk.test.core.BugTest(value = "BUG-42", unresolved = true)
- *   public void testMethod() {
- *     org.junit.Assert.fail();
- *   }
- * }
- * 
- *

- * - * @see BugTest - */ -public final class BugTestAwareRule implements InvocationInterceptor { - - private static final String ERROR_TEST_MUST_FAIL = "The unresolved bug test must fail:"; //$NON-NLS-1$ - /** The singleton instance, or {@code null} if not cached. */ - private static BugTestAwareRule instance; - private static final Object LOCK = new Object(); - - /** - * Creates a new instance of {@link BugTestAwareRule}. - */ - private BugTestAwareRule() { - // prevent instantiation - } - - /** - * Returns a shared singleton instance. - * - * @return a shared instance, never {@code null} - */ - public static BugTestAwareRule getInstance() { - synchronized (LOCK) { - if (instance == null) { - instance = new BugTestAwareRule(); - } - return instance; - } - } - - @SuppressWarnings("nls") - @Override - public void interceptTestMethod(final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { - BugTest bugTestAnnotation = extensionContext.getRequiredTestMethod().getAnnotation(BugTest.class); - if (bugTestAnnotation == null && extensionContext.getRequiredTestClass() != null) { - bugTestAnnotation = extensionContext.getRequiredTestClass().getAnnotation(BugTest.class); - } - if (bugTestAnnotation != null && bugTestAnnotation.unresolved()) { - try { - invocation.proceed(); - } catch (AssertionError exception) { - return; - } - String testCase = extensionContext.getRequiredTestClass().getSimpleName() + "." + extensionContext.getRequiredTestMethod().getName(); - throw new AssertionError(ERROR_TEST_MUST_FAIL + " " + testCase); - } else { - invocation.proceed(); - } - } - -} +/******************************************************************************* + * 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.test.core.jupiter; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; + +import com.avaloq.tools.ddk.test.core.BugTest; + + +/** + * This {@link InvocationInterceptor} implementation changes the behavior for unresolved bug tests. + *

+ * The behavior for at test that is annotated with {@link BugTest(unresolved=true)} is the following: + *

+ *

+ *

+ * Example for a bug test: + * + *

+ * public class TestClass {
+ *
+ *   @Rule
+ *   public BugTestAwareRule rule = BugTestAwareRule.getInstance();
+ *
+ *   @org.junit.Test
+ *   @com.avaloq.tools.ddk.test.core.BugTest(value = "BUG-42", unresolved = true)
+ *   public void testMethod() {
+ *     org.junit.Assert.fail();
+ *   }
+ * }
+ * 
+ *

+ * + * @see BugTest + */ +public final class BugTestAwareRule implements InvocationInterceptor { + + private static final String ERROR_TEST_MUST_FAIL = "The unresolved bug test must fail:"; //$NON-NLS-1$ + /** The singleton instance, or {@code null} if not cached. */ + private static BugTestAwareRule instance; + private static final Object LOCK = new Object(); + + /** + * Creates a new instance of {@link BugTestAwareRule}. + */ + private BugTestAwareRule() { + // prevent instantiation + } + + /** + * Returns a shared singleton instance. + * + * @return a shared instance, never {@code null} + */ + public static BugTestAwareRule getInstance() { + synchronized (LOCK) { + if (instance == null) { + instance = new BugTestAwareRule(); + } + return instance; + } + } + + @SuppressWarnings("nls") + @Override + public void interceptTestMethod(final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { + BugTest bugTestAnnotation = extensionContext.getRequiredTestMethod().getAnnotation(BugTest.class); + if (bugTestAnnotation == null && extensionContext.getRequiredTestClass() != null) { + bugTestAnnotation = extensionContext.getRequiredTestClass().getAnnotation(BugTest.class); + } + if (bugTestAnnotation != null && bugTestAnnotation.unresolved()) { + try { + invocation.proceed(); + } catch (AssertionError exception) { + return; + } + String testCase = extensionContext.getRequiredTestClass().getSimpleName() + "." + extensionContext.getRequiredTestMethod().getName(); + throw new AssertionError(ERROR_TEST_MUST_FAIL + " " + testCase); + } else { + invocation.proceed(); + } + } + +} diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/IssueAwareRule.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/IssueAwareRule.java index 2dfe5c3155..59b9f1224e 100644 --- a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/IssueAwareRule.java +++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/IssueAwareRule.java @@ -1,99 +1,99 @@ -/******************************************************************************* - * 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.test.core.jupiter; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.InvocationInterceptor; -import org.junit.jupiter.api.extension.ReflectiveInvocationContext; - -import com.avaloq.tools.ddk.test.core.Issue; - - -/** - * This {@link InvocationInterceptor} implementation changes the behavior for not fixed issues. - *

- * The behavior for at test that is annotated with {@link Issue(fixed = false)} is the following: - *

    - *
  • Test evaluation OK results in FAIL ({@link AssertionError})
  • - *
  • Test evaluation FAIL results in OK
  • - *
  • Test evaluation ERROR results in ERROR
  • - *
- *

- *

- * Example for a issue test: - * - *

- * public class TestClass {
- *
- *   @Rule
- *   public IssueAwareRule rule = IssueAwareRule.getInstance();
- *
- *   @org.junit.Test
- *   @com.avaloq.tools.ddk.test.core.Issue(value = "ISSUE-42", fixed = false)
- *   public void testMethod() {
- *     org.junit.Assert.fail();
- *   }
- * }
- * 
- *

- * - * @see Issue - */ -public final class IssueAwareRule implements InvocationInterceptor { - - private static final String ERROR_TEST_MUST_FAIL = "The issue test for a not fixed issue must fail:"; //$NON-NLS-1$ - /** The singleton instance, or {@code null} if not cached. */ - private static IssueAwareRule instance; - private static Object lock = new Object(); - - /** - * Creates a new instance of {@link IssueAwareRule}. - */ - private IssueAwareRule() { - // prevent instantiation - } - - /** - * Returns a shared singleton instance. - * - * @return a shared instance, never {@code null} - */ - public static IssueAwareRule getInstance() { - synchronized (lock) { - if (instance == null) { - instance = new IssueAwareRule(); - } - return instance; - } - } - - @SuppressWarnings("nls") - @Override - public void interceptTestMethod(final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { - Issue issueAnnotation = extensionContext.getRequiredTestMethod().getAnnotation(Issue.class); - if (issueAnnotation == null && extensionContext.getRequiredTestClass() != null) { - issueAnnotation = extensionContext.getRequiredTestClass().getAnnotation(Issue.class); - } - if (issueAnnotation != null && issueAnnotation.fixed()) { - try { - invocation.proceed(); - } catch (AssertionError exception) { - return; - } - String testCase = extensionContext.getRequiredTestClass().getSimpleName() + "." + extensionContext.getRequiredTestMethod().getName(); - throw new AssertionError(ERROR_TEST_MUST_FAIL + " " + testCase); - } else { - invocation.proceed(); - } - } -} +/******************************************************************************* + * 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.test.core.jupiter; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; + +import com.avaloq.tools.ddk.test.core.Issue; + + +/** + * This {@link InvocationInterceptor} implementation changes the behavior for not fixed issues. + *

+ * The behavior for at test that is annotated with {@link Issue(fixed = false)} is the following: + *

    + *
  • Test evaluation OK results in FAIL ({@link AssertionError})
  • + *
  • Test evaluation FAIL results in OK
  • + *
  • Test evaluation ERROR results in ERROR
  • + *
+ *

+ *

+ * Example for a issue test: + * + *

+ * public class TestClass {
+ *
+ *   @Rule
+ *   public IssueAwareRule rule = IssueAwareRule.getInstance();
+ *
+ *   @org.junit.Test
+ *   @com.avaloq.tools.ddk.test.core.Issue(value = "ISSUE-42", fixed = false)
+ *   public void testMethod() {
+ *     org.junit.Assert.fail();
+ *   }
+ * }
+ * 
+ *

+ * + * @see Issue + */ +public final class IssueAwareRule implements InvocationInterceptor { + + private static final String ERROR_TEST_MUST_FAIL = "The issue test for a not fixed issue must fail:"; //$NON-NLS-1$ + /** The singleton instance, or {@code null} if not cached. */ + private static IssueAwareRule instance; + private static Object lock = new Object(); + + /** + * Creates a new instance of {@link IssueAwareRule}. + */ + private IssueAwareRule() { + // prevent instantiation + } + + /** + * Returns a shared singleton instance. + * + * @return a shared instance, never {@code null} + */ + public static IssueAwareRule getInstance() { + synchronized (lock) { + if (instance == null) { + instance = new IssueAwareRule(); + } + return instance; + } + } + + @SuppressWarnings("nls") + @Override + public void interceptTestMethod(final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { + Issue issueAnnotation = extensionContext.getRequiredTestMethod().getAnnotation(Issue.class); + if (issueAnnotation == null && extensionContext.getRequiredTestClass() != null) { + issueAnnotation = extensionContext.getRequiredTestClass().getAnnotation(Issue.class); + } + if (issueAnnotation != null && issueAnnotation.fixed()) { + try { + invocation.proceed(); + } catch (AssertionError exception) { + return; + } + String testCase = extensionContext.getRequiredTestClass().getSimpleName() + "." + extensionContext.getRequiredTestMethod().getName(); + throw new AssertionError(ERROR_TEST_MUST_FAIL + " " + testCase); + } else { + invocation.proceed(); + } + } +} diff --git a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/LoggingRule.java b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/LoggingRule.java index 9152b473d6..13539a76f5 100644 --- a/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/LoggingRule.java +++ b/com.avaloq.tools.ddk.test.core/src/com/avaloq/tools/ddk/test/core/jupiter/LoggingRule.java @@ -1,93 +1,93 @@ -/******************************************************************************* - * 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.test.core.jupiter; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestWatcher; - - -/** - * A test watcher that logs the start and end of each test, as well as its success or failure. - */ -@SuppressWarnings("nls") -public final class LoggingRule implements TestWatcher, BeforeEachCallback, AfterEachCallback { - - private static final Logger LOGGER = LogManager.getLogger(LoggingRule.class); - - /** The singleton instance, or {@code null} if not cached. */ - private static LoggingRule instance; - - private static final Object LOCK = new Object(); - - /** - * Creates a new instance of {@link LoggingRule}. - */ - private LoggingRule() { - // prevent instantiation - } - - /** - * Returns a shared singleton instance. - * - * @return a shared instance, never {@code null} - */ - public static LoggingRule getInstance() { - synchronized (LOCK) { - if (instance == null) { - instance = new LoggingRule(); - } - return instance; - } - } - - @Override - public void beforeEach(final ExtensionContext context) throws Exception { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("STARTING: " + getDescriptionName(context)); - } - } - - /** - * Returns the name of a test to be logged. - * - * @param description - * the description, must not be {@code null} - * @return the description name, never {@code null} - */ - private String getDescriptionName(final ExtensionContext context) { - return context.getRequiredTestClass().getSimpleName() + '.' + context.getRequiredTestMethod().getName(); - } - - @Override - public void testSuccessful(final ExtensionContext context) { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("SUCCEEDED: " + getDescriptionName(context)); - } - } - - @Override - public void testFailed(final ExtensionContext context, final Throwable cause) { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("FAILED: " + getDescriptionName(context)); - } - } - - @Override - public void afterEach(final ExtensionContext context) throws Exception { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("FINISHED: " + getDescriptionName(context)); - } - } -} +/******************************************************************************* + * 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.test.core.jupiter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; + + +/** + * A test watcher that logs the start and end of each test, as well as its success or failure. + */ +@SuppressWarnings("nls") +public final class LoggingRule implements TestWatcher, BeforeEachCallback, AfterEachCallback { + + private static final Logger LOGGER = LogManager.getLogger(LoggingRule.class); + + /** The singleton instance, or {@code null} if not cached. */ + private static LoggingRule instance; + + private static final Object LOCK = new Object(); + + /** + * Creates a new instance of {@link LoggingRule}. + */ + private LoggingRule() { + // prevent instantiation + } + + /** + * Returns a shared singleton instance. + * + * @return a shared instance, never {@code null} + */ + public static LoggingRule getInstance() { + synchronized (LOCK) { + if (instance == null) { + instance = new LoggingRule(); + } + return instance; + } + } + + @Override + public void beforeEach(final ExtensionContext context) throws Exception { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("STARTING: " + getDescriptionName(context)); + } + } + + /** + * Returns the name of a test to be logged. + * + * @param description + * the description, must not be {@code null} + * @return the description name, never {@code null} + */ + private String getDescriptionName(final ExtensionContext context) { + return context.getRequiredTestClass().getSimpleName() + '.' + context.getRequiredTestMethod().getName(); + } + + @Override + public void testSuccessful(final ExtensionContext context) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("SUCCEEDED: " + getDescriptionName(context)); + } + } + + @Override + public void testFailed(final ExtensionContext context, final Throwable cause) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("FAILED: " + getDescriptionName(context)); + } + } + + @Override + public void afterEach(final ExtensionContext context) throws Exception { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("FINISHED: " + getDescriptionName(context)); + } + } +} diff --git a/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/IStandaloneBuilderParticipant.java b/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/IStandaloneBuilderParticipant.java index 82a6e994d8..dcd1f0fda2 100644 --- a/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/IStandaloneBuilderParticipant.java +++ b/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/IStandaloneBuilderParticipant.java @@ -1,68 +1,68 @@ -/* - * Copyright (c) Avaloq Group AG - * Schwerzistrasse 6, 8807 Freienbach, Switzerland, http://www.avaloq.com - * All Rights Reserved. - * - * This software is the confidential and proprietary information of Avaloq Group AG. - * You shall not disclose whole or parts of it and shall use it only in accordance with the terms of the - * licence agreement you entered into with Avaloq Group AG. - */ - -package com.avaloq.tools.ddk.xtext.builder; - -import org.eclipse.xtext.builder.IXtextBuilderParticipant; - - -/** - * Stand-alone version of {@link IXtextBuilderParticipant}. The stand-alone builder will process only build participants that implement this interface. Classes - * implementing this must be prepared to deal with being called in {@link IXtextBuilderParticipant#build build()} with an - * {@link IXtextBuilderParticipant.IBuildContext} that will return {@code null} in {@link IXtextBuilderParticipant.IBuildContext#getProject - * getProject()}. - */ -public interface IStandaloneBuilderParticipant extends IXtextBuilderParticipant { - - /** - * Determines what builder participants should actually do when get executed. - */ - enum ExecutionMode { - /** - * If an indexing build is running it has the purpose to restore the build state only. - * So generators must not change the system. However, we still want to determine what - * derived objects generators would produce. So we let them run as normal, but ignore - * the results. Implementers of the generators may decide to do less work in this mode - * for performance reasons. - */ - METADATA, - - /** - * When ASMD really need to run server-side to produce sources, this mode is used. - * Currently not used. - */ - FULL - } - - /** - * Sets the execution mode for the builder participants. - * - * @param mode - * the new execution mode - */ - void setExecutionMode(ExecutionMode mode); - - /** - * When running in {@link ExecutionMode#FULL} mode some of the generators may still be disabled. - * Those that are configured to actually perform generation in the standalone builder need to be - * explicitly enabled by the standalone language setup via this method. - *

- * Note that disabled builder participants will still run, the metadata will still be written, - * but the results will have no effect and generated sources will be ignored. - *

- * - * @param isActive - * whether the builder participant is fully enabled for this language - */ - void setActive(boolean isActive); - -} - +/* + * Copyright (c) Avaloq Group AG + * Schwerzistrasse 6, 8807 Freienbach, Switzerland, http://www.avaloq.com + * All Rights Reserved. + * + * This software is the confidential and proprietary information of Avaloq Group AG. + * You shall not disclose whole or parts of it and shall use it only in accordance with the terms of the + * licence agreement you entered into with Avaloq Group AG. + */ + +package com.avaloq.tools.ddk.xtext.builder; + +import org.eclipse.xtext.builder.IXtextBuilderParticipant; + + +/** + * Stand-alone version of {@link IXtextBuilderParticipant}. The stand-alone builder will process only build participants that implement this interface. Classes + * implementing this must be prepared to deal with being called in {@link IXtextBuilderParticipant#build build()} with an + * {@link IXtextBuilderParticipant.IBuildContext} that will return {@code null} in {@link IXtextBuilderParticipant.IBuildContext#getProject + * getProject()}. + */ +public interface IStandaloneBuilderParticipant extends IXtextBuilderParticipant { + + /** + * Determines what builder participants should actually do when get executed. + */ + enum ExecutionMode { + /** + * If an indexing build is running it has the purpose to restore the build state only. + * So generators must not change the system. However, we still want to determine what + * derived objects generators would produce. So we let them run as normal, but ignore + * the results. Implementers of the generators may decide to do less work in this mode + * for performance reasons. + */ + METADATA, + + /** + * When ASMD really need to run server-side to produce sources, this mode is used. + * Currently not used. + */ + FULL + } + + /** + * Sets the execution mode for the builder participants. + * + * @param mode + * the new execution mode + */ + void setExecutionMode(ExecutionMode mode); + + /** + * When running in {@link ExecutionMode#FULL} mode some of the generators may still be disabled. + * Those that are configured to actually perform generation in the standalone builder need to be + * explicitly enabled by the standalone language setup via this method. + *

+ * Note that disabled builder participants will still run, the metadata will still be written, + * but the results will have no effect and generated sources will be ignored. + *

+ * + * @param isActive + * whether the builder participant is fully enabled for this language + */ + void setActive(boolean isActive); + +} + /* Copyright (c) Avaloq Group AG */ \ No newline at end of file diff --git a/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/RegistryBuilderParticipant.java b/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/RegistryBuilderParticipant.java index e1ace03367..6aa55e3f66 100644 --- a/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/RegistryBuilderParticipant.java +++ b/com.avaloq.tools.ddk.xtext.builder/src/com/avaloq/tools/ddk/xtext/builder/RegistryBuilderParticipant.java @@ -1,350 +1,350 @@ -/******************************************************************************* - * Copyright (c) 2016 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.builder; - -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IConfigurationElement; -import org.eclipse.core.runtime.IExtensionRegistry; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.core.runtime.SubMonitor; -import org.eclipse.osgi.util.NLS; -import org.eclipse.xtext.IGrammarAccess; -import org.eclipse.xtext.builder.IXtextBuilderParticipant; -import org.eclipse.xtext.builder.impl.Messages; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.IResourceServiceProvider; - -import com.avaloq.tools.ddk.xtext.resource.ResourceServiceProviderLocator; -import com.avaloq.tools.ddk.xtext.util.BuilderParticipantSettings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.inject.ConfigurationException; -import com.google.inject.Inject; -import com.google.inject.Injector; -import com.google.inject.Singleton; - - -/** - * A custom {@link org.eclipse.xtext.builder.impl.RegistryBuilderParticipant}, able to support injection for - * {@link ILanguageSpecificBuilderParticipant}s. - * Unfortunately, the base class was not well extendible, so most of it had to be copied. - */ -@Singleton -public class RegistryBuilderParticipant extends org.eclipse.xtext.builder.impl.RegistryBuilderParticipant { - private static final int MONITOR_PARTICIPANTS_PER_LANGUAGE = 1000; - private static final String PARTICIPANT = "participant"; //$NON-NLS-1$ - private static final String EXTENSION_POINT_ID = PARTICIPANT; - private static final String ATT_CLASS = "class"; //$NON-NLS-1$ - private static final Logger LOG = LogManager.getLogger(RegistryBuilderParticipant.class); - - @Inject - private IExtensionRegistry extensionRegistry; - - @Inject - private ResourceServiceProviderLocator resourceServiceProviderLocator; - - private ImmutableList immutableCommonParticipants; // NOPMD LooseCoupling - - private final Map classToParticipant = Maps.newHashMap(); - private final Map> serviceProviderToParticipants = Maps.newHashMap(); - private final Set initializedParticipants = Sets.newHashSet(); - - @Override - public void build(final IBuildContext buildContext, final IProgressMonitor monitor) throws CoreException { - SubMonitor progress = SubMonitor.convert(monitor, 2); - buildLanguageSpecificParticipants(buildContext, progress.newChild(1)); - buildOtherParticipants(buildContext, progress.newChild(1)); - } - - /** - * For each {@link IResourceDescription.Delta} searches and calls the responsible {@link ILanguageSpecificBuilderParticipant}s. - * - * @param buildContext - * the {@link IBuildContext}, must not be {@code null} - * @param monitor - * the {@link IProgressMonitor}, must not be {@code null} - */ - protected void buildLanguageSpecificParticipants(final IBuildContext buildContext, final IProgressMonitor monitor) { - initParticipants(); - SubMonitor progress = SubMonitor.convert(monitor, buildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE); - Map languageIdToBuildContext = Maps.newHashMap(); - for (IResourceDescription.Delta delta : buildContext.getDeltas()) { - IResourceServiceProvider resourceServiceProvider = IResourceServiceProvider.Registry.INSTANCE.getResourceServiceProvider(delta.getUri()); - if (resourceServiceProvider == null) { - progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); - continue; - } - IGrammarAccess grammarAccess; - try { - grammarAccess = resourceServiceProvider.get(IGrammarAccess.class); - } catch (ConfigurationException e) { - progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); - continue; - } - if (grammarAccess == null) { - progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); - continue; - } - String languageId = grammarAccess.getGrammar().getName(); - BuildContext entryBuildContext = languageIdToBuildContext.get(languageId); - if (entryBuildContext == null) { - entryBuildContext = new BuildContext(buildContext.getBuiltProject(), buildContext.getResourceSet(), buildContext.getBuildType()); - languageIdToBuildContext.put(languageId, entryBuildContext); - } - entryBuildContext.addDelta(delta); - } - builLanguageSpecificContext(buildContext, progress, languageIdToBuildContext); - } - - /** - * For the given {@link IBuildContext} calls the responsible {@link ILanguageSpecificBuilderParticipant}s. - * - * @param buildContext - * the {@link IBuildContext}, must not be {@code null} - * @param progress - * the {@link SubMonitor}, must not be {@code null} - * @param languageIdToBuildContext - * the map of which build context should be used with responsible {@link ILanguageSpecificBuilderParticipant}, must not be {@code null} - */ - private void builLanguageSpecificContext(final IBuildContext buildContext, final SubMonitor progress, final Map languageIdToBuildContext) { - for (final Entry entry : languageIdToBuildContext.entrySet()) { - final String languageId = entry.getKey(); - final BuildContext entryBuildContext = entry.getValue(); - final Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); - if (languageSpecificBuilderParticipants != null && !languageSpecificBuilderParticipants.isEmpty()) { - int workUnits = entryBuildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE / languageSpecificBuilderParticipants.size(); - for (final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant : languageSpecificBuilderParticipants) { - try { - if (initializeParticipant(languageSpecificBuilderParticipant)) { - languageSpecificBuilderParticipant.build(entryBuildContext, progress.newChild(workUnits)); - if (entryBuildContext.isRebuildRequired()) { - buildContext.needRebuild(buildContext.getBuiltProject()); - } - } - // CHECKSTYLE:CHECK-OFF IllegalCatchCheck we need to recover from any exception and continue the build - } catch (Throwable throwable) { - // CHECKSTYLE:CHECK-ON IllegalCatchCheck - LOG.error("Error occurred during build of an ILanguageSpecificBuilderParticipant: " //$NON-NLS-1$ - + languageSpecificBuilderParticipant.getClass().getName(), throwable); - } - } - } else { - progress.worked(entryBuildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE); - } - } - } - - /** - * Builds all other registered (non-language specific) {@link IXtextBuilderParticipant}s. - * - * @param buildContext - * the {@link IBuildContext}, must not be {@code null} - * @param monitor - * the {@link IProgressMonitor}, must not be {@code null} - * @throws CoreException - * caused by an {@link IXtextBuilderParticipant} - */ - protected void buildOtherParticipants(final IBuildContext buildContext, final IProgressMonitor monitor) throws CoreException { - List otherBuilderParticipants = getParticipants(); - if (otherBuilderParticipants.isEmpty()) { - return; - } - SubMonitor progress = SubMonitor.convert(monitor, otherBuilderParticipants.size()); - progress.subTask(Messages.RegistryBuilderParticipant_InvokingBuildParticipants); - for (final IXtextBuilderParticipant participant : otherBuilderParticipants) { - if (progress.isCanceled()) { - throw new OperationCanceledException(); - } - try { - if (initializeParticipant(participant)) { - participant.build(buildContext, progress.newChild(1)); - } - // CHECKSTYLE:CHECK-OFF IllegalCatchCheck we need to recover from any exception and continue the build - } catch (Throwable throwable) { - // CHECKSTYLE:CHECK-ON IllegalCatchCheck - LOG.error("Error occurred during build of builder participant: " //$NON-NLS-1$ - + participant.getClass().getName(), throwable); - } - } - } - - /** - * Initializes the given {@link IXtextBuilderParticipant}. - * - * @param participant - * the {@link IXtextBuilderParticipant} to initialize, must not be {@code null} - * @return whether the builder participant was initialized successfully - */ - private boolean initializeParticipant(final IXtextBuilderParticipant participant) { - String languageId = null; - if (participant instanceof IGeneratorModuleProvider) { - languageId = ((IGeneratorModuleProvider) participant).getGeneratorModuleId(); - } else if (participant instanceof ILanguageSpecificBuilderParticipant) { - languageId = ((ILanguageSpecificBuilderParticipant) participant).getLanguageId(); - } - if (languageId != null && !BuilderParticipantSettings.isBuilderParticipantEnabled(languageId)) { - return false; - } - if (!initializedParticipants.contains(participant)) { - if (languageId != null) { - final IResourceServiceProvider resourceServiceProvider = resourceServiceProviderLocator.getResourceServiceProviderById(languageId); - if (resourceServiceProvider != null) { - // inject members of the participant - final Injector injector = resourceServiceProvider.get(Injector.class); - injector.injectMembers(participant); - } else { - LOG.error(NLS.bind("No ResourceServiceProvider found for builder participant ''{0}'' and language id ''{1}''", participant.getClass().getName(), languageId)); //$NON-NLS-1$ - return false; - } - } - initializedParticipants.add(participant); - } - return true; - } - - @Override - public ImmutableList getParticipants() { - return initParticipants(); - } - - /** - * This method should be used only in a system test. - * In the productive code the list of participants should be initialized only once and never be reseted. - * If one resets the list, nothing bad will happen - the list of builder participants will be loaded during the next build. - */ - public synchronized void resetParticipants() { - immutableCommonParticipants = null; - classToParticipant.clear(); - serviceProviderToParticipants.clear(); - initializedParticipants.clear(); - } - - @Override - protected synchronized ImmutableList initParticipants() { // NOPMD LooseCoupling - if (immutableCommonParticipants == null) { - String pluginID = "org.eclipse.xtext.builder"; //$NON-NLS-1$ // Activator.getDefault().getBundle().getSymbolicName(); - String extensionPointID = EXTENSION_POINT_ID; - ConditionalBuilderParticipantReader reader = new ConditionalBuilderParticipantReader(extensionRegistry, pluginID, extensionPointID); - reader.readRegistry(); - immutableCommonParticipants = reader.getCommonParticipants(); - } - return immutableCommonParticipants; - } - - /** - * Maintains the set of extensions. - */ - public class ConditionalBuilderParticipantReader extends BuilderParticipantReader { - - private final List commonParticipants = Lists.newArrayList(); - - public ConditionalBuilderParticipantReader(final IExtensionRegistry pluginRegistry, final String pluginID, final String extensionPointID) { - super(pluginRegistry, pluginID, extensionPointID); - } - - protected ImmutableList getCommonParticipants() { // NOPMD LooseCoupling - return ImmutableList.copyOf(commonParticipants); - } - - /** - * {@inheritDoc} - */ - @Override - protected boolean readElement(final IConfigurationElement element, final boolean add) { - if (!PARTICIPANT.equals(element.getName())) { - return false; - } - String className = element.getAttribute(ATT_CLASS); - if (className == null) { - logMissingAttribute(element, ATT_CLASS); - return false; - } - if (add) { - try { - Object participant = element.createExecutableExtension(ATT_CLASS); - if (participant instanceof IXtextBuilderParticipant) { - IXtextBuilderParticipant xtextBuilderParticipant = (IXtextBuilderParticipant) participant; - if (classToParticipant.containsKey(className)) { - LOG.warn("The builder participant '" + className + "' was registered twice."); //$NON-NLS-1$ //$NON-NLS-2$ - } - classToParticipant.put(className, xtextBuilderParticipant); - if (participant instanceof ILanguageSpecificBuilderParticipant) { - ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant = (ILanguageSpecificBuilderParticipant) participant; - String languageId = languageSpecificBuilderParticipant.getLanguageId(); - registerLanguageSpecificBuilderParticipant(languageId, languageSpecificBuilderParticipant); - } else { - commonParticipants.add(xtextBuilderParticipant); - } - initializedParticipants.remove(participant); - } else { - logError(element, className + " did not yield an instance of IXtextBuilderParticipant but " + participant.getClass().getName()); //$NON-NLS-1$ - } - return true; - } catch (CoreException e) { - logError(element, e.getMessage()); - } - } else { - IXtextBuilderParticipant xtextBuilderParticipant = classToParticipant.remove(className); - if (xtextBuilderParticipant instanceof ILanguageSpecificBuilderParticipant) { - ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant = ((ILanguageSpecificBuilderParticipant) xtextBuilderParticipant); - String languageId = languageSpecificBuilderParticipant.getLanguageId(); - unregisterLanguageSpecificBuilderParticipant(languageId, languageSpecificBuilderParticipant); - } else if (xtextBuilderParticipant != null) { - commonParticipants.remove(xtextBuilderParticipant); - } - return true; - } - return false; - } - - /** - * Registers a {@link ILanguageSpecificBuilderParticipant} for the given {@link IResourceServiceProvider}. - * - * @param languageId - * the language id for which to register the {@link ILanguageSpecificBuilderParticipant} - * @param languageSpecificBuilderParticipant - * the {@link ILanguageSpecificBuilderParticipant} to register - * @return {@code true} if the {@link ILanguageSpecificBuilderParticipant} was not registered yet, {@code false} otherwise - */ - private boolean registerLanguageSpecificBuilderParticipant(final String languageId, final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant) { - Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); - if (languageSpecificBuilderParticipants == null) { - languageSpecificBuilderParticipants = Sets.newHashSet(); - serviceProviderToParticipants.put(languageId, languageSpecificBuilderParticipants); - } - return languageSpecificBuilderParticipants.add(languageSpecificBuilderParticipant); - } - - /** - * Unregisters a {@link ILanguageSpecificBuilderParticipant} for the given {@link IResourceServiceProvider}. - * - * @param languageId - * the language id for which to unregister the {@link ILanguageSpecificBuilderParticipant}. - * @param languageSpecificBuilderParticipant - * the {@link ILanguageSpecificBuilderParticipant} to unregister - * @return {@code true} if the {@link ILanguageSpecificBuilderParticipant} was registered before, {@code false} otherwise - */ - private boolean unregisterLanguageSpecificBuilderParticipant(final String languageId, final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant) { - Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); - return languageSpecificBuilderParticipants != null && languageSpecificBuilderParticipants.remove(languageSpecificBuilderParticipant); - } - } -} +/******************************************************************************* + * Copyright (c) 2016 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.builder; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.osgi.util.NLS; +import org.eclipse.xtext.IGrammarAccess; +import org.eclipse.xtext.builder.IXtextBuilderParticipant; +import org.eclipse.xtext.builder.impl.Messages; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceServiceProvider; + +import com.avaloq.tools.ddk.xtext.resource.ResourceServiceProviderLocator; +import com.avaloq.tools.ddk.xtext.util.BuilderParticipantSettings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.inject.ConfigurationException; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; + + +/** + * A custom {@link org.eclipse.xtext.builder.impl.RegistryBuilderParticipant}, able to support injection for + * {@link ILanguageSpecificBuilderParticipant}s. + * Unfortunately, the base class was not well extendible, so most of it had to be copied. + */ +@Singleton +public class RegistryBuilderParticipant extends org.eclipse.xtext.builder.impl.RegistryBuilderParticipant { + private static final int MONITOR_PARTICIPANTS_PER_LANGUAGE = 1000; + private static final String PARTICIPANT = "participant"; //$NON-NLS-1$ + private static final String EXTENSION_POINT_ID = PARTICIPANT; + private static final String ATT_CLASS = "class"; //$NON-NLS-1$ + private static final Logger LOG = LogManager.getLogger(RegistryBuilderParticipant.class); + + @Inject + private IExtensionRegistry extensionRegistry; + + @Inject + private ResourceServiceProviderLocator resourceServiceProviderLocator; + + private ImmutableList immutableCommonParticipants; // NOPMD LooseCoupling + + private final Map classToParticipant = Maps.newHashMap(); + private final Map> serviceProviderToParticipants = Maps.newHashMap(); + private final Set initializedParticipants = Sets.newHashSet(); + + @Override + public void build(final IBuildContext buildContext, final IProgressMonitor monitor) throws CoreException { + SubMonitor progress = SubMonitor.convert(monitor, 2); + buildLanguageSpecificParticipants(buildContext, progress.newChild(1)); + buildOtherParticipants(buildContext, progress.newChild(1)); + } + + /** + * For each {@link IResourceDescription.Delta} searches and calls the responsible {@link ILanguageSpecificBuilderParticipant}s. + * + * @param buildContext + * the {@link IBuildContext}, must not be {@code null} + * @param monitor + * the {@link IProgressMonitor}, must not be {@code null} + */ + protected void buildLanguageSpecificParticipants(final IBuildContext buildContext, final IProgressMonitor monitor) { + initParticipants(); + SubMonitor progress = SubMonitor.convert(monitor, buildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE); + Map languageIdToBuildContext = Maps.newHashMap(); + for (IResourceDescription.Delta delta : buildContext.getDeltas()) { + IResourceServiceProvider resourceServiceProvider = IResourceServiceProvider.Registry.INSTANCE.getResourceServiceProvider(delta.getUri()); + if (resourceServiceProvider == null) { + progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); + continue; + } + IGrammarAccess grammarAccess; + try { + grammarAccess = resourceServiceProvider.get(IGrammarAccess.class); + } catch (ConfigurationException e) { + progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); + continue; + } + if (grammarAccess == null) { + progress.worked(MONITOR_PARTICIPANTS_PER_LANGUAGE); + continue; + } + String languageId = grammarAccess.getGrammar().getName(); + BuildContext entryBuildContext = languageIdToBuildContext.get(languageId); + if (entryBuildContext == null) { + entryBuildContext = new BuildContext(buildContext.getBuiltProject(), buildContext.getResourceSet(), buildContext.getBuildType()); + languageIdToBuildContext.put(languageId, entryBuildContext); + } + entryBuildContext.addDelta(delta); + } + builLanguageSpecificContext(buildContext, progress, languageIdToBuildContext); + } + + /** + * For the given {@link IBuildContext} calls the responsible {@link ILanguageSpecificBuilderParticipant}s. + * + * @param buildContext + * the {@link IBuildContext}, must not be {@code null} + * @param progress + * the {@link SubMonitor}, must not be {@code null} + * @param languageIdToBuildContext + * the map of which build context should be used with responsible {@link ILanguageSpecificBuilderParticipant}, must not be {@code null} + */ + private void builLanguageSpecificContext(final IBuildContext buildContext, final SubMonitor progress, final Map languageIdToBuildContext) { + for (final Entry entry : languageIdToBuildContext.entrySet()) { + final String languageId = entry.getKey(); + final BuildContext entryBuildContext = entry.getValue(); + final Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); + if (languageSpecificBuilderParticipants != null && !languageSpecificBuilderParticipants.isEmpty()) { + int workUnits = entryBuildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE / languageSpecificBuilderParticipants.size(); + for (final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant : languageSpecificBuilderParticipants) { + try { + if (initializeParticipant(languageSpecificBuilderParticipant)) { + languageSpecificBuilderParticipant.build(entryBuildContext, progress.newChild(workUnits)); + if (entryBuildContext.isRebuildRequired()) { + buildContext.needRebuild(buildContext.getBuiltProject()); + } + } + // CHECKSTYLE:CHECK-OFF IllegalCatchCheck we need to recover from any exception and continue the build + } catch (Throwable throwable) { + // CHECKSTYLE:CHECK-ON IllegalCatchCheck + LOG.error("Error occurred during build of an ILanguageSpecificBuilderParticipant: " //$NON-NLS-1$ + + languageSpecificBuilderParticipant.getClass().getName(), throwable); + } + } + } else { + progress.worked(entryBuildContext.getDeltas().size() * MONITOR_PARTICIPANTS_PER_LANGUAGE); + } + } + } + + /** + * Builds all other registered (non-language specific) {@link IXtextBuilderParticipant}s. + * + * @param buildContext + * the {@link IBuildContext}, must not be {@code null} + * @param monitor + * the {@link IProgressMonitor}, must not be {@code null} + * @throws CoreException + * caused by an {@link IXtextBuilderParticipant} + */ + protected void buildOtherParticipants(final IBuildContext buildContext, final IProgressMonitor monitor) throws CoreException { + List otherBuilderParticipants = getParticipants(); + if (otherBuilderParticipants.isEmpty()) { + return; + } + SubMonitor progress = SubMonitor.convert(monitor, otherBuilderParticipants.size()); + progress.subTask(Messages.RegistryBuilderParticipant_InvokingBuildParticipants); + for (final IXtextBuilderParticipant participant : otherBuilderParticipants) { + if (progress.isCanceled()) { + throw new OperationCanceledException(); + } + try { + if (initializeParticipant(participant)) { + participant.build(buildContext, progress.newChild(1)); + } + // CHECKSTYLE:CHECK-OFF IllegalCatchCheck we need to recover from any exception and continue the build + } catch (Throwable throwable) { + // CHECKSTYLE:CHECK-ON IllegalCatchCheck + LOG.error("Error occurred during build of builder participant: " //$NON-NLS-1$ + + participant.getClass().getName(), throwable); + } + } + } + + /** + * Initializes the given {@link IXtextBuilderParticipant}. + * + * @param participant + * the {@link IXtextBuilderParticipant} to initialize, must not be {@code null} + * @return whether the builder participant was initialized successfully + */ + private boolean initializeParticipant(final IXtextBuilderParticipant participant) { + String languageId = null; + if (participant instanceof IGeneratorModuleProvider) { + languageId = ((IGeneratorModuleProvider) participant).getGeneratorModuleId(); + } else if (participant instanceof ILanguageSpecificBuilderParticipant) { + languageId = ((ILanguageSpecificBuilderParticipant) participant).getLanguageId(); + } + if (languageId != null && !BuilderParticipantSettings.isBuilderParticipantEnabled(languageId)) { + return false; + } + if (!initializedParticipants.contains(participant)) { + if (languageId != null) { + final IResourceServiceProvider resourceServiceProvider = resourceServiceProviderLocator.getResourceServiceProviderById(languageId); + if (resourceServiceProvider != null) { + // inject members of the participant + final Injector injector = resourceServiceProvider.get(Injector.class); + injector.injectMembers(participant); + } else { + LOG.error(NLS.bind("No ResourceServiceProvider found for builder participant ''{0}'' and language id ''{1}''", participant.getClass().getName(), languageId)); //$NON-NLS-1$ + return false; + } + } + initializedParticipants.add(participant); + } + return true; + } + + @Override + public ImmutableList getParticipants() { + return initParticipants(); + } + + /** + * This method should be used only in a system test. + * In the productive code the list of participants should be initialized only once and never be reseted. + * If one resets the list, nothing bad will happen - the list of builder participants will be loaded during the next build. + */ + public synchronized void resetParticipants() { + immutableCommonParticipants = null; + classToParticipant.clear(); + serviceProviderToParticipants.clear(); + initializedParticipants.clear(); + } + + @Override + protected synchronized ImmutableList initParticipants() { // NOPMD LooseCoupling + if (immutableCommonParticipants == null) { + String pluginID = "org.eclipse.xtext.builder"; //$NON-NLS-1$ // Activator.getDefault().getBundle().getSymbolicName(); + String extensionPointID = EXTENSION_POINT_ID; + ConditionalBuilderParticipantReader reader = new ConditionalBuilderParticipantReader(extensionRegistry, pluginID, extensionPointID); + reader.readRegistry(); + immutableCommonParticipants = reader.getCommonParticipants(); + } + return immutableCommonParticipants; + } + + /** + * Maintains the set of extensions. + */ + public class ConditionalBuilderParticipantReader extends BuilderParticipantReader { + + private final List commonParticipants = Lists.newArrayList(); + + public ConditionalBuilderParticipantReader(final IExtensionRegistry pluginRegistry, final String pluginID, final String extensionPointID) { + super(pluginRegistry, pluginID, extensionPointID); + } + + protected ImmutableList getCommonParticipants() { // NOPMD LooseCoupling + return ImmutableList.copyOf(commonParticipants); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean readElement(final IConfigurationElement element, final boolean add) { + if (!PARTICIPANT.equals(element.getName())) { + return false; + } + String className = element.getAttribute(ATT_CLASS); + if (className == null) { + logMissingAttribute(element, ATT_CLASS); + return false; + } + if (add) { + try { + Object participant = element.createExecutableExtension(ATT_CLASS); + if (participant instanceof IXtextBuilderParticipant) { + IXtextBuilderParticipant xtextBuilderParticipant = (IXtextBuilderParticipant) participant; + if (classToParticipant.containsKey(className)) { + LOG.warn("The builder participant '" + className + "' was registered twice."); //$NON-NLS-1$ //$NON-NLS-2$ + } + classToParticipant.put(className, xtextBuilderParticipant); + if (participant instanceof ILanguageSpecificBuilderParticipant) { + ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant = (ILanguageSpecificBuilderParticipant) participant; + String languageId = languageSpecificBuilderParticipant.getLanguageId(); + registerLanguageSpecificBuilderParticipant(languageId, languageSpecificBuilderParticipant); + } else { + commonParticipants.add(xtextBuilderParticipant); + } + initializedParticipants.remove(participant); + } else { + logError(element, className + " did not yield an instance of IXtextBuilderParticipant but " + participant.getClass().getName()); //$NON-NLS-1$ + } + return true; + } catch (CoreException e) { + logError(element, e.getMessage()); + } + } else { + IXtextBuilderParticipant xtextBuilderParticipant = classToParticipant.remove(className); + if (xtextBuilderParticipant instanceof ILanguageSpecificBuilderParticipant) { + ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant = ((ILanguageSpecificBuilderParticipant) xtextBuilderParticipant); + String languageId = languageSpecificBuilderParticipant.getLanguageId(); + unregisterLanguageSpecificBuilderParticipant(languageId, languageSpecificBuilderParticipant); + } else if (xtextBuilderParticipant != null) { + commonParticipants.remove(xtextBuilderParticipant); + } + return true; + } + return false; + } + + /** + * Registers a {@link ILanguageSpecificBuilderParticipant} for the given {@link IResourceServiceProvider}. + * + * @param languageId + * the language id for which to register the {@link ILanguageSpecificBuilderParticipant} + * @param languageSpecificBuilderParticipant + * the {@link ILanguageSpecificBuilderParticipant} to register + * @return {@code true} if the {@link ILanguageSpecificBuilderParticipant} was not registered yet, {@code false} otherwise + */ + private boolean registerLanguageSpecificBuilderParticipant(final String languageId, final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant) { + Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); + if (languageSpecificBuilderParticipants == null) { + languageSpecificBuilderParticipants = Sets.newHashSet(); + serviceProviderToParticipants.put(languageId, languageSpecificBuilderParticipants); + } + return languageSpecificBuilderParticipants.add(languageSpecificBuilderParticipant); + } + + /** + * Unregisters a {@link ILanguageSpecificBuilderParticipant} for the given {@link IResourceServiceProvider}. + * + * @param languageId + * the language id for which to unregister the {@link ILanguageSpecificBuilderParticipant}. + * @param languageSpecificBuilderParticipant + * the {@link ILanguageSpecificBuilderParticipant} to unregister + * @return {@code true} if the {@link ILanguageSpecificBuilderParticipant} was registered before, {@code false} otherwise + */ + private boolean unregisterLanguageSpecificBuilderParticipant(final String languageId, final ILanguageSpecificBuilderParticipant languageSpecificBuilderParticipant) { + Set languageSpecificBuilderParticipants = serviceProviderToParticipants.get(languageId); + return languageSpecificBuilderParticipants != null && languageSpecificBuilderParticipants.remove(languageSpecificBuilderParticipant); + } + } +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractAcfContentAssistTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractAcfContentAssistTest.java index af1c607e5c..aa945db99f 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractAcfContentAssistTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractAcfContentAssistTest.java @@ -1,357 +1,357 @@ -/******************************************************************************* - * 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.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; - -import java.text.MessageFormat; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.eclipse.emf.common.util.WrappedException; -import org.eclipse.jface.text.contentassist.ICompletionProposal; - -import com.avaloq.tools.ddk.xtext.test.AcfContentAssistProcessorTestBuilder; -import com.avaloq.tools.ddk.xtext.test.TagCompilationParticipant; -import com.google.common.collect.Maps; -import com.google.inject.Injector; - - -/** - * The Class AcfContentAssistTest provides utility operations for non-UI content assist - * tests. It allows testing content assist on non-complete models step by step, as a user - * would also use it when defining a new model in the corresponding editor. - */ -// CHECKSTYLE:OFF -@SuppressWarnings("nls") -public abstract class AbstractAcfContentAssistTest extends AbstractXtextMarkerBasedTest { - private final AcfContentAssistMarkerTagsInfo acfContentAssistMarkerTagInfo = new AcfContentAssistMarkerTagsInfo(); - private static final String AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED = "At least one proposal was provided"; - private static final String EXPECTED_PROPOSALS_NOT_SET = "No expected proposals provided to check against actual proposals."; - // CHECKSTYLE:ON - private static final String SEPARATOR = ", "; - - /** - * Creates a new completion proposal builder. - * - * @return the content assist processor test builder - */ - protected AcfContentAssistProcessorTestBuilder newBuilder() { - try { - return new AcfContentAssistProcessorTestBuilder(getXtextTestUtil().get(Injector.class), getTestUtil()); - // CHECKSTYLE:OFF - } catch (Exception e) { - // CHECKSTYLE:ON - throw new WrappedException("Could not create ContentAssistProcessorTestBuilder.", e); - } - } - - /** - * Gets the completion proposal display strings. - * - * @param computedProposals - * the computed proposals - * @return the completion proposal display strings - */ - private String getCompletionProposalDisplayStrings(final ICompletionProposal... computedProposals) { - String result = ""; - for (ICompletionProposal p : computedProposals) { - result += p.getDisplayString() + SEPARATOR; // NOPMD - } - if (result != null && result.length() > SEPARATOR.length()) { - return result.substring(0, result.length() - SEPARATOR.length()); - } - return null; - } - - /** - * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected - * proposals not found. - * - * @param computedProposals - * the computed proposals - * @param positiveTest - * indicates the type of test. - * TRUE: the test is positive and must fail if proposals not found. - * FALSE: the test is negative and must fail if proposals found. - * @param proposals - * the expected proposals as display strings - */ - private void assertCompletionProposal(final ICompletionProposal[] computedProposals, final boolean positiveTest, final String... proposals) { - assertNotEquals(AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED, proposals.length, 0); - for (final String s : proposals) { - boolean foundProposal = false; - for (ICompletionProposal p : computedProposals) { - if (s.equals(p.getDisplayString())) { - foundProposal = true; - break; - } - } - if (positiveTest && !foundProposal) { - fail(MessageFormat.format("Expected to find proposal \"{0}\" but could only find \"{1}\"", s, getCompletionProposalDisplayStrings(computedProposals))); - } else if (!positiveTest && foundProposal) { - fail(MessageFormat.format("Not expected to find proposal \"{0}\" but found \"{1}\"", s, getCompletionProposalDisplayStrings(computedProposals))); - } - } - } - - /** - * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected - * proposals not found. - * - * @param computedProposals - * the computed proposals - * @param expectedProposals - * the expected proposals as display strings - */ - protected void assertCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { - assertCompletionProposal(computedProposals, true, expectedProposals); - } - - /** - * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected - * proposals not found. - * - * @param cursorPosition - * the cursor position within the main test source - * @param expectedProposals - * the expected proposals as display strings - */ - protected void assertCompletionProposal(final int cursorPosition, final String... expectedProposals) { - assertCompletionProposal(newBuilder().computeCompletionProposals(getTestSource(), cursorPosition), expectedProposals); - } - - /** - * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more not expected - * proposals found. - * - * @param computedProposals - * the computed proposals - * @param expectedProposals - * the not expected proposals as display strings - */ - protected void assertNotCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { - assertCompletionProposal(computedProposals, false, expectedProposals); - } - - /** - * Ensures that the list of expected proposals corresponds exactly the list of computed proposals. - * - * @param computedProposals - * the computed proposals - * @param expectedProposals - * the expected proposals as display strings - */ - protected void assertExactlyCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { - assertNotEquals(AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED, expectedProposals.length, 0); - - Set computedProposalsAsSet = new HashSet(); - for (ICompletionProposal p : computedProposals) { - computedProposalsAsSet.add(p.getDisplayString()); - } - - Set expectedProposalsAsSet = new HashSet(); - expectedProposalsAsSet.addAll(Arrays.asList(expectedProposals)); - - if (computedProposalsAsSet.size() != expectedProposalsAsSet.size()) { - // Calculate missing templates - Set missing = new HashSet(expectedProposalsAsSet); - missing.removeAll(computedProposalsAsSet); - if (!missing.isEmpty()) { - fail(MessageFormat.format("Proposals not found: \"{0}\".", missing.toString())); - } - // Calculate false positives - Set notExpected = new HashSet(computedProposalsAsSet); - notExpected.removeAll(expectedProposalsAsSet); - if (!notExpected.isEmpty()) { - fail(MessageFormat.format("Not expected: \"{0}\".", notExpected.toString())); - } - } - } - - /** - * Asserts the expected valid and invalid proposals based on the given registered source filename. - * - * @param sourceFileName - * the filename of the test source that the proposals were to be computed from, must not be {@code null} - */ - @SuppressWarnings("restriction") - private void assertSourceProposals(final String sourceFileName) { - try { - AcfContentAssistProcessorTestBuilder builder = newBuilder().append(getTestSource(sourceFileName).getContent()); - assertFalse(EXPECTED_PROPOSALS_NOT_SET, (acfContentAssistMarkerTagInfo.expectedProposalMap.isEmpty() - && acfContentAssistMarkerTagInfo.notExpectedProposalMap.isEmpty() && acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.isEmpty())); - for (int markerId : getUsedTagsItems()) { - final ICompletionProposal[] proposals = builder.computeCompletionProposals(getOffsetForTag(markerId)); - if (acfContentAssistMarkerTagInfo.expectedProposalMap.containsKey(markerId)) { - assertCompletionProposal(proposals, acfContentAssistMarkerTagInfo.expectedProposalMap.get(markerId)); - } - if (acfContentAssistMarkerTagInfo.notExpectedProposalMap.containsKey(markerId)) { - assertNotCompletionProposal(proposals, acfContentAssistMarkerTagInfo.notExpectedProposalMap.get(markerId)); - } - if (acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.containsKey(markerId)) { - assertExactlyCompletionProposal(proposals, acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.get(markerId)); - } - } - // CHECKSTYLE:OFF - } catch (Exception e) { - // CHECKSTYLE:ON - throw new WrappedException("Could not assert the expected valid and invalid proposals.", e); - } - } - - /** - * Asserts the proposals for the given kernel test filename and content. - * - * @param sourceFileName - * the filename of the test source that the proposals were to be computed from, must not be {@code null} - * @param sourceContent - * the content of the test source, must not be {@code null} - */ - protected void assertKernelSourceProposals(final String sourceFileName, final CharSequence sourceContent) { - addKernelSourceToWorkspace(sourceFileName, sourceContent); - assertSourceProposals(sourceFileName); - } - - /** - * Asserts the proposals for the given customer test filename and content. - * - * @param sourceFileName - * the filename of the test source that the proposals were to be computed from, must not be {@code null} - * @param sourceContent - * the content of the test source, must not be {@code null} - */ - protected void assertCustomerSourceProposals(final String sourceFileName, final CharSequence sourceContent) { - addCustomerSourceToWorkspace(sourceFileName, sourceContent); - assertSourceProposals(sourceFileName); - } - - /** - * Registers the marker with the given proposals. Each of the expected proposal must have a match with the computed proposals on the marked position. - * - * @param proposals - * the expected proposals, must not be {@code null} - * @return mark text to be inserted in the source file, never {@code null} - */ - protected String expected(final String... proposals) { - Integer markerId = getTag(); - acfContentAssistMarkerTagInfo.expectedProposalMap.put(markerId, proposals); - return mark(markerId); - } - - /** - * Registers the marker with the given proposals. The expected proposals must EXACTLY match the computed proposals on the marked position. - * - * @param proposals - * the exact expected proposals, must not be {@code null} - * @return mark text to be inserted in the source file, never {@code null} - */ - protected String expectedExactly(final String... proposals) { - Integer markerId = getTag(); - acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.put(markerId, proposals); - return mark(markerId); - } - - /** - * Registers the marker with the given proposals. Each of the expected proposal must NOT match with any of the computed proposals on the marked position. - * - * @param proposals - * the expected invalid proposals, must not be {@code null} - * @return mark text to be inserted in the source file, never {@code null} - */ - protected String notExpected(final String... proposals) { - Integer markerId = getTag(); - acfContentAssistMarkerTagInfo.notExpectedProposalMap.put(markerId, proposals); - return mark(markerId); - } - - /** - * Stores the valid proposals in a corresponding map using the given marker id. - * This method will return the marker id that will be used to {@link #mark()} the position in - * which the valid proposals are expected. - * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. - * - * @param markerId - * the unique marker id, must not be {@code null}} - * @param proposals - * the expected valid proposals, must not be {@code null} - * @return the marker id, never {@code null} - */ - protected int expected(final int markerId, final String... proposals) { - acfContentAssistMarkerTagInfo.expectedProposalMap.put(markerId, proposals); - return markerId; - } - - /** - * Stores the invalid proposals in a corresponding map using the given marker id. - * This method will return the marker id that will be used to {@link #mark()} the position in which the proposals are not expected. - * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. - * - * @param markerId - * the unique marker id, must not be {@code null}} - * @param proposals - * the expected invalid proposals, must not be {@code null} - * @return the marker id, never {@code null} - */ - protected int notExpected(final int markerId, final String... proposals) { - acfContentAssistMarkerTagInfo.notExpectedProposalMap.put(markerId, proposals); - return markerId; - } - - /** - * Stores the exact valid proposals in a corresponding map using the given marker id. - * This method will return the marker id that will be used to {@link #mark()} the position in which the exact proposals are expected. - * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. - * - * @param markerId - * the unique marker id, must not be {@code null}} - * @param proposals - * the expected exact proposals, must not be {@code null} - * @return the marker id, never {@code null} - */ - protected int expectedExactly(final int markerId, final String... proposals) { - acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.put(markerId, proposals); - return markerId; - } - - /** - * This class preserves information about tags in the sources for all tests within - * a content assist test class. - */ - private final class AcfContentAssistMarkerTagsInfo extends MarkerTagsInfo { - private final Map expectedProposalMap = Maps.newHashMap(); - private final Map notExpectedProposalMap = Maps.newHashMap(); - private final Map expectedExactlyProposalMap = Maps.newHashMap(); - - @Override - public void clearTags(final long maxId) { - super.clearTags(maxId); - expectedProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); - notExpectedProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); - expectedExactlyProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); - } - } - - @Override - protected AcfContentAssistMarkerTagsInfo getMarkerTagsInfo() { - AcfContentAssistMarkerTagsInfo info = (AcfContentAssistMarkerTagsInfo) getTestInformation().getTestObject(AcfContentAssistMarkerTagsInfo.class); - if (info == null) { - info = acfContentAssistMarkerTagInfo; - getTestInformation().putTestObject(AcfContentAssistMarkerTagsInfo.class, acfContentAssistMarkerTagInfo); - } - return info; - } - -} +/******************************************************************************* + * 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.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.emf.common.util.WrappedException; +import org.eclipse.jface.text.contentassist.ICompletionProposal; + +import com.avaloq.tools.ddk.xtext.test.AcfContentAssistProcessorTestBuilder; +import com.avaloq.tools.ddk.xtext.test.TagCompilationParticipant; +import com.google.common.collect.Maps; +import com.google.inject.Injector; + + +/** + * The Class AcfContentAssistTest provides utility operations for non-UI content assist + * tests. It allows testing content assist on non-complete models step by step, as a user + * would also use it when defining a new model in the corresponding editor. + */ +// CHECKSTYLE:OFF +@SuppressWarnings("nls") +public abstract class AbstractAcfContentAssistTest extends AbstractXtextMarkerBasedTest { + private final AcfContentAssistMarkerTagsInfo acfContentAssistMarkerTagInfo = new AcfContentAssistMarkerTagsInfo(); + private static final String AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED = "At least one proposal was provided"; + private static final String EXPECTED_PROPOSALS_NOT_SET = "No expected proposals provided to check against actual proposals."; + // CHECKSTYLE:ON + private static final String SEPARATOR = ", "; + + /** + * Creates a new completion proposal builder. + * + * @return the content assist processor test builder + */ + protected AcfContentAssistProcessorTestBuilder newBuilder() { + try { + return new AcfContentAssistProcessorTestBuilder(getXtextTestUtil().get(Injector.class), getTestUtil()); + // CHECKSTYLE:OFF + } catch (Exception e) { + // CHECKSTYLE:ON + throw new WrappedException("Could not create ContentAssistProcessorTestBuilder.", e); + } + } + + /** + * Gets the completion proposal display strings. + * + * @param computedProposals + * the computed proposals + * @return the completion proposal display strings + */ + private String getCompletionProposalDisplayStrings(final ICompletionProposal... computedProposals) { + String result = ""; + for (ICompletionProposal p : computedProposals) { + result += p.getDisplayString() + SEPARATOR; // NOPMD + } + if (result != null && result.length() > SEPARATOR.length()) { + return result.substring(0, result.length() - SEPARATOR.length()); + } + return null; + } + + /** + * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected + * proposals not found. + * + * @param computedProposals + * the computed proposals + * @param positiveTest + * indicates the type of test. + * TRUE: the test is positive and must fail if proposals not found. + * FALSE: the test is negative and must fail if proposals found. + * @param proposals + * the expected proposals as display strings + */ + private void assertCompletionProposal(final ICompletionProposal[] computedProposals, final boolean positiveTest, final String... proposals) { + assertNotEquals(AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED, proposals.length, 0); + for (final String s : proposals) { + boolean foundProposal = false; + for (ICompletionProposal p : computedProposals) { + if (s.equals(p.getDisplayString())) { + foundProposal = true; + break; + } + } + if (positiveTest && !foundProposal) { + fail(MessageFormat.format("Expected to find proposal \"{0}\" but could only find \"{1}\"", s, getCompletionProposalDisplayStrings(computedProposals))); + } else if (!positiveTest && foundProposal) { + fail(MessageFormat.format("Not expected to find proposal \"{0}\" but found \"{1}\"", s, getCompletionProposalDisplayStrings(computedProposals))); + } + } + } + + /** + * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected + * proposals not found. + * + * @param computedProposals + * the computed proposals + * @param expectedProposals + * the expected proposals as display strings + */ + protected void assertCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { + assertCompletionProposal(computedProposals, true, expectedProposals); + } + + /** + * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more expected + * proposals not found. + * + * @param cursorPosition + * the cursor position within the main test source + * @param expectedProposals + * the expected proposals as display strings + */ + protected void assertCompletionProposal(final int cursorPosition, final String... expectedProposals) { + assertCompletionProposal(newBuilder().computeCompletionProposals(getTestSource(), cursorPosition), expectedProposals); + } + + /** + * Iterate over given computed completion proposals and compare resulting display string with each expected proposal text. Fail if one or more not expected + * proposals found. + * + * @param computedProposals + * the computed proposals + * @param expectedProposals + * the not expected proposals as display strings + */ + protected void assertNotCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { + assertCompletionProposal(computedProposals, false, expectedProposals); + } + + /** + * Ensures that the list of expected proposals corresponds exactly the list of computed proposals. + * + * @param computedProposals + * the computed proposals + * @param expectedProposals + * the expected proposals as display strings + */ + protected void assertExactlyCompletionProposal(final ICompletionProposal[] computedProposals, final String... expectedProposals) { + assertNotEquals(AT_LEAST_ONE_PROPOSAL_WAS_PROVIDED, expectedProposals.length, 0); + + Set computedProposalsAsSet = new HashSet(); + for (ICompletionProposal p : computedProposals) { + computedProposalsAsSet.add(p.getDisplayString()); + } + + Set expectedProposalsAsSet = new HashSet(); + expectedProposalsAsSet.addAll(Arrays.asList(expectedProposals)); + + if (computedProposalsAsSet.size() != expectedProposalsAsSet.size()) { + // Calculate missing templates + Set missing = new HashSet(expectedProposalsAsSet); + missing.removeAll(computedProposalsAsSet); + if (!missing.isEmpty()) { + fail(MessageFormat.format("Proposals not found: \"{0}\".", missing.toString())); + } + // Calculate false positives + Set notExpected = new HashSet(computedProposalsAsSet); + notExpected.removeAll(expectedProposalsAsSet); + if (!notExpected.isEmpty()) { + fail(MessageFormat.format("Not expected: \"{0}\".", notExpected.toString())); + } + } + } + + /** + * Asserts the expected valid and invalid proposals based on the given registered source filename. + * + * @param sourceFileName + * the filename of the test source that the proposals were to be computed from, must not be {@code null} + */ + @SuppressWarnings("restriction") + private void assertSourceProposals(final String sourceFileName) { + try { + AcfContentAssistProcessorTestBuilder builder = newBuilder().append(getTestSource(sourceFileName).getContent()); + assertFalse(EXPECTED_PROPOSALS_NOT_SET, (acfContentAssistMarkerTagInfo.expectedProposalMap.isEmpty() + && acfContentAssistMarkerTagInfo.notExpectedProposalMap.isEmpty() && acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.isEmpty())); + for (int markerId : getUsedTagsItems()) { + final ICompletionProposal[] proposals = builder.computeCompletionProposals(getOffsetForTag(markerId)); + if (acfContentAssistMarkerTagInfo.expectedProposalMap.containsKey(markerId)) { + assertCompletionProposal(proposals, acfContentAssistMarkerTagInfo.expectedProposalMap.get(markerId)); + } + if (acfContentAssistMarkerTagInfo.notExpectedProposalMap.containsKey(markerId)) { + assertNotCompletionProposal(proposals, acfContentAssistMarkerTagInfo.notExpectedProposalMap.get(markerId)); + } + if (acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.containsKey(markerId)) { + assertExactlyCompletionProposal(proposals, acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.get(markerId)); + } + } + // CHECKSTYLE:OFF + } catch (Exception e) { + // CHECKSTYLE:ON + throw new WrappedException("Could not assert the expected valid and invalid proposals.", e); + } + } + + /** + * Asserts the proposals for the given kernel test filename and content. + * + * @param sourceFileName + * the filename of the test source that the proposals were to be computed from, must not be {@code null} + * @param sourceContent + * the content of the test source, must not be {@code null} + */ + protected void assertKernelSourceProposals(final String sourceFileName, final CharSequence sourceContent) { + addKernelSourceToWorkspace(sourceFileName, sourceContent); + assertSourceProposals(sourceFileName); + } + + /** + * Asserts the proposals for the given customer test filename and content. + * + * @param sourceFileName + * the filename of the test source that the proposals were to be computed from, must not be {@code null} + * @param sourceContent + * the content of the test source, must not be {@code null} + */ + protected void assertCustomerSourceProposals(final String sourceFileName, final CharSequence sourceContent) { + addCustomerSourceToWorkspace(sourceFileName, sourceContent); + assertSourceProposals(sourceFileName); + } + + /** + * Registers the marker with the given proposals. Each of the expected proposal must have a match with the computed proposals on the marked position. + * + * @param proposals + * the expected proposals, must not be {@code null} + * @return mark text to be inserted in the source file, never {@code null} + */ + protected String expected(final String... proposals) { + Integer markerId = getTag(); + acfContentAssistMarkerTagInfo.expectedProposalMap.put(markerId, proposals); + return mark(markerId); + } + + /** + * Registers the marker with the given proposals. The expected proposals must EXACTLY match the computed proposals on the marked position. + * + * @param proposals + * the exact expected proposals, must not be {@code null} + * @return mark text to be inserted in the source file, never {@code null} + */ + protected String expectedExactly(final String... proposals) { + Integer markerId = getTag(); + acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.put(markerId, proposals); + return mark(markerId); + } + + /** + * Registers the marker with the given proposals. Each of the expected proposal must NOT match with any of the computed proposals on the marked position. + * + * @param proposals + * the expected invalid proposals, must not be {@code null} + * @return mark text to be inserted in the source file, never {@code null} + */ + protected String notExpected(final String... proposals) { + Integer markerId = getTag(); + acfContentAssistMarkerTagInfo.notExpectedProposalMap.put(markerId, proposals); + return mark(markerId); + } + + /** + * Stores the valid proposals in a corresponding map using the given marker id. + * This method will return the marker id that will be used to {@link #mark()} the position in + * which the valid proposals are expected. + * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. + * + * @param markerId + * the unique marker id, must not be {@code null}} + * @param proposals + * the expected valid proposals, must not be {@code null} + * @return the marker id, never {@code null} + */ + protected int expected(final int markerId, final String... proposals) { + acfContentAssistMarkerTagInfo.expectedProposalMap.put(markerId, proposals); + return markerId; + } + + /** + * Stores the invalid proposals in a corresponding map using the given marker id. + * This method will return the marker id that will be used to {@link #mark()} the position in which the proposals are not expected. + * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. + * + * @param markerId + * the unique marker id, must not be {@code null}} + * @param proposals + * the expected invalid proposals, must not be {@code null} + * @return the marker id, never {@code null} + */ + protected int notExpected(final int markerId, final String... proposals) { + acfContentAssistMarkerTagInfo.notExpectedProposalMap.put(markerId, proposals); + return markerId; + } + + /** + * Stores the exact valid proposals in a corresponding map using the given marker id. + * This method will return the marker id that will be used to {@link #mark()} the position in which the exact proposals are expected. + * Use {@link #getTag()} for the marker id when accessing locally. For global declarations, use @Tag annotation for marker id. + * + * @param markerId + * the unique marker id, must not be {@code null}} + * @param proposals + * the expected exact proposals, must not be {@code null} + * @return the marker id, never {@code null} + */ + protected int expectedExactly(final int markerId, final String... proposals) { + acfContentAssistMarkerTagInfo.expectedExactlyProposalMap.put(markerId, proposals); + return markerId; + } + + /** + * This class preserves information about tags in the sources for all tests within + * a content assist test class. + */ + private final class AcfContentAssistMarkerTagsInfo extends MarkerTagsInfo { + private final Map expectedProposalMap = Maps.newHashMap(); + private final Map notExpectedProposalMap = Maps.newHashMap(); + private final Map expectedExactlyProposalMap = Maps.newHashMap(); + + @Override + public void clearTags(final long maxId) { + super.clearTags(maxId); + expectedProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); + notExpectedProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); + expectedExactlyProposalMap.entrySet().removeIf(entry -> entry.getKey() < TagCompilationParticipant.COUNTER_BASE); + } + } + + @Override + protected AcfContentAssistMarkerTagsInfo getMarkerTagsInfo() { + AcfContentAssistMarkerTagsInfo info = (AcfContentAssistMarkerTagsInfo) getTestInformation().getTestObject(AcfContentAssistMarkerTagsInfo.class); + if (info == null) { + info = acfContentAssistMarkerTagInfo; + getTestInformation().putTestObject(AcfContentAssistMarkerTagsInfo.class, acfContentAssistMarkerTagInfo); + } + return info; + } + +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractFormattingTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractFormattingTest.java index 1763755138..b2d495908b 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractFormattingTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractFormattingTest.java @@ -1,172 +1,172 @@ -/******************************************************************************* - * 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 java.util.List; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.formatting.INodeModelFormatter; -import org.eclipse.xtext.formatting.INodeModelFormatter.IFormattedRegion; -import org.eclipse.xtext.nodemodel.ICompositeNode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.resource.SaveOptions; -import org.junit.jupiter.api.Test; - - -/** - * Base class for formatting tests. - */ -@SuppressWarnings("nls") -public abstract class AbstractFormattingTest extends AbstractXtextTest { - - private static final String CR_LF = "\r\n"; - private static final String LF = "\n"; - - @Override - protected List getRequiredSourceFileNames() { - List result = super.getRequiredSourceFileNames(); - result.add(getExpectedTestSourceFileName()); - return result; - } - - @Override - protected String getTestSourceModelName() { - return super.getTestSourceModelName() + "Input"; - } - - /** - * The default implementation returns the default source model name for the test class and adds 'Expected'. - * this test. A test class needs to override this, if the name of the expected formatting test source model differs from the default. - * - * @return the name of the expected formatting test source model - */ - private String getExpectedTestSourceModelName() { - return super.getTestSourceModelName() + "Expected"; - } - - /** - * The default implementation returns the default source model name for the test class and adds 'Expected' and the default file extension for the grammar of - * this test. A test class needs to override this, if the name of the expected formatting test source file differs from the default. - * - * @return the name of the expected formatting test source file - */ - protected String getExpectedTestSourceFileName() { - return getExpectedTestSourceModelName() + "." + getXtextTestUtil().getFileExtension(); - } - - /** - * Test formatting based on the NodeModel. - */ - @Test - public void formattedNodeModel() { - assertFormattedNodeModel(); - } - - /** - * Test formatting based on the ParseTreeConstructor. - */ - public void formattedParseTreeConstructor() { - assertFormattedParseTreeConstructor(); - } - - /** - * Test preservation of formatting using ParseTreeConstructor. - */ - public void preservedParseTreeConstructor() { - assertPreservedParseTreeConstructor(); - } - - /** - * Test preservation of formatting using NodeModelFormatter. - */ - @Test - public void preservedNodeModel() { - assertPreservedNodeModel(); - } - - /** - * Test formatting based on the ParseTreeConstructor. - */ - protected final void assertFormattedParseTreeConstructor() { - assertFormattedParseTreeConstructor(getSemanticModel(), getTestSource(getExpectedTestSourceFileName()).getContent()); - } - - /** - * Test formatting based on the NodeModel. - * - * @param offset - * Offset from which to start formatting - * @param length - * Length of region to format - */ - private void assertFormattedNodeModel(final int offset, final int length) { - assertFormattedNodeModel(getSemanticModel(), getTestSource().getContent(), getTestSource(getExpectedTestSourceFileName()).getContent(), offset, length); - } - - /** - * Test formatting based on the NodeModel. - */ - private void assertFormattedNodeModel() { - assertFormattedNodeModel(0, getTestSource().getContent().length()); - } - - /** - * Test preservation of formatting. - */ - private void assertPreservedNodeModel() { - String expectedContent = getTestSource(getExpectedTestSourceFileName()).getContent(); - assertFormattedNodeModel(getTestSource(getExpectedTestSourceFileName()).getModel(), expectedContent, expectedContent, 0, expectedContent.length()); - } - - /** - * Test preservation of formatting. - */ - protected final void assertPreservedParseTreeConstructor() { - assertFormattedParseTreeConstructor(getTestSource(getExpectedTestSourceFileName()).getModel(), getTestSource(getExpectedTestSourceFileName()).getContent()); - } - - /** - * Test formatting based on the ParseTreeConstructor. - * - * @param model - * the model to be serialized and compared with expected string - * @param expected - * Expected formatted String - */ - private void assertFormattedParseTreeConstructor(final EObject model, final String expected) { - String actual = getXtextTestUtil().getSerializer().serialize(model, SaveOptions.newBuilder().format().getOptions()); - assertEquals(expected.replaceAll(CR_LF, LF), actual.replaceAll(CR_LF, LF), "Formatted ParseTree"); - } - - /** - * Test formatting based on the NodeModel. - * - * @param model - * the model to check - * @param input - * String representing a serialized model - * @param expected - * Expected formatted String - * @param offset - * Offset from which to start formatting - * @param length - * Length of region to format - */ - private void assertFormattedNodeModel(final EObject model, final String input, final String expected, final int offset, final int length) { - ICompositeNode node = NodeModelUtils.getNode(model).getRootNode(); - IFormattedRegion region = getXtextTestUtil().get(INodeModelFormatter.class).format(node, offset, length); - String actual = input.substring(0, offset) + region.getFormattedText() + input.substring(length + offset); - assertEquals(expected.replaceAll(CR_LF, LF), actual.replaceAll(CR_LF, LF), "Formatted NodeModel"); - } - -} +/******************************************************************************* + * 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 java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.formatting.INodeModelFormatter; +import org.eclipse.xtext.formatting.INodeModelFormatter.IFormattedRegion; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.SaveOptions; +import org.junit.jupiter.api.Test; + + +/** + * Base class for formatting tests. + */ +@SuppressWarnings("nls") +public abstract class AbstractFormattingTest extends AbstractXtextTest { + + private static final String CR_LF = "\r\n"; + private static final String LF = "\n"; + + @Override + protected List getRequiredSourceFileNames() { + List result = super.getRequiredSourceFileNames(); + result.add(getExpectedTestSourceFileName()); + return result; + } + + @Override + protected String getTestSourceModelName() { + return super.getTestSourceModelName() + "Input"; + } + + /** + * The default implementation returns the default source model name for the test class and adds 'Expected'. + * this test. A test class needs to override this, if the name of the expected formatting test source model differs from the default. + * + * @return the name of the expected formatting test source model + */ + private String getExpectedTestSourceModelName() { + return super.getTestSourceModelName() + "Expected"; + } + + /** + * The default implementation returns the default source model name for the test class and adds 'Expected' and the default file extension for the grammar of + * this test. A test class needs to override this, if the name of the expected formatting test source file differs from the default. + * + * @return the name of the expected formatting test source file + */ + protected String getExpectedTestSourceFileName() { + return getExpectedTestSourceModelName() + "." + getXtextTestUtil().getFileExtension(); + } + + /** + * Test formatting based on the NodeModel. + */ + @Test + public void formattedNodeModel() { + assertFormattedNodeModel(); + } + + /** + * Test formatting based on the ParseTreeConstructor. + */ + public void formattedParseTreeConstructor() { + assertFormattedParseTreeConstructor(); + } + + /** + * Test preservation of formatting using ParseTreeConstructor. + */ + public void preservedParseTreeConstructor() { + assertPreservedParseTreeConstructor(); + } + + /** + * Test preservation of formatting using NodeModelFormatter. + */ + @Test + public void preservedNodeModel() { + assertPreservedNodeModel(); + } + + /** + * Test formatting based on the ParseTreeConstructor. + */ + protected final void assertFormattedParseTreeConstructor() { + assertFormattedParseTreeConstructor(getSemanticModel(), getTestSource(getExpectedTestSourceFileName()).getContent()); + } + + /** + * Test formatting based on the NodeModel. + * + * @param offset + * Offset from which to start formatting + * @param length + * Length of region to format + */ + private void assertFormattedNodeModel(final int offset, final int length) { + assertFormattedNodeModel(getSemanticModel(), getTestSource().getContent(), getTestSource(getExpectedTestSourceFileName()).getContent(), offset, length); + } + + /** + * Test formatting based on the NodeModel. + */ + private void assertFormattedNodeModel() { + assertFormattedNodeModel(0, getTestSource().getContent().length()); + } + + /** + * Test preservation of formatting. + */ + private void assertPreservedNodeModel() { + String expectedContent = getTestSource(getExpectedTestSourceFileName()).getContent(); + assertFormattedNodeModel(getTestSource(getExpectedTestSourceFileName()).getModel(), expectedContent, expectedContent, 0, expectedContent.length()); + } + + /** + * Test preservation of formatting. + */ + protected final void assertPreservedParseTreeConstructor() { + assertFormattedParseTreeConstructor(getTestSource(getExpectedTestSourceFileName()).getModel(), getTestSource(getExpectedTestSourceFileName()).getContent()); + } + + /** + * Test formatting based on the ParseTreeConstructor. + * + * @param model + * the model to be serialized and compared with expected string + * @param expected + * Expected formatted String + */ + private void assertFormattedParseTreeConstructor(final EObject model, final String expected) { + String actual = getXtextTestUtil().getSerializer().serialize(model, SaveOptions.newBuilder().format().getOptions()); + assertEquals(expected.replaceAll(CR_LF, LF), actual.replaceAll(CR_LF, LF), "Formatted ParseTree"); + } + + /** + * Test formatting based on the NodeModel. + * + * @param model + * the model to check + * @param input + * String representing a serialized model + * @param expected + * Expected formatted String + * @param offset + * Offset from which to start formatting + * @param length + * Length of region to format + */ + private void assertFormattedNodeModel(final EObject model, final String input, final String expected, final int offset, final int length) { + ICompositeNode node = NodeModelUtils.getNode(model).getRootNode(); + IFormattedRegion region = getXtextTestUtil().get(INodeModelFormatter.class).format(node, offset, length); + String actual = input.substring(0, offset) + region.getFormattedText() + input.substring(length + offset); + assertEquals(expected.replaceAll(CR_LF, LF), actual.replaceAll(CR_LF, LF), "Formatted NodeModel"); + } + +} 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 index ee5891b2a5..aa740921e4 100644 --- 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 @@ -1,455 +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(expected, actual, message); - } - } - - /** - * 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; - } -} +/******************************************************************************* + * 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(expected, actual, message); + } + } + + /** + * 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/AbstractScopingTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractScopingTest.java index 87b0c5b684..d873827ac5 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractScopingTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractScopingTest.java @@ -1,875 +1,875 @@ -/******************************************************************************* - * 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 com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider.REP_SEPARATOR; -import static com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider.SEGMENT_SEPARATOR; -import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.EQ_OP; -import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.SELECTOR_END; -import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.SELECTOR_START; -import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.UNIQUE; -import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.VALUE_SEP; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; -import java.util.regex.Pattern; - -import org.eclipse.core.runtime.Assert; -import org.eclipse.emf.common.util.EList; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.EReference; -import org.eclipse.emf.ecore.util.EObjectResolvingEList; -import org.eclipse.emf.ecore.util.EcoreUtil; -import org.eclipse.osgi.util.NLS; -import org.eclipse.xtext.Assignment; -import org.eclipse.xtext.CrossReference; -import org.eclipse.xtext.EcoreUtil2; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.resource.IEObjectDescription; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.eclipse.xtext.resource.XtextResource; -import org.eclipse.xtext.scoping.IScope; -import org.eclipse.xtext.scoping.IScopeProvider; -import org.eclipse.xtext.util.Triple; -import org.eclipse.xtext.xbase.lib.Pair; - -import com.avaloq.tools.ddk.caching.Regexps; -import com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider; -import com.avaloq.tools.ddk.xtext.naming.QualifiedNames; -import com.avaloq.tools.ddk.xtext.resource.IFingerprintComputer; -import com.avaloq.tools.ddk.xtext.scoping.ContainerQuery; -import com.avaloq.tools.ddk.xtext.scoping.IDomain; -import com.google.common.base.Function; -import com.google.common.base.Splitter; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - - -/** - * Base class for scoping tests. - */ -@SuppressWarnings("nls") -public abstract class AbstractScopingTest extends AbstractXtextMarkerBasedTest { - private static final String PARAMETER_EXPECTED_OBJECTS = "expectedObjects"; - private static final String PARAMETER_REFERENCE = "reference"; - private static final String PARAMETER_CONTEXT = "context"; - public static final String TOP_LEVEL_OBJECT_FRAGMENT = SEGMENT_SEPARATOR + "0" + SEGMENT_SEPARATOR + "1"; - public static final String TOP_LEVEL_SURROGATE_FRAGMENT = SEGMENT_SEPARATOR + "0" + REP_SEPARATOR + "2"; - public static final String INFERRED_DATA_DICTIONARY_FRAGMENT = SEGMENT_SEPARATOR + "1"; - - private static final String NUMBER_OF_ELEMENTS_MESSAGE = "Incorrect number of elements in scope."; - private static final Splitter FRAGMENT_SEGMENT_SPLITTER = Splitter.onPattern("(? expectedLinkAssertions = new ArrayList(); - - /** - * Creates a new instance of {@link AbstractScopingTest}. - */ - public AbstractScopingTest() { - this(new IDomain.NullMapper()); - - } - - /** - * Creates a new instance of {@link AbstractScopingTest}. - * - * @param domainMapper - * the domainMapper to use - */ - public AbstractScopingTest(final IDomain.Mapper domainMapper) { - this.domainMapper = domainMapper; - } - - /** - * Returns all contents of the main {@link XtextTestResource}. - * - * @return all contents of the main {@link XtextTestResource} - */ - @SuppressWarnings("unchecked") - public Iterable getContents() { - return (Iterable) getTestInformation().getTestObject(Iterable.class); - } - - /** - * Set up scoping. - */ - @Override - protected void beforeAllTests() { - super.beforeAllTests(); - Iterable allContents = new Iterable() { - @Override - public Iterator iterator() { - return getXtextTestResource().getAllContents(); - } - }; - getTestInformation().putTestObject(Iterable.class, allContents); - } - - @Override - protected void afterEachTest() { - assertTrue(expectedLinkAssertions.isEmpty(), "Expected links were set with link(int) but testLinking(String, CharSequence) was never called"); - super.afterEachTest(); - } - - /** - * Returns the scope provider used for unit testing. - * - * @return the scope provider instance - */ - protected IScopeProvider getScopeProvider() { - return getXtextTestUtil().get(IScopeProvider.class); - } - - /** - * Check if scope expected is found in context provided. - * - * @param context - * element from which an element shall be referenced - * @param reference - * to be used to filter the elements - * @param expectedSourceName - * (upper case!) name of scope element to look for (kernel source) - * @param expectedSourceType - * type of scope element to look for - */ - protected void assertScope(final EObject context, final EReference reference, final String expectedSourceName, final String expectedSourceType) { - assertScope(context, reference, QualifiedNames.safeQualifiedName(expectedSourceName), getTargetSourceUri(expectedSourceName + '.' - + expectedSourceType).appendFragment(TOP_LEVEL_OBJECT_FRAGMENT)); - } - - /** - * Check if scope expected is found in context provided. - * - * @param context - * element from which an element shall be referenced - * @param reference - * to be used to filter the elements - * @param expectedElementName - * name of scope element to look for - * @param expectedSourceName - * (upper case!) name of the source within to look for the scope element - * @param expectedSourceType - * type of scope element to look for - * @param referenceElementType - * the type of the referenced element - */ - @SuppressWarnings("PMD.UseObjectForClearerAPI") - protected void assertScopeForElement(final EObject context, final EReference reference, final String expectedElementName, final String expectedSourceName, final String expectedSourceType, final String referenceElementType) { - assertScope(context, reference, QualifiedNames.safeQualifiedName(expectedElementName), getTargetSourceUri(expectedSourceName + '.' - + expectedSourceType).appendFragment(TOP_LEVEL_OBJECT_FRAGMENT + SEGMENT_SEPARATOR + referenceElementType)); - } - - /** - * Check if scope expected is found in context provided. - * - * @param context - * element from which an element shall be referenced - * @param reference - * to be used to filter the elements - * @param expectedUris - * of source referenced - */ - protected void assertScope(final EObject context, final EReference reference, final URI... expectedUris) { - assertScope(context, reference, Sets.newHashSet(expectedUris)); - } - - /** - * Check if scope expected is found in context provided. - * - * @param context - * element from which an element shall be referenced - * @param reference - * to be used to filter the elements - * @param expectedUriSet - * of source referenced - */ - protected void assertScope(final EObject context, final EReference reference, final Set expectedUriSet) { - IScope scope = getScopeProvider().getScope(context, reference); - for (IEObjectDescription description : scope.getAllElements()) { - expectedUriSet.remove(description.getEObjectURI()); - if (expectedUriSet.isEmpty()) { - return; - } - } - assertTrue(expectedUriSet.isEmpty(), "Expected URIs not found in scope: " + expectedUriSet); - } - - /** - * Checks if the given objects are in scope of the given reference for the given context. - * - * @param context - * {@link EObject} element from which an element shall be referenced, must not be {@code null} - * @param reference - * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element - * @param expectedObjects - * for given scope, must not be {@code null} - */ - protected void assertScopedObjects(final EObject context, final EReference reference, final EObject... expectedObjects) { - Assert.isNotNull(expectedObjects, PARAMETER_EXPECTED_OBJECTS); - assertScopedObjects(context, reference, Lists.newArrayList(expectedObjects)); - } - - /** - * Checks if the given objects are in scope of the given reference for the given context. - * - * @param context - * {@link EObject} element from which an element shall be referenced, must not be {@code null} - * @param reference - * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element - * @param firstExpectedObjectCollection - * for given scope, must not be {@code null} - * @param furtherExpectedObjectCollections - * for given scope, must not be {@code null} - */ - @SuppressWarnings("unchecked") - protected void assertScopedObjects(final EObject context, final EReference reference, final Collection firstExpectedObjectCollection, final Collection... furtherExpectedObjectCollections) { - Assert.isNotNull(firstExpectedObjectCollection, "firstExpectedObjectCollection"); - Assert.isNotNull(furtherExpectedObjectCollections, "furtherExpectedObjectCollections"); - Collection consolidatedList = Lists.newArrayList(firstExpectedObjectCollection); - for (Collection expectedObjects : furtherExpectedObjectCollections) { - consolidatedList.addAll(expectedObjects); - } - assertScopedObjects(context, reference, consolidatedList); - } - - /** - * Checks if the scope of the given reference for the given context contains only the expected objects. - * In addition, checks that the reference of the given context references at least one of the expected - * objects. If the reference has multiplicity > 1, then every reference must reference at least - * one of the expected objects. - * - * @param context - * {@link EObject} from which the given objects shall be referenced, must not be {@code null} - * @param reference - * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element - * @param expectedObjects - * the objects expected in the scope, must not be {@code null} - */ - protected void assertScopedObjects(final EObject context, final EReference reference, final Collection expectedObjects) { - Assert.isNotNull(context, PARAMETER_CONTEXT); - Assert.isNotNull(reference, PARAMETER_REFERENCE); - Assert.isNotNull(expectedObjects, PARAMETER_EXPECTED_OBJECTS); - Assert.isTrue(context.eClass().getEAllReferences().contains(reference), String.format("Contract for argument '%s' failed: Parameter is not within specified range (Expected: %s, Actual: %s).", PARAMETER_CONTEXT, "The context object must contain the given reference.", "Reference not contained by the context object!")); - Set expectedUriSet = Sets.newHashSet(); - for (EObject object : expectedObjects) { - expectedUriSet.add(EcoreUtil.getURI(object)); - } - IScope scope = getScopeProvider().getScope(context, reference); - Iterable allScopedElements = scope.getAllElements(); - Set scopedUriSet = Sets.newHashSet(); - for (IEObjectDescription description : allScopedElements) { - URI uri = description.getEObjectURI(); - scopedUriSet.add(uri); - } - if (!expectedUriSet.equals(scopedUriSet)) { - fail("The scope must exactly consist of the expected URIs. Missing " + Sets.difference(expectedUriSet, scopedUriSet) + " extra " - + Sets.difference(scopedUriSet, expectedUriSet)); - } - // test that link resolving worked - boolean elementResolved; - if (reference.isMany()) { - @SuppressWarnings("unchecked") - EList objects = (EList) context.eGet(reference, true); - elementResolved = !objects.isEmpty(); // NOPMD - for (Iterator objectIter = objects.iterator(); objectIter.hasNext() && elementResolved;) { - EObject eObject = EcoreUtil.resolve(objectIter.next(), context); - elementResolved = expectedUriSet.contains(EcoreUtil.getURI(eObject)); - } - } else { - EObject resolvedObject = (EObject) context.eGet(reference, true); - elementResolved = expectedUriSet.contains(EcoreUtil.getURI(resolvedObject)); - } - assertTrue(elementResolved, "Linking must have resolved one of the expected objects."); - } - - /** - * Check if scope expected is found in context provided. - * - * @param context - * element from which an element shall be referenced - * @param reference - * to be used to filter the elements - * @param expectedName - * name of scope element to look for - * @param expectedUri - * of source referenced - */ - private void assertScope(final EObject context, final EReference reference, final QualifiedName expectedName, final URI expectedUri) { - IScope scope = getScopeProvider().getScope(context, reference); - Iterable descriptions = scope.getElements(expectedName); - assertFalse(Iterables.isEmpty(descriptions), "Description missing for: " + expectedName); - URI currentUri = null; - for (IEObjectDescription desc : descriptions) { - currentUri = desc.getEObjectURI(); - if (currentUri.equals(expectedUri)) { - return; - } - } - assertEquals(expectedUri, currentUri, "Scope URI is not equal to expected URI"); - } - - /** - * Assert the scope for given elements. - * - * @param context - * the context - * @param reference - * the reference - * @param expectedSourceName - * the name of the referenced source (without file extension) - * @param expectedSourceType - * type of scope element to look for - * @param elementNames - * array of tuples with the name and uri of each element - */ - protected void assertScopeForElements(final EObject context, final EReference reference, final String expectedSourceName, final String expectedSourceType, final String[]... elementNames) { - for (String[] elementName : elementNames) { - assertScopeForElement(context, reference, elementName[0], expectedSourceName, expectedSourceType, elementName[1]); - } - int actualScopeSize = Iterables.size(getScopeProvider().getScope(context, reference).getAllElements()); - assertEquals(elementNames.length, actualScopeSize, NUMBER_OF_ELEMENTS_MESSAGE); - } - - /** - * Asserts the scope for the given context, reference, source type, and elements. - * - * @param context - * the context object - * @param reference - * the reference feature - * @param expectedSourceType - * the source-type name - * @param elements - * list of triples with the expected elements, each triple ordered as: {element name, source name, URI fragment} - */ - protected void assertScopeForElements(final EObject context, final EReference reference, final String expectedSourceType, final List> elements) { - Iterable allElements = getScopeProvider().getScope(context, reference).getAllElements(); - - // create a set containing the URIs (to avoid counting any duplicates the scope provider might have delivered) - Set uris = new HashSet(); - for (IEObjectDescription d : allElements) { - uris.add(d.getEObjectURI()); - } - - int actualScopeSizeWithoutDuplicates = uris.size(); - assertEquals(elements.size(), actualScopeSizeWithoutDuplicates, NUMBER_OF_ELEMENTS_MESSAGE); - for (Triple elementName : elements) { - assertScopeForElement(context, reference, elementName.getFirst(), elementName.getSecond(), expectedSourceType, elementName.getThird()); - } - } - - /** - * Asserts that the scope of the reference in the given context contains exactly the given sources. - * - * @param scopeContext - * the context of the scope test - * @param reference - * the reference to check its scope for - * @param modelElementClass - * the {@link EClass} of the model element to find - * @param sources - * the array of sources - */ - protected void assertScopeForSources(final EObject scopeContext, final EReference reference, final EClass modelElementClass, final String... sources) { - assertScope(scopeContext, reference, getExpectedURIs(scopeContext, modelElementClass, sources)); - int actualScopeSize = Iterables.size(getScopeProvider().getScope(scopeContext, reference).getAllElements()); - assertEquals(sources.length, actualScopeSize, NUMBER_OF_ELEMENTS_MESSAGE); - } - - /** - * Returns the expected uris for the given sources in the given context. - * - * @param context - * the context - * @param modelElementClass - * the class of the exported model element - * @param sources - * the sources to get the uris for - * @return the expected uris for the given sources in the given context - */ - private Set getExpectedURIs(final EObject context, final EClass modelElementClass, final String... sources) { - Set expectedURIs = new HashSet(); - for (String source : sources) { - expectedURIs.add(Iterables.get(getExportedObjects(context, modelElementClass, source), 0).getEObjectURI()); - } - return expectedURIs; - } - - /** - * Gets the exported objects. - * - * @param context - * the context - * @param type - * the type - * @param resourcePattern - * the resource pattern - * @return the exported objects - */ - public Iterable getExportedObjects(final EObject context, final EClass type, final String resourcePattern) { - Pattern regexp = Regexps.fromGlob(URI.encodeSegment(resourcePattern, true)); - return Iterables.filter(ContainerQuery.newBuilder(domainMapper, type).execute(context), (o) -> regexp.matcher(o.getEObjectURI().lastSegment()).matches()); - } - - /** - * Gets the exported names. - * - * @param execute - * the execute - * @return the exported names - */ - public List getExportedNames(final Iterable execute) { - return Lists.newArrayList(Iterables.transform(execute, new Function() { - @Override - public String apply(final IEObjectDescription from) { - return from.getName().toString(); - } - })); - } - - /** - * Checks if an object with given name (case sensitive) and type is exported. - * - * @param context - * the context - * @param name - * the name - * @param type - * the type - * @return true, if is exported - */ - public boolean isExported(final EObject context, final String name, final EClass type) { - return isExported(context, name, type, false); - } - - /** - * Checks if an object with given name, case sensitive or not, and type is exported. - * - * @param context - * the context - * @param name - * the name - * @param type - * the type - * @param ignoreCase - * the ignore case - * @return true, if is exported - */ - public boolean isExported(final EObject context, final String name, final EClass type, final boolean ignoreCase) { - List exportedNames = getExportedNames(ContainerQuery.newBuilder(domainMapper, type).execute(context)); - if (ignoreCase) { - return Iterables.contains(Iterables.transform(exportedNames, new Function() { - @Override - public String apply(final String from) { - return from.toLowerCase(); // NOPMD - } - }), name); - } else { - return exportedNames.contains(name); - } - } - - /** - * Gets the resource description for a given Xtext resource. - * - * @param resource - * the resource - * @return the resource description - */ - protected final IResourceDescription getResourceDescription(final XtextResource resource) { - final IResourceServiceProvider resourceServiceProvider = resource.getResourceServiceProvider(); - final IResourceDescription.Manager descriptionManager = resourceServiceProvider.getResourceDescriptionManager(); - return descriptionManager.getResourceDescription(resource); - } - - /** - * Gets the fingerprint for a given resource description, returns null if resource description does not export any objects or if a non-existing - * user data field was queried. - * - * @param description - * the description - * @return the fingerprint or null if no fingerprint found - */ - protected String getFingerprint(final IResourceDescription description) { - Iterable objects = description.getExportedObjects(); - if (!Iterables.isEmpty(objects)) { - IEObjectDescription objectDescription = Iterables.get(objects, 0); - return objectDescription.getUserData(IFingerprintComputer.RESOURCE_FINGERPRINT); - } - return null; - } - - /** - * Creates a top level URI fragment with a leading segment separator from the given segments - * taking into account repetitions. - * - * @param segments - * list of feature IDs, indexes (for multi valued features), and other fragment segments - * @return URI fragment - */ - public static String createTopLevelURIFragment(final Object... segments) { - return createURIFragment(true, segments); - } - - /** - * Creates a URI fragment from the given segments taking into account repetitions. - * - * @param segments - * list of feature IDs, indexes (for multi valued features), and other fragment segments - * @return URI fragment - */ - public static String createURIFragment(final Object... segments) { - return createURIFragment(false, segments); - } - - /** - * Creates a URI fragment from the given segments taking into account repetitions. - * - * @param topLevel - * whether the fragment is top level resulting in a leading segment separator. - * @param segments - * list of feature IDs, indexes (for multi valued features), and other fragment segments - * @return URI fragment - */ - @SuppressWarnings("PMD.UnusedPrivateMethod") - private static String createURIFragment(final boolean topLevel, final Object... segments) { - StringBuilder b = new StringBuilder(); - if (segments.length == 0) { - return b.toString(); - } - if (topLevel) { - b.append(SEGMENT_SEPARATOR); - } - List parsedSegments = Lists.newArrayList(); - for (Object segment : segments) { - Iterables.addAll(parsedSegments, FRAGMENT_SEGMENT_SPLITTER.split(segment.toString())); - } - - String lastSegment = parsedSegments.get(0); - int reps = 1; - for (int i = 1; i < parsedSegments.size(); i++) { - if (parsedSegments.get(i).equals(lastSegment)) { - reps++; - continue; - } - b.append(lastSegment); - if (reps > 1) { - b.append(REP_SEPARATOR).append(reps); - reps = 1; - } - b.append(SEGMENT_SEPARATOR); - lastSegment = parsedSegments.get(i); - } - b.append(lastSegment); - if (reps > 1) { - b.append(REP_SEPARATOR).append(reps); - } - return b.toString(); - } - - /** - * Creates a URI fragment list segment for the given feature selection string and list index. - * - * @param feature - * the feature selection string, must not be {@code null} or empty - * @param index - * the list index, must not be negative - * @return the URI fragment list segment, never {@code null} or empty - */ - public static String listFragmentSegment(final String feature, final int index) { - return feature + AbstractFragmentProvider.LIST_SEPARATOR + index; - } - - /** - * Creates a URI fragment list segment for the given feature id and list index. - * - * @param featureId - * the featureId, must not be negative - * @param index - * the list index, must not be negative - * @return the URI fragment list segment, never {@code null} or empty - */ - public static String listFragmentSegment(final int featureId, final int index) { - return listFragmentSegment(String.valueOf(featureId), index); - } - - /** - * Creates a URI fragment segment to be used for languages using the {@code AbstractSelectorFragmentProvider}. - * - * @param containmentFeature - * containment feature - * @param selectorFeature - * selector feature - * @param value - * value for selector feature - * @param unique - * if value is unique - * @return URI fragment segment - */ - public static String selectorFragmentSegment(final int containmentFeature, final int selectorFeature, final String value, final boolean unique) { - StringBuilder builder = new StringBuilder(); - builder.append(containmentFeature).append(SELECTOR_START).append(selectorFeature).append(EQ_OP).append(VALUE_SEP).append(value).append(VALUE_SEP); - if (unique) { - builder.append(UNIQUE); - } - builder.append(SELECTOR_END); - return builder.toString(); - } - - /** - * Creates an expectation of a link. Use this method in tests to insert an expectation that a cross reference does actually point to the object tagged by the - * target tag. Expectations can be tested by calling {@link #testExpectedLinking()}. Implicit items will be traversed. - * - * @see #mark(int) - * @see #testLinking(String, CharSequence) - * @param targetTag - * Tag pointing to the destination object - * @return Mark text to be inserted in the source file, never {@code null} - */ - protected String link(final int targetTag) { - return link(() -> getObjectForTag(targetTag)); - } - - /** - * Creates an expectation of a link. Use this method in tests to insert an expectation that a cross reference does actually point to the object tagged by the - * target tag. Expectations can be tested by calling {@link #testExpectedLinking()}. Implicit items will be traversed. - * - * @see #mark(int) - * @see #testLinking(String, CharSequence) - * @param getTargetObject - * supplier to get the destination object, must not be {@code null} - * @return Mark text to be inserted in the source file, never {@code null} - */ - protected String link(final Supplier getTargetObject) { - final int sourceTag = getTag(); - expectedLinkAssertions.add(() -> testLinking(sourceTag, getTargetObject.get())); - return mark(sourceTag); - } - - /** - * Performs linking test. Checks expectations which were set in a source using {@link #link(int)} or {@link #link(Function, int). - * - * @see #link(int) - * @see #link(Supplier) - * @see #testLinking(int, EObject) - * @param sourceFileNameAndContent - * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} - */ - protected void testLinking(final Pair sourceFileNameAndContent) { - testLinking(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); - } - - /** - * Performs linking test. Checks expectations which were set in a source using {@link #link(int)} or {@link #link(Function, int). - * - * @see #link(int) - * @see #link(Supplier) - * @see #testLinking(int, EObject) - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - */ - protected void testLinking(final String sourceFileName, final CharSequence sourceContent) { - registerModel(sourceFileName, sourceContent); - expectedLinkAssertions.forEach(Runnable::run); - expectedLinkAssertions.clear(); - } - - /** - * Performs linking test. Checks that given cross reference tagged with sourceTag does actually point to the object tagged by the target tag. - * Detailed error reporting can be viewed in a compare view. Implicit items will be traversed. - * - * @param sourceTag - * Tag pointing to cross reference - * @param targetTag - * Tag pointing to the destination object - */ - protected void testLinking(final int sourceTag, final int targetTag) { - testLinking(sourceTag, getObjectForTag(targetTag), true); - } - - /** - * Performs linking test. Checks that the cross reference marked with sourceTag does actually point to the object provided as the second argument. Implicit - * items will be traversed. - * - * @param sourceTag - * Tag pointing to a cross reference - * @param targetObject - * Expected target object - */ - protected void testLinking(final int sourceTag, final EObject targetObject) { - testLinking(sourceTag, targetObject, true); - } - - /** - * Performs linking test. Checks that the source object is the same as the object pointed by targetTag provided as the second argument. Implicit - * items will be traversed. - * - * @param sourceObject - * Source object - * @param targetTag - * Tag to the expected target object - */ - protected void testLinking(final EObject sourceObject, final int targetTag) { - testLinking(sourceObject, targetTag, true); - } - - /** - * Performs linking test. Checks that the cross reference marked with sourceTag does actually point to the object provided as the second argument. - * - * @param sourceTag - * Tag pointing to a cross reference - * @param targetObject - * Expected target object, must not be {@code null} - * @param traverseImplicitItems - * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this - * implicit item was created - */ - protected void testLinking(final int sourceTag, final EObject targetObject, final boolean traverseImplicitItems) { - assertNotNull(targetObject, "Target object must not be null."); //$NON-NLS-1$ - CrossReference crossReference = getMarkerTagsInfo().getCrossReference(sourceTag); - EObject referencedSourceObject = getCrossReferencedObject(sourceTag, traverseImplicitItems, crossReference); - assertEObjectsAreEqual(referencedSourceObject, targetObject, crossReference); - } - - /** - * Performs linking test. Checks that the source object is the same as the object pointed by targetTag provided as the second argument. - * Does not deal with cross-referencing. - * - * @param sourceObject - * Source object, must not be {@code null} - * @param targetTag - * Tag to the referenced target object - * @param traverseImplicitItems - * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this - * implicit item was created - */ - protected void testLinking(final EObject sourceObject, final int targetTag, final boolean traverseImplicitItems) { - assertNotNull(sourceObject, "Source object must not be null."); //$NON-NLS-1$ - EObject referencedTargetObject = getObjectForTag(targetTag); - assertEObjectsAreEqual(sourceObject, referencedTargetObject, null); - } - - /** - * Asserts whether the two objects are equal. - * - * @param sourceObject - * First object needed for comparison. - * @param targetObject - * Target object needed for comparison. - * @param crossReference - * CrossReference object, can be {@code null} - */ - protected void assertEObjectsAreEqual(final EObject sourceObject, final EObject targetObject, final CrossReference crossReference) { - StringBuilder expected = new StringBuilder(); - StringBuilder found = new StringBuilder(); - if (crossReference != null) { - String crossReferenceText = "Cross reference:\n" + crossReference.toString() + "\n"; //$NON-NLS-1$ //$NON-NLS-2$ - expected.append(crossReferenceText); - found.append(crossReferenceText); - } - expected.append(LINKS_TO); - found.append(LINKS_TO); - URI targetUri = EcoreUtil.getURI(targetObject); - expected.append(targetUri); - String sourceObjectUri = EcoreUtil.getURI(sourceObject).toString(); - found.append(sourceObjectUri); - expected.append(WHICH_CORRESPONDS_TO); - INode node; - node = NodeModelUtils.findActualNodeFor(targetObject); - if (node != null) { - expected.append(NodeModelUtils.getTokenText(node)); - } else { - expected.append(NO_NODE_MODEL_COULD_BE_A_DERIVED_OBJECT); - } - found.append(WHICH_CORRESPONDS_TO); - node = NodeModelUtils.findActualNodeFor(sourceObject); - if (sourceObject.eIsProxy()) { - found.append(UNRESOLVED_REFERENCE); - } else if (node != null) { - found.append(NodeModelUtils.getTokenText(node)); - } else { - found.append(NO_NODE_MODEL_COULD_BE_A_DERIVED_OBJECT); - } - assertEquals(expected.toString(), found.toString(), "Errors found. Consider compare view."); //$NON-NLS-1$ - } - - /** - * Returns the referenced {@link EObject} pointed to by the cross reference. - *

- * Note: For implicit item traversal to work, a custom implementation must be provided by overriding this method. - *

- * - * @param sourceTag - * the source tag - * @param traverseImplicitItems - * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this - * implicit item was created. - * @param crossReference - * Cross reference to be resolved, must not be {@code null} - * @return the referenced {@link EObject}, must not be {@code null} - */ - protected EObject getCrossReferencedObject(final int sourceTag, final boolean traverseImplicitItems, final CrossReference crossReference) { - EObject context = getObjectForTag(sourceTag); - if (crossReference == null) { - throw new IllegalArgumentException(NLS.bind("Cross reference on object ''{0}'' could not be resolved.", context.toString())); //$NON-NLS-1$ - } - // We only handle references in assignments - Assignment assignment = EcoreUtil2.getContainerOfType(crossReference, Assignment.class); - EObject sourceObject; - String featureName = assignment.getFeature(); - EReference reference = (EReference) context.eClass().getEStructuralFeature(featureName); - if (reference.isMany()) { - Object featureValue = context.eGet(reference, false); - assertInstanceOf(EObjectResolvingEList.class, featureValue, "List must be of type EObjectResolvingEList"); //$NON-NLS-1$ - @SuppressWarnings("unchecked") - EList objects = (EObjectResolvingEList) context.eGet(reference, false); - if (objects.size() == 1) { - sourceObject = EcoreUtil.resolve(objects.get(0), context); - } else { - // TODO DSL-166: Handle this case when needed for tests. - throw new AssertionError("Multiple references not supported yet"); //$NON-NLS-1$ - } - } else { - sourceObject = (EObject) context.eGet(reference, true); - } - assertNotNull(sourceObject, "Bad test. Referenced object is null."); //$NON-NLS-1$ - return sourceObject; - } -} +/******************************************************************************* + * 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 com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider.REP_SEPARATOR; +import static com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider.SEGMENT_SEPARATOR; +import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.EQ_OP; +import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.SELECTOR_END; +import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.SELECTOR_START; +import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.UNIQUE; +import static com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProvider.VALUE_SEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.util.EObjectResolvingEList; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.osgi.util.NLS; +import org.eclipse.xtext.Assignment; +import org.eclipse.xtext.CrossReference; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceServiceProvider; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.scoping.IScope; +import org.eclipse.xtext.scoping.IScopeProvider; +import org.eclipse.xtext.util.Triple; +import org.eclipse.xtext.xbase.lib.Pair; + +import com.avaloq.tools.ddk.caching.Regexps; +import com.avaloq.tools.ddk.xtext.linking.AbstractFragmentProvider; +import com.avaloq.tools.ddk.xtext.naming.QualifiedNames; +import com.avaloq.tools.ddk.xtext.resource.IFingerprintComputer; +import com.avaloq.tools.ddk.xtext.scoping.ContainerQuery; +import com.avaloq.tools.ddk.xtext.scoping.IDomain; +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + + +/** + * Base class for scoping tests. + */ +@SuppressWarnings("nls") +public abstract class AbstractScopingTest extends AbstractXtextMarkerBasedTest { + private static final String PARAMETER_EXPECTED_OBJECTS = "expectedObjects"; + private static final String PARAMETER_REFERENCE = "reference"; + private static final String PARAMETER_CONTEXT = "context"; + public static final String TOP_LEVEL_OBJECT_FRAGMENT = SEGMENT_SEPARATOR + "0" + SEGMENT_SEPARATOR + "1"; + public static final String TOP_LEVEL_SURROGATE_FRAGMENT = SEGMENT_SEPARATOR + "0" + REP_SEPARATOR + "2"; + public static final String INFERRED_DATA_DICTIONARY_FRAGMENT = SEGMENT_SEPARATOR + "1"; + + private static final String NUMBER_OF_ELEMENTS_MESSAGE = "Incorrect number of elements in scope."; + private static final Splitter FRAGMENT_SEGMENT_SPLITTER = Splitter.onPattern("(? expectedLinkAssertions = new ArrayList(); + + /** + * Creates a new instance of {@link AbstractScopingTest}. + */ + public AbstractScopingTest() { + this(new IDomain.NullMapper()); + + } + + /** + * Creates a new instance of {@link AbstractScopingTest}. + * + * @param domainMapper + * the domainMapper to use + */ + public AbstractScopingTest(final IDomain.Mapper domainMapper) { + this.domainMapper = domainMapper; + } + + /** + * Returns all contents of the main {@link XtextTestResource}. + * + * @return all contents of the main {@link XtextTestResource} + */ + @SuppressWarnings("unchecked") + public Iterable getContents() { + return (Iterable) getTestInformation().getTestObject(Iterable.class); + } + + /** + * Set up scoping. + */ + @Override + protected void beforeAllTests() { + super.beforeAllTests(); + Iterable allContents = new Iterable() { + @Override + public Iterator iterator() { + return getXtextTestResource().getAllContents(); + } + }; + getTestInformation().putTestObject(Iterable.class, allContents); + } + + @Override + protected void afterEachTest() { + assertTrue(expectedLinkAssertions.isEmpty(), "Expected links were set with link(int) but testLinking(String, CharSequence) was never called"); + super.afterEachTest(); + } + + /** + * Returns the scope provider used for unit testing. + * + * @return the scope provider instance + */ + protected IScopeProvider getScopeProvider() { + return getXtextTestUtil().get(IScopeProvider.class); + } + + /** + * Check if scope expected is found in context provided. + * + * @param context + * element from which an element shall be referenced + * @param reference + * to be used to filter the elements + * @param expectedSourceName + * (upper case!) name of scope element to look for (kernel source) + * @param expectedSourceType + * type of scope element to look for + */ + protected void assertScope(final EObject context, final EReference reference, final String expectedSourceName, final String expectedSourceType) { + assertScope(context, reference, QualifiedNames.safeQualifiedName(expectedSourceName), getTargetSourceUri(expectedSourceName + '.' + + expectedSourceType).appendFragment(TOP_LEVEL_OBJECT_FRAGMENT)); + } + + /** + * Check if scope expected is found in context provided. + * + * @param context + * element from which an element shall be referenced + * @param reference + * to be used to filter the elements + * @param expectedElementName + * name of scope element to look for + * @param expectedSourceName + * (upper case!) name of the source within to look for the scope element + * @param expectedSourceType + * type of scope element to look for + * @param referenceElementType + * the type of the referenced element + */ + @SuppressWarnings("PMD.UseObjectForClearerAPI") + protected void assertScopeForElement(final EObject context, final EReference reference, final String expectedElementName, final String expectedSourceName, final String expectedSourceType, final String referenceElementType) { + assertScope(context, reference, QualifiedNames.safeQualifiedName(expectedElementName), getTargetSourceUri(expectedSourceName + '.' + + expectedSourceType).appendFragment(TOP_LEVEL_OBJECT_FRAGMENT + SEGMENT_SEPARATOR + referenceElementType)); + } + + /** + * Check if scope expected is found in context provided. + * + * @param context + * element from which an element shall be referenced + * @param reference + * to be used to filter the elements + * @param expectedUris + * of source referenced + */ + protected void assertScope(final EObject context, final EReference reference, final URI... expectedUris) { + assertScope(context, reference, Sets.newHashSet(expectedUris)); + } + + /** + * Check if scope expected is found in context provided. + * + * @param context + * element from which an element shall be referenced + * @param reference + * to be used to filter the elements + * @param expectedUriSet + * of source referenced + */ + protected void assertScope(final EObject context, final EReference reference, final Set expectedUriSet) { + IScope scope = getScopeProvider().getScope(context, reference); + for (IEObjectDescription description : scope.getAllElements()) { + expectedUriSet.remove(description.getEObjectURI()); + if (expectedUriSet.isEmpty()) { + return; + } + } + assertTrue(expectedUriSet.isEmpty(), "Expected URIs not found in scope: " + expectedUriSet); + } + + /** + * Checks if the given objects are in scope of the given reference for the given context. + * + * @param context + * {@link EObject} element from which an element shall be referenced, must not be {@code null} + * @param reference + * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element + * @param expectedObjects + * for given scope, must not be {@code null} + */ + protected void assertScopedObjects(final EObject context, final EReference reference, final EObject... expectedObjects) { + Assert.isNotNull(expectedObjects, PARAMETER_EXPECTED_OBJECTS); + assertScopedObjects(context, reference, Lists.newArrayList(expectedObjects)); + } + + /** + * Checks if the given objects are in scope of the given reference for the given context. + * + * @param context + * {@link EObject} element from which an element shall be referenced, must not be {@code null} + * @param reference + * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element + * @param firstExpectedObjectCollection + * for given scope, must not be {@code null} + * @param furtherExpectedObjectCollections + * for given scope, must not be {@code null} + */ + @SuppressWarnings("unchecked") + protected void assertScopedObjects(final EObject context, final EReference reference, final Collection firstExpectedObjectCollection, final Collection... furtherExpectedObjectCollections) { + Assert.isNotNull(firstExpectedObjectCollection, "firstExpectedObjectCollection"); + Assert.isNotNull(furtherExpectedObjectCollections, "furtherExpectedObjectCollections"); + Collection consolidatedList = Lists.newArrayList(firstExpectedObjectCollection); + for (Collection expectedObjects : furtherExpectedObjectCollections) { + consolidatedList.addAll(expectedObjects); + } + assertScopedObjects(context, reference, consolidatedList); + } + + /** + * Checks if the scope of the given reference for the given context contains only the expected objects. + * In addition, checks that the reference of the given context references at least one of the expected + * objects. If the reference has multiplicity > 1, then every reference must reference at least + * one of the expected objects. + * + * @param context + * {@link EObject} from which the given objects shall be referenced, must not be {@code null} + * @param reference + * the structural feature of {@code context} for which the scope should be asserted, must not be {@code null} and part of the context element + * @param expectedObjects + * the objects expected in the scope, must not be {@code null} + */ + protected void assertScopedObjects(final EObject context, final EReference reference, final Collection expectedObjects) { + Assert.isNotNull(context, PARAMETER_CONTEXT); + Assert.isNotNull(reference, PARAMETER_REFERENCE); + Assert.isNotNull(expectedObjects, PARAMETER_EXPECTED_OBJECTS); + Assert.isTrue(context.eClass().getEAllReferences().contains(reference), String.format("Contract for argument '%s' failed: Parameter is not within specified range (Expected: %s, Actual: %s).", PARAMETER_CONTEXT, "The context object must contain the given reference.", "Reference not contained by the context object!")); + Set expectedUriSet = Sets.newHashSet(); + for (EObject object : expectedObjects) { + expectedUriSet.add(EcoreUtil.getURI(object)); + } + IScope scope = getScopeProvider().getScope(context, reference); + Iterable allScopedElements = scope.getAllElements(); + Set scopedUriSet = Sets.newHashSet(); + for (IEObjectDescription description : allScopedElements) { + URI uri = description.getEObjectURI(); + scopedUriSet.add(uri); + } + if (!expectedUriSet.equals(scopedUriSet)) { + fail("The scope must exactly consist of the expected URIs. Missing " + Sets.difference(expectedUriSet, scopedUriSet) + " extra " + + Sets.difference(scopedUriSet, expectedUriSet)); + } + // test that link resolving worked + boolean elementResolved; + if (reference.isMany()) { + @SuppressWarnings("unchecked") + EList objects = (EList) context.eGet(reference, true); + elementResolved = !objects.isEmpty(); // NOPMD + for (Iterator objectIter = objects.iterator(); objectIter.hasNext() && elementResolved;) { + EObject eObject = EcoreUtil.resolve(objectIter.next(), context); + elementResolved = expectedUriSet.contains(EcoreUtil.getURI(eObject)); + } + } else { + EObject resolvedObject = (EObject) context.eGet(reference, true); + elementResolved = expectedUriSet.contains(EcoreUtil.getURI(resolvedObject)); + } + assertTrue(elementResolved, "Linking must have resolved one of the expected objects."); + } + + /** + * Check if scope expected is found in context provided. + * + * @param context + * element from which an element shall be referenced + * @param reference + * to be used to filter the elements + * @param expectedName + * name of scope element to look for + * @param expectedUri + * of source referenced + */ + private void assertScope(final EObject context, final EReference reference, final QualifiedName expectedName, final URI expectedUri) { + IScope scope = getScopeProvider().getScope(context, reference); + Iterable descriptions = scope.getElements(expectedName); + assertFalse(Iterables.isEmpty(descriptions), "Description missing for: " + expectedName); + URI currentUri = null; + for (IEObjectDescription desc : descriptions) { + currentUri = desc.getEObjectURI(); + if (currentUri.equals(expectedUri)) { + return; + } + } + assertEquals(expectedUri, currentUri, "Scope URI is not equal to expected URI"); + } + + /** + * Assert the scope for given elements. + * + * @param context + * the context + * @param reference + * the reference + * @param expectedSourceName + * the name of the referenced source (without file extension) + * @param expectedSourceType + * type of scope element to look for + * @param elementNames + * array of tuples with the name and uri of each element + */ + protected void assertScopeForElements(final EObject context, final EReference reference, final String expectedSourceName, final String expectedSourceType, final String[]... elementNames) { + for (String[] elementName : elementNames) { + assertScopeForElement(context, reference, elementName[0], expectedSourceName, expectedSourceType, elementName[1]); + } + int actualScopeSize = Iterables.size(getScopeProvider().getScope(context, reference).getAllElements()); + assertEquals(elementNames.length, actualScopeSize, NUMBER_OF_ELEMENTS_MESSAGE); + } + + /** + * Asserts the scope for the given context, reference, source type, and elements. + * + * @param context + * the context object + * @param reference + * the reference feature + * @param expectedSourceType + * the source-type name + * @param elements + * list of triples with the expected elements, each triple ordered as: {element name, source name, URI fragment} + */ + protected void assertScopeForElements(final EObject context, final EReference reference, final String expectedSourceType, final List> elements) { + Iterable allElements = getScopeProvider().getScope(context, reference).getAllElements(); + + // create a set containing the URIs (to avoid counting any duplicates the scope provider might have delivered) + Set uris = new HashSet(); + for (IEObjectDescription d : allElements) { + uris.add(d.getEObjectURI()); + } + + int actualScopeSizeWithoutDuplicates = uris.size(); + assertEquals(elements.size(), actualScopeSizeWithoutDuplicates, NUMBER_OF_ELEMENTS_MESSAGE); + for (Triple elementName : elements) { + assertScopeForElement(context, reference, elementName.getFirst(), elementName.getSecond(), expectedSourceType, elementName.getThird()); + } + } + + /** + * Asserts that the scope of the reference in the given context contains exactly the given sources. + * + * @param scopeContext + * the context of the scope test + * @param reference + * the reference to check its scope for + * @param modelElementClass + * the {@link EClass} of the model element to find + * @param sources + * the array of sources + */ + protected void assertScopeForSources(final EObject scopeContext, final EReference reference, final EClass modelElementClass, final String... sources) { + assertScope(scopeContext, reference, getExpectedURIs(scopeContext, modelElementClass, sources)); + int actualScopeSize = Iterables.size(getScopeProvider().getScope(scopeContext, reference).getAllElements()); + assertEquals(sources.length, actualScopeSize, NUMBER_OF_ELEMENTS_MESSAGE); + } + + /** + * Returns the expected uris for the given sources in the given context. + * + * @param context + * the context + * @param modelElementClass + * the class of the exported model element + * @param sources + * the sources to get the uris for + * @return the expected uris for the given sources in the given context + */ + private Set getExpectedURIs(final EObject context, final EClass modelElementClass, final String... sources) { + Set expectedURIs = new HashSet(); + for (String source : sources) { + expectedURIs.add(Iterables.get(getExportedObjects(context, modelElementClass, source), 0).getEObjectURI()); + } + return expectedURIs; + } + + /** + * Gets the exported objects. + * + * @param context + * the context + * @param type + * the type + * @param resourcePattern + * the resource pattern + * @return the exported objects + */ + public Iterable getExportedObjects(final EObject context, final EClass type, final String resourcePattern) { + Pattern regexp = Regexps.fromGlob(URI.encodeSegment(resourcePattern, true)); + return Iterables.filter(ContainerQuery.newBuilder(domainMapper, type).execute(context), (o) -> regexp.matcher(o.getEObjectURI().lastSegment()).matches()); + } + + /** + * Gets the exported names. + * + * @param execute + * the execute + * @return the exported names + */ + public List getExportedNames(final Iterable execute) { + return Lists.newArrayList(Iterables.transform(execute, new Function() { + @Override + public String apply(final IEObjectDescription from) { + return from.getName().toString(); + } + })); + } + + /** + * Checks if an object with given name (case sensitive) and type is exported. + * + * @param context + * the context + * @param name + * the name + * @param type + * the type + * @return true, if is exported + */ + public boolean isExported(final EObject context, final String name, final EClass type) { + return isExported(context, name, type, false); + } + + /** + * Checks if an object with given name, case sensitive or not, and type is exported. + * + * @param context + * the context + * @param name + * the name + * @param type + * the type + * @param ignoreCase + * the ignore case + * @return true, if is exported + */ + public boolean isExported(final EObject context, final String name, final EClass type, final boolean ignoreCase) { + List exportedNames = getExportedNames(ContainerQuery.newBuilder(domainMapper, type).execute(context)); + if (ignoreCase) { + return Iterables.contains(Iterables.transform(exportedNames, new Function() { + @Override + public String apply(final String from) { + return from.toLowerCase(); // NOPMD + } + }), name); + } else { + return exportedNames.contains(name); + } + } + + /** + * Gets the resource description for a given Xtext resource. + * + * @param resource + * the resource + * @return the resource description + */ + protected final IResourceDescription getResourceDescription(final XtextResource resource) { + final IResourceServiceProvider resourceServiceProvider = resource.getResourceServiceProvider(); + final IResourceDescription.Manager descriptionManager = resourceServiceProvider.getResourceDescriptionManager(); + return descriptionManager.getResourceDescription(resource); + } + + /** + * Gets the fingerprint for a given resource description, returns null if resource description does not export any objects or if a non-existing + * user data field was queried. + * + * @param description + * the description + * @return the fingerprint or null if no fingerprint found + */ + protected String getFingerprint(final IResourceDescription description) { + Iterable objects = description.getExportedObjects(); + if (!Iterables.isEmpty(objects)) { + IEObjectDescription objectDescription = Iterables.get(objects, 0); + return objectDescription.getUserData(IFingerprintComputer.RESOURCE_FINGERPRINT); + } + return null; + } + + /** + * Creates a top level URI fragment with a leading segment separator from the given segments + * taking into account repetitions. + * + * @param segments + * list of feature IDs, indexes (for multi valued features), and other fragment segments + * @return URI fragment + */ + public static String createTopLevelURIFragment(final Object... segments) { + return createURIFragment(true, segments); + } + + /** + * Creates a URI fragment from the given segments taking into account repetitions. + * + * @param segments + * list of feature IDs, indexes (for multi valued features), and other fragment segments + * @return URI fragment + */ + public static String createURIFragment(final Object... segments) { + return createURIFragment(false, segments); + } + + /** + * Creates a URI fragment from the given segments taking into account repetitions. + * + * @param topLevel + * whether the fragment is top level resulting in a leading segment separator. + * @param segments + * list of feature IDs, indexes (for multi valued features), and other fragment segments + * @return URI fragment + */ + @SuppressWarnings("PMD.UnusedPrivateMethod") + private static String createURIFragment(final boolean topLevel, final Object... segments) { + StringBuilder b = new StringBuilder(); + if (segments.length == 0) { + return b.toString(); + } + if (topLevel) { + b.append(SEGMENT_SEPARATOR); + } + List parsedSegments = Lists.newArrayList(); + for (Object segment : segments) { + Iterables.addAll(parsedSegments, FRAGMENT_SEGMENT_SPLITTER.split(segment.toString())); + } + + String lastSegment = parsedSegments.get(0); + int reps = 1; + for (int i = 1; i < parsedSegments.size(); i++) { + if (parsedSegments.get(i).equals(lastSegment)) { + reps++; + continue; + } + b.append(lastSegment); + if (reps > 1) { + b.append(REP_SEPARATOR).append(reps); + reps = 1; + } + b.append(SEGMENT_SEPARATOR); + lastSegment = parsedSegments.get(i); + } + b.append(lastSegment); + if (reps > 1) { + b.append(REP_SEPARATOR).append(reps); + } + return b.toString(); + } + + /** + * Creates a URI fragment list segment for the given feature selection string and list index. + * + * @param feature + * the feature selection string, must not be {@code null} or empty + * @param index + * the list index, must not be negative + * @return the URI fragment list segment, never {@code null} or empty + */ + public static String listFragmentSegment(final String feature, final int index) { + return feature + AbstractFragmentProvider.LIST_SEPARATOR + index; + } + + /** + * Creates a URI fragment list segment for the given feature id and list index. + * + * @param featureId + * the featureId, must not be negative + * @param index + * the list index, must not be negative + * @return the URI fragment list segment, never {@code null} or empty + */ + public static String listFragmentSegment(final int featureId, final int index) { + return listFragmentSegment(String.valueOf(featureId), index); + } + + /** + * Creates a URI fragment segment to be used for languages using the {@code AbstractSelectorFragmentProvider}. + * + * @param containmentFeature + * containment feature + * @param selectorFeature + * selector feature + * @param value + * value for selector feature + * @param unique + * if value is unique + * @return URI fragment segment + */ + public static String selectorFragmentSegment(final int containmentFeature, final int selectorFeature, final String value, final boolean unique) { + StringBuilder builder = new StringBuilder(); + builder.append(containmentFeature).append(SELECTOR_START).append(selectorFeature).append(EQ_OP).append(VALUE_SEP).append(value).append(VALUE_SEP); + if (unique) { + builder.append(UNIQUE); + } + builder.append(SELECTOR_END); + return builder.toString(); + } + + /** + * Creates an expectation of a link. Use this method in tests to insert an expectation that a cross reference does actually point to the object tagged by the + * target tag. Expectations can be tested by calling {@link #testExpectedLinking()}. Implicit items will be traversed. + * + * @see #mark(int) + * @see #testLinking(String, CharSequence) + * @param targetTag + * Tag pointing to the destination object + * @return Mark text to be inserted in the source file, never {@code null} + */ + protected String link(final int targetTag) { + return link(() -> getObjectForTag(targetTag)); + } + + /** + * Creates an expectation of a link. Use this method in tests to insert an expectation that a cross reference does actually point to the object tagged by the + * target tag. Expectations can be tested by calling {@link #testExpectedLinking()}. Implicit items will be traversed. + * + * @see #mark(int) + * @see #testLinking(String, CharSequence) + * @param getTargetObject + * supplier to get the destination object, must not be {@code null} + * @return Mark text to be inserted in the source file, never {@code null} + */ + protected String link(final Supplier getTargetObject) { + final int sourceTag = getTag(); + expectedLinkAssertions.add(() -> testLinking(sourceTag, getTargetObject.get())); + return mark(sourceTag); + } + + /** + * Performs linking test. Checks expectations which were set in a source using {@link #link(int)} or {@link #link(Function, int). + * + * @see #link(int) + * @see #link(Supplier) + * @see #testLinking(int, EObject) + * @param sourceFileNameAndContent + * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} + */ + protected void testLinking(final Pair sourceFileNameAndContent) { + testLinking(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); + } + + /** + * Performs linking test. Checks expectations which were set in a source using {@link #link(int)} or {@link #link(Function, int). + * + * @see #link(int) + * @see #link(Supplier) + * @see #testLinking(int, EObject) + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + */ + protected void testLinking(final String sourceFileName, final CharSequence sourceContent) { + registerModel(sourceFileName, sourceContent); + expectedLinkAssertions.forEach(Runnable::run); + expectedLinkAssertions.clear(); + } + + /** + * Performs linking test. Checks that given cross reference tagged with sourceTag does actually point to the object tagged by the target tag. + * Detailed error reporting can be viewed in a compare view. Implicit items will be traversed. + * + * @param sourceTag + * Tag pointing to cross reference + * @param targetTag + * Tag pointing to the destination object + */ + protected void testLinking(final int sourceTag, final int targetTag) { + testLinking(sourceTag, getObjectForTag(targetTag), true); + } + + /** + * Performs linking test. Checks that the cross reference marked with sourceTag does actually point to the object provided as the second argument. Implicit + * items will be traversed. + * + * @param sourceTag + * Tag pointing to a cross reference + * @param targetObject + * Expected target object + */ + protected void testLinking(final int sourceTag, final EObject targetObject) { + testLinking(sourceTag, targetObject, true); + } + + /** + * Performs linking test. Checks that the source object is the same as the object pointed by targetTag provided as the second argument. Implicit + * items will be traversed. + * + * @param sourceObject + * Source object + * @param targetTag + * Tag to the expected target object + */ + protected void testLinking(final EObject sourceObject, final int targetTag) { + testLinking(sourceObject, targetTag, true); + } + + /** + * Performs linking test. Checks that the cross reference marked with sourceTag does actually point to the object provided as the second argument. + * + * @param sourceTag + * Tag pointing to a cross reference + * @param targetObject + * Expected target object, must not be {@code null} + * @param traverseImplicitItems + * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this + * implicit item was created + */ + protected void testLinking(final int sourceTag, final EObject targetObject, final boolean traverseImplicitItems) { + assertNotNull(targetObject, "Target object must not be null."); //$NON-NLS-1$ + CrossReference crossReference = getMarkerTagsInfo().getCrossReference(sourceTag); + EObject referencedSourceObject = getCrossReferencedObject(sourceTag, traverseImplicitItems, crossReference); + assertEObjectsAreEqual(referencedSourceObject, targetObject, crossReference); + } + + /** + * Performs linking test. Checks that the source object is the same as the object pointed by targetTag provided as the second argument. + * Does not deal with cross-referencing. + * + * @param sourceObject + * Source object, must not be {@code null} + * @param targetTag + * Tag to the referenced target object + * @param traverseImplicitItems + * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this + * implicit item was created + */ + protected void testLinking(final EObject sourceObject, final int targetTag, final boolean traverseImplicitItems) { + assertNotNull(sourceObject, "Source object must not be null."); //$NON-NLS-1$ + EObject referencedTargetObject = getObjectForTag(targetTag); + assertEObjectsAreEqual(sourceObject, referencedTargetObject, null); + } + + /** + * Asserts whether the two objects are equal. + * + * @param sourceObject + * First object needed for comparison. + * @param targetObject + * Target object needed for comparison. + * @param crossReference + * CrossReference object, can be {@code null} + */ + protected void assertEObjectsAreEqual(final EObject sourceObject, final EObject targetObject, final CrossReference crossReference) { + StringBuilder expected = new StringBuilder(); + StringBuilder found = new StringBuilder(); + if (crossReference != null) { + String crossReferenceText = "Cross reference:\n" + crossReference.toString() + "\n"; //$NON-NLS-1$ //$NON-NLS-2$ + expected.append(crossReferenceText); + found.append(crossReferenceText); + } + expected.append(LINKS_TO); + found.append(LINKS_TO); + URI targetUri = EcoreUtil.getURI(targetObject); + expected.append(targetUri); + String sourceObjectUri = EcoreUtil.getURI(sourceObject).toString(); + found.append(sourceObjectUri); + expected.append(WHICH_CORRESPONDS_TO); + INode node; + node = NodeModelUtils.findActualNodeFor(targetObject); + if (node != null) { + expected.append(NodeModelUtils.getTokenText(node)); + } else { + expected.append(NO_NODE_MODEL_COULD_BE_A_DERIVED_OBJECT); + } + found.append(WHICH_CORRESPONDS_TO); + node = NodeModelUtils.findActualNodeFor(sourceObject); + if (sourceObject.eIsProxy()) { + found.append(UNRESOLVED_REFERENCE); + } else if (node != null) { + found.append(NodeModelUtils.getTokenText(node)); + } else { + found.append(NO_NODE_MODEL_COULD_BE_A_DERIVED_OBJECT); + } + assertEquals(expected.toString(), found.toString(), "Errors found. Consider compare view."); //$NON-NLS-1$ + } + + /** + * Returns the referenced {@link EObject} pointed to by the cross reference. + *

+ * Note: For implicit item traversal to work, a custom implementation must be provided by overriding this method. + *

+ * + * @param sourceTag + * the source tag + * @param traverseImplicitItems + * If target of a reference is an implicit item and this parameter is set to true, the test will get and compare the original object from which this + * implicit item was created. + * @param crossReference + * Cross reference to be resolved, must not be {@code null} + * @return the referenced {@link EObject}, must not be {@code null} + */ + protected EObject getCrossReferencedObject(final int sourceTag, final boolean traverseImplicitItems, final CrossReference crossReference) { + EObject context = getObjectForTag(sourceTag); + if (crossReference == null) { + throw new IllegalArgumentException(NLS.bind("Cross reference on object ''{0}'' could not be resolved.", context.toString())); //$NON-NLS-1$ + } + // We only handle references in assignments + Assignment assignment = EcoreUtil2.getContainerOfType(crossReference, Assignment.class); + EObject sourceObject; + String featureName = assignment.getFeature(); + EReference reference = (EReference) context.eClass().getEStructuralFeature(featureName); + if (reference.isMany()) { + Object featureValue = context.eGet(reference, false); + assertInstanceOf(EObjectResolvingEList.class, featureValue, "List must be of type EObjectResolvingEList"); //$NON-NLS-1$ + @SuppressWarnings("unchecked") + EList objects = (EObjectResolvingEList) context.eGet(reference, false); + if (objects.size() == 1) { + sourceObject = EcoreUtil.resolve(objects.get(0), context); + } else { + // TODO DSL-166: Handle this case when needed for tests. + throw new AssertionError("Multiple references not supported yet"); //$NON-NLS-1$ + } + } else { + sourceObject = (EObject) context.eGet(reference, true); + } + assertNotNull(sourceObject, "Bad test. Referenced object is null."); //$NON-NLS-1$ + return sourceObject; + } +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractTest.java index 34eba33761..2adc7cb3b5 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractTest.java @@ -1,440 +1,440 @@ -/******************************************************************************* - * 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 java.lang.reflect.InvocationTargetException; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.common.util.WrappedException; -import org.eclipse.ui.IEditorPart; -import org.eclipse.ui.actions.WorkspaceModifyOperation; -import org.eclipse.xtext.testing.extensions.InjectionExtension; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.avaloq.tools.ddk.test.core.jupiter.BugTestAwareRule; -import com.avaloq.tools.ddk.test.core.jupiter.IssueAwareRule; -import com.avaloq.tools.ddk.test.core.jupiter.LoggingRule; -import com.avaloq.tools.ddk.test.core.mock.ExtensionRegistryMock; -import com.avaloq.tools.ddk.test.core.mock.ServiceMock; -import com.avaloq.tools.ddk.xtext.test.AbstractTestUtil; -import com.avaloq.tools.ddk.xtext.test.ITestProjectManager; -import com.avaloq.tools.ddk.xtext.test.TestInformation; -import com.avaloq.tools.ddk.xtext.test.TestSource; -import com.avaloq.tools.ddk.xtext.test.XtextTestSource; -import com.google.common.collect.ImmutableList; - - -/** - * Provides a test class specific custom test framework for tests that run in the ACF environment. - * All exceptions are wrapped and handed over to the JUnit framework. - */ -@ExtendWith(InjectionExtension.class) -@SuppressWarnings("nls") -@TestInstance(Lifecycle.PER_CLASS) -public abstract class AbstractTest { - - /** - * Prefix for customer sources. - * Consider also: com.avaloq.tools.asmd.testbase.TestUtil.CUSTR_PREFIX - * The duplicated definition of the prefix must be harmonized based on harmonization of test plugins. - */ - protected static final String CUSTOMER_SOURCE_PREFIX = "custr_"; - - protected static final String PROJECT_NAME = "SDK"; - - private static final String HIGH_LATENCY_PROPERTY = "com.avaloq.tools.hl.supported"; - - private static final String LANGUAGE_VERSION_CACHING_PROPERTY = "com.avaloq.tools.LanguageConfigCaching"; - - private static Map, TestInformation> testInformationMap = new HashMap, TestInformation>(); - - @RegisterExtension - // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private - public final LoggingRule watchman = LoggingRule.getInstance(); - // CHECKSTYLE:CHECK-ON Visibility - /** - * Enables support for unresolved bug tests. - */ - @RegisterExtension - // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private - public BugTestAwareRule bugTestRule = BugTestAwareRule.getInstance(); - // CHECKSTYLE:CHECK-ON Visibility - @RegisterExtension - // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private - public final IssueAwareRule issueRule = IssueAwareRule.getInstance(); - // CHECKSTYLE:CHECK-ON Visibility - - protected ITestProjectManager getTestProjectManager() { - return getTestUtil().getTestProjectManager(); - } - - /** - * Returns the URI for the given target source file. - * - * @param fullSourceName - * full source name - * @return URI of source - */ - public URI getTargetSourceUri(final String fullSourceName) { - return getTestProjectManager().createTestSourceUri(fullSourceName); - } - - /** - * Returns a list of all kernel source file names that this test will require. - * This can be overridden to extend or replace the returned list. - * - * @return list of all required kernel source file names - */ - protected List getRequiredSourceFileNames() { - List requiredSources = new LinkedList(); - String testSourceFileName = getTestSourceFileName(); - if (testSourceFileName != null && testSourceFileName.length() > 0) { - requiredSources.add(testSourceFileName); - } - return requiredSources; - } - - /** - * Registers all required sources for this test. - * This method can be overridden to register other required source files. - */ - protected void registerRequiredSources() { - addSourcesToWorkspace(getRequiredSourceFileNames()); - } - - /** - * Non-static instance set up before all tests. - */ - @BeforeAll - public final void setUp() { - synchronized (testInformationMap) { - if (!testInformationMap.containsKey(this.getClass())) { - testInformationMap.put(this.getClass(), new TestInformation()); - beforeAllTests(); - } - } - } - - /** - * Non-static instance tear down after all tests. - */ - @AfterAll - public final void tearDown() { - synchronized (testInformationMap) { - afterAllTests(); - ExtensionRegistryMock.assertUnMocked(); - ServiceMock.assertAllMocksRemoved(); - testInformationMap.remove(this.getClass()); - } - } - - /** - * This method prepares the test environment for a test. It is called by the JUnit framework before each test. - * If it is run the first time, it calls the beforeClass method first. Do not call this method manually! - * All exceptions are wrapped and handed over to the JUnit framework. - */ - @BeforeEach - public final void before() { - beforeEachTest(); - } - - /** - * This method cleans up the test environment after a test. It is called by the JUnit framework after each test. - * If no more tests are to be run, it calls the afterClass method. Do not call this method manually! - * All exceptions are wrapped and handed over to the JUnit framework. - */ - @AfterEach - public final void after() { - afterEachTest(); - } - - /** - * Prepares the test class after the test class has been instantiated. This method can be used to setup the test class before any test is run. - * Do not use JUnit annotation. - * Exceptions are wrapped and handed over to the JUnit framework. - */ - protected void beforeAllTests() { - // check method annotations to ensure test framework policies - System.setProperty(HIGH_LATENCY_PROPERTY, Boolean.FALSE.toString()); - System.setProperty(LANGUAGE_VERSION_CACHING_PROPERTY, Boolean.FALSE.toString()); - getTestProjectManager().setup(ImmutableList. of()); - registerRequiredSources(); - getTestProjectManager().build(); - } - - /** - * After the last task has run but before the test class instance has been garbage collected, this method is called to clean up the test environment. - * Do not use JUnit annotation. - * All exceptions are wrapped and handed over to the JUnit framework. - */ - protected void afterAllTests() { - getTestProjectManager().teardown(); - System.clearProperty(LANGUAGE_VERSION_CACHING_PROPERTY); - System.setProperty(HIGH_LATENCY_PROPERTY, Boolean.TRUE.toString()); - } - - /** - * Prepares for the next test. This method can be used to (re-)initialize the test environment before a (next) test is run. Resource allocations must be dealt - * with in {@link afterEachTest}. - * Do not use JUnit annotation. - * All exceptions are wrapped and handed over to the JUnit framework. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void beforeEachTest() { - // empty - } - - /** - * Called after each test to clean up initializations done in {@link beforeEachTest}. - * Do not use JUnit annotation. - * All exceptions are wrapped and handed over to the JUnit framework. - */ - @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") - protected void afterEachTest() { - // empty - } - - /** - * Registers a source that is required by the test class. - * The source will be removed from the system when {@link afterAllTests} is called. - * All exceptions are wrapped and handed over to the JUnit framework. - * - * @param sourceFileName - * the name of the file where the source is located, and where the content of the source shall be written to. This is the source name available - * during the test. - */ - protected void addSourceToWorkspace(final String sourceFileName) { - addSourceToWorkspace(sourceFileName, getResourceContent(sourceFileName)); - } - - /** - * Registers a kernel source that is required by the test class. - * The source will be removed from the system when {@link afterAllTests} is called. - * All exceptions are wrapped and handed over to the JUnit framework. - * - * @param sourceFileName - * the name of the file where the content of the source shall be written to. This is the source name available - * during the test. - * @param sourceContent - * the content of the source that shall be written to the file in workspace. - */ - protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - addSourceToWorkspace(sourceFileName, sourceContent.toString()); - } - - /** - * Registers a customer source that is required by the test class. - * The source will be removed from the system when {@link afterAllTests} is called. - * All exceptions are wrapped and handed over to the JUnit framework. - * - * @param sourceFileName - * the name of the file where the content of the source shall be written to. This is the source name available - * during the test. - * @param sourceContent - * the content of the source that shall be written to the file in workspace. - */ - protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - addSourceToWorkspace(CUSTOMER_SOURCE_PREFIX.concat(sourceFileName), sourceContent.toString()); - } - - /** - * Registers a source that is required by the test class. - * The source will be removed from the system when {@link afterAllTests} is called. - * All exceptions are wrapped and handed over to the JUnit framework. - * - * @param sourceFileName - * the name of the file where the content of the source shall be written to. This is the source name available - * during the test. - * @param sourceContent - * the content of the source that shall be written to the file in workspace. - */ - private void addSourceToWorkspace(final String sourceFileName, final String sourceContent) { - createTestSource(sourceFileName, sourceContent); - } - - /** - * Returns the string contents of the loaded resource with the given name. - * - * @param sourceFileName - * the file name - * @return the string contents of the loaded resource - */ - protected String getResourceContent(final String sourceFileName) { - return TestSource.getResourceContent(this.getClass(), sourceFileName); - } - - /** - * Registers a set of sources that is required by the test class. - * The sources will be removed from the system when {@link afterAllTests} is called. - * All exceptions are wrapped and handed over to the JUnit framework. - * - * @param sourceFileNames - * the names of the files where the sources are located, and where the content of the sources shall be written to. - */ - private void addSourcesToWorkspace(final List sourceFileNames) { - try { - new WorkspaceModifyOperation() { - @Override - protected void execute(final IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException { - for (String sourceFileName : sourceFileNames) { - addSourceToWorkspace(sourceFileName); - } - } - }.run(new NullProgressMonitor()); - } catch (InvocationTargetException e) { - throw new WrappedException("failed adding sources to workspace", e); - } catch (InterruptedException e) { - throw new WrappedException("adding sources to workspace interrupted", e); - } - } - - protected Collection getTestSources() { - return getTestProjectManager().getTestSources(); - } - - /** - * Returns the kernel {@link TestSource} for the given sourceFileName. - * - * @param sourceFileName - * the file name of the {@link TestSource} - * @return the {@link TestSource} for the given sourceFileName - */ - protected XtextTestSource getTestSource(final String sourceFileName) { - return (XtextTestSource) getTestProjectManager().getTestSource(sourceFileName); - } - - /** - * Returns the kernel {@link TestSource} for this test class. - * - * @return the {@link TestSource} for this test class - */ - protected TestSource getTestSource() { - return getTestProjectManager().getTestSource(getTestSourceFileName()); - } - - /** - * Get the name of the main test source file. - * - * @return the file name of the main test source file - */ - protected abstract String getTestSourceFileName(); - - /** - * The default implementation returns the name of the test class for the model name of the test source. - * A test class needs to override this, if the name of the main test source model differs from the default. - * - * @return the name of the main test source model - */ - protected String getTestSourceModelName() { - return this.getClass().getSimpleName(); - } - - /** - * Wait for validation jobs to finish. - */ - protected void waitForValidation() { - waitForJobsOfFamily(org.eclipse.xtext.ui.editor.validation.ValidationJob.XTEXT_VALIDATION_FAMILY); - } - - /** - * Wait for jobs of a given family to finish. - * - * @param family - * to wait for. - */ - protected void waitForJobsOfFamily(final Object family) { - getTestUtil().waitForJobsOfFamily(family); - } - - /** - * Wait for synchronization jobs on opening/closing the editor. - * - * @param editor - * editor part - */ - protected void waitForEditorJobs(final IEditorPart editor) { - getTestUtil().waitForEditorJobs(editor); - } - - /** - * Wait for jobs of a given family to appear. A {@code null} family will - * cause this to wait for any job. - * - * @param family - * to wait for, may be {@code null} - * @param timeout - * ms to wait for. - */ - protected void waitForJobOfFamilyToAppear(final Object family, final long timeout) { - final long timeLimit = System.currentTimeMillis() + timeout; - do { - if (Job.getJobManager().find(family).length > 0) { - return; - } - } while (System.currentTimeMillis() < timeLimit); - } - - /** - * Returns the test information for the current test class. - * - * @return information for the current test class - */ - protected TestInformation getTestInformation() { - synchronized (testInformationMap) { - return testInformationMap.get(this.getClass()); - } - } - - /** - * Create a test source for testing from an existing file. - * - * @param sourceFileName - * file name for source - * @param content - * content of source - * @return a new {@link TestSource} with the given parameters - */ - - protected TestSource createTestSource(final String sourceFileName, final String content) { - TestSource testSource = new TestSource(sourceFileName, content); - getTestProjectManager().addSourceToProject(testSource); - return testSource; - } - - /** - * Get the test class utility for this test. The minimum functionality is given by - * AbstractTestUtil, which does not require that any methods be overridden. Tests - * that require more than this minimal functionality must override this method. - *

- * This method is expected to always return the same instance, even when invoked on different instances of the test class. This is because the associated - * {@link ITestProjectManager} is stateful and required by {@link #beforeAllTests()}, {@link #afterAllTests()}, and {@link #getTestSources()}. - * - * @return the test class utility for this test. - */ - protected abstract AbstractTestUtil getTestUtil(); - -} +/******************************************************************************* + * 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 java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.common.util.WrappedException; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.actions.WorkspaceModifyOperation; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.avaloq.tools.ddk.test.core.jupiter.BugTestAwareRule; +import com.avaloq.tools.ddk.test.core.jupiter.IssueAwareRule; +import com.avaloq.tools.ddk.test.core.jupiter.LoggingRule; +import com.avaloq.tools.ddk.test.core.mock.ExtensionRegistryMock; +import com.avaloq.tools.ddk.test.core.mock.ServiceMock; +import com.avaloq.tools.ddk.xtext.test.AbstractTestUtil; +import com.avaloq.tools.ddk.xtext.test.ITestProjectManager; +import com.avaloq.tools.ddk.xtext.test.TestInformation; +import com.avaloq.tools.ddk.xtext.test.TestSource; +import com.avaloq.tools.ddk.xtext.test.XtextTestSource; +import com.google.common.collect.ImmutableList; + + +/** + * Provides a test class specific custom test framework for tests that run in the ACF environment. + * All exceptions are wrapped and handed over to the JUnit framework. + */ +@ExtendWith(InjectionExtension.class) +@SuppressWarnings("nls") +@TestInstance(Lifecycle.PER_CLASS) +public abstract class AbstractTest { + + /** + * Prefix for customer sources. + * Consider also: com.avaloq.tools.asmd.testbase.TestUtil.CUSTR_PREFIX + * The duplicated definition of the prefix must be harmonized based on harmonization of test plugins. + */ + protected static final String CUSTOMER_SOURCE_PREFIX = "custr_"; + + protected static final String PROJECT_NAME = "SDK"; + + private static final String HIGH_LATENCY_PROPERTY = "com.avaloq.tools.hl.supported"; + + private static final String LANGUAGE_VERSION_CACHING_PROPERTY = "com.avaloq.tools.LanguageConfigCaching"; + + private static Map, TestInformation> testInformationMap = new HashMap, TestInformation>(); + + @RegisterExtension + // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private + public final LoggingRule watchman = LoggingRule.getInstance(); + // CHECKSTYLE:CHECK-ON Visibility + /** + * Enables support for unresolved bug tests. + */ + @RegisterExtension + // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private + public BugTestAwareRule bugTestRule = BugTestAwareRule.getInstance(); + // CHECKSTYLE:CHECK-ON Visibility + @RegisterExtension + // CHECKSTYLE:CHECK-OFF Visibility MethodRules cannot be private + public final IssueAwareRule issueRule = IssueAwareRule.getInstance(); + // CHECKSTYLE:CHECK-ON Visibility + + protected ITestProjectManager getTestProjectManager() { + return getTestUtil().getTestProjectManager(); + } + + /** + * Returns the URI for the given target source file. + * + * @param fullSourceName + * full source name + * @return URI of source + */ + public URI getTargetSourceUri(final String fullSourceName) { + return getTestProjectManager().createTestSourceUri(fullSourceName); + } + + /** + * Returns a list of all kernel source file names that this test will require. + * This can be overridden to extend or replace the returned list. + * + * @return list of all required kernel source file names + */ + protected List getRequiredSourceFileNames() { + List requiredSources = new LinkedList(); + String testSourceFileName = getTestSourceFileName(); + if (testSourceFileName != null && testSourceFileName.length() > 0) { + requiredSources.add(testSourceFileName); + } + return requiredSources; + } + + /** + * Registers all required sources for this test. + * This method can be overridden to register other required source files. + */ + protected void registerRequiredSources() { + addSourcesToWorkspace(getRequiredSourceFileNames()); + } + + /** + * Non-static instance set up before all tests. + */ + @BeforeAll + public final void setUp() { + synchronized (testInformationMap) { + if (!testInformationMap.containsKey(this.getClass())) { + testInformationMap.put(this.getClass(), new TestInformation()); + beforeAllTests(); + } + } + } + + /** + * Non-static instance tear down after all tests. + */ + @AfterAll + public final void tearDown() { + synchronized (testInformationMap) { + afterAllTests(); + ExtensionRegistryMock.assertUnMocked(); + ServiceMock.assertAllMocksRemoved(); + testInformationMap.remove(this.getClass()); + } + } + + /** + * This method prepares the test environment for a test. It is called by the JUnit framework before each test. + * If it is run the first time, it calls the beforeClass method first. Do not call this method manually! + * All exceptions are wrapped and handed over to the JUnit framework. + */ + @BeforeEach + public final void before() { + beforeEachTest(); + } + + /** + * This method cleans up the test environment after a test. It is called by the JUnit framework after each test. + * If no more tests are to be run, it calls the afterClass method. Do not call this method manually! + * All exceptions are wrapped and handed over to the JUnit framework. + */ + @AfterEach + public final void after() { + afterEachTest(); + } + + /** + * Prepares the test class after the test class has been instantiated. This method can be used to setup the test class before any test is run. + * Do not use JUnit annotation. + * Exceptions are wrapped and handed over to the JUnit framework. + */ + protected void beforeAllTests() { + // check method annotations to ensure test framework policies + System.setProperty(HIGH_LATENCY_PROPERTY, Boolean.FALSE.toString()); + System.setProperty(LANGUAGE_VERSION_CACHING_PROPERTY, Boolean.FALSE.toString()); + getTestProjectManager().setup(ImmutableList. of()); + registerRequiredSources(); + getTestProjectManager().build(); + } + + /** + * After the last task has run but before the test class instance has been garbage collected, this method is called to clean up the test environment. + * Do not use JUnit annotation. + * All exceptions are wrapped and handed over to the JUnit framework. + */ + protected void afterAllTests() { + getTestProjectManager().teardown(); + System.clearProperty(LANGUAGE_VERSION_CACHING_PROPERTY); + System.setProperty(HIGH_LATENCY_PROPERTY, Boolean.TRUE.toString()); + } + + /** + * Prepares for the next test. This method can be used to (re-)initialize the test environment before a (next) test is run. Resource allocations must be dealt + * with in {@link afterEachTest}. + * Do not use JUnit annotation. + * All exceptions are wrapped and handed over to the JUnit framework. + */ + @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") + protected void beforeEachTest() { + // empty + } + + /** + * Called after each test to clean up initializations done in {@link beforeEachTest}. + * Do not use JUnit annotation. + * All exceptions are wrapped and handed over to the JUnit framework. + */ + @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") + protected void afterEachTest() { + // empty + } + + /** + * Registers a source that is required by the test class. + * The source will be removed from the system when {@link afterAllTests} is called. + * All exceptions are wrapped and handed over to the JUnit framework. + * + * @param sourceFileName + * the name of the file where the source is located, and where the content of the source shall be written to. This is the source name available + * during the test. + */ + protected void addSourceToWorkspace(final String sourceFileName) { + addSourceToWorkspace(sourceFileName, getResourceContent(sourceFileName)); + } + + /** + * Registers a kernel source that is required by the test class. + * The source will be removed from the system when {@link afterAllTests} is called. + * All exceptions are wrapped and handed over to the JUnit framework. + * + * @param sourceFileName + * the name of the file where the content of the source shall be written to. This is the source name available + * during the test. + * @param sourceContent + * the content of the source that shall be written to the file in workspace. + */ + protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + addSourceToWorkspace(sourceFileName, sourceContent.toString()); + } + + /** + * Registers a customer source that is required by the test class. + * The source will be removed from the system when {@link afterAllTests} is called. + * All exceptions are wrapped and handed over to the JUnit framework. + * + * @param sourceFileName + * the name of the file where the content of the source shall be written to. This is the source name available + * during the test. + * @param sourceContent + * the content of the source that shall be written to the file in workspace. + */ + protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + addSourceToWorkspace(CUSTOMER_SOURCE_PREFIX.concat(sourceFileName), sourceContent.toString()); + } + + /** + * Registers a source that is required by the test class. + * The source will be removed from the system when {@link afterAllTests} is called. + * All exceptions are wrapped and handed over to the JUnit framework. + * + * @param sourceFileName + * the name of the file where the content of the source shall be written to. This is the source name available + * during the test. + * @param sourceContent + * the content of the source that shall be written to the file in workspace. + */ + private void addSourceToWorkspace(final String sourceFileName, final String sourceContent) { + createTestSource(sourceFileName, sourceContent); + } + + /** + * Returns the string contents of the loaded resource with the given name. + * + * @param sourceFileName + * the file name + * @return the string contents of the loaded resource + */ + protected String getResourceContent(final String sourceFileName) { + return TestSource.getResourceContent(this.getClass(), sourceFileName); + } + + /** + * Registers a set of sources that is required by the test class. + * The sources will be removed from the system when {@link afterAllTests} is called. + * All exceptions are wrapped and handed over to the JUnit framework. + * + * @param sourceFileNames + * the names of the files where the sources are located, and where the content of the sources shall be written to. + */ + private void addSourcesToWorkspace(final List sourceFileNames) { + try { + new WorkspaceModifyOperation() { + @Override + protected void execute(final IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException { + for (String sourceFileName : sourceFileNames) { + addSourceToWorkspace(sourceFileName); + } + } + }.run(new NullProgressMonitor()); + } catch (InvocationTargetException e) { + throw new WrappedException("failed adding sources to workspace", e); + } catch (InterruptedException e) { + throw new WrappedException("adding sources to workspace interrupted", e); + } + } + + protected Collection getTestSources() { + return getTestProjectManager().getTestSources(); + } + + /** + * Returns the kernel {@link TestSource} for the given sourceFileName. + * + * @param sourceFileName + * the file name of the {@link TestSource} + * @return the {@link TestSource} for the given sourceFileName + */ + protected XtextTestSource getTestSource(final String sourceFileName) { + return (XtextTestSource) getTestProjectManager().getTestSource(sourceFileName); + } + + /** + * Returns the kernel {@link TestSource} for this test class. + * + * @return the {@link TestSource} for this test class + */ + protected TestSource getTestSource() { + return getTestProjectManager().getTestSource(getTestSourceFileName()); + } + + /** + * Get the name of the main test source file. + * + * @return the file name of the main test source file + */ + protected abstract String getTestSourceFileName(); + + /** + * The default implementation returns the name of the test class for the model name of the test source. + * A test class needs to override this, if the name of the main test source model differs from the default. + * + * @return the name of the main test source model + */ + protected String getTestSourceModelName() { + return this.getClass().getSimpleName(); + } + + /** + * Wait for validation jobs to finish. + */ + protected void waitForValidation() { + waitForJobsOfFamily(org.eclipse.xtext.ui.editor.validation.ValidationJob.XTEXT_VALIDATION_FAMILY); + } + + /** + * Wait for jobs of a given family to finish. + * + * @param family + * to wait for. + */ + protected void waitForJobsOfFamily(final Object family) { + getTestUtil().waitForJobsOfFamily(family); + } + + /** + * Wait for synchronization jobs on opening/closing the editor. + * + * @param editor + * editor part + */ + protected void waitForEditorJobs(final IEditorPart editor) { + getTestUtil().waitForEditorJobs(editor); + } + + /** + * Wait for jobs of a given family to appear. A {@code null} family will + * cause this to wait for any job. + * + * @param family + * to wait for, may be {@code null} + * @param timeout + * ms to wait for. + */ + protected void waitForJobOfFamilyToAppear(final Object family, final long timeout) { + final long timeLimit = System.currentTimeMillis() + timeout; + do { + if (Job.getJobManager().find(family).length > 0) { + return; + } + } while (System.currentTimeMillis() < timeLimit); + } + + /** + * Returns the test information for the current test class. + * + * @return information for the current test class + */ + protected TestInformation getTestInformation() { + synchronized (testInformationMap) { + return testInformationMap.get(this.getClass()); + } + } + + /** + * Create a test source for testing from an existing file. + * + * @param sourceFileName + * file name for source + * @param content + * content of source + * @return a new {@link TestSource} with the given parameters + */ + + protected TestSource createTestSource(final String sourceFileName, final String content) { + TestSource testSource = new TestSource(sourceFileName, content); + getTestProjectManager().addSourceToProject(testSource); + return testSource; + } + + /** + * Get the test class utility for this test. The minimum functionality is given by + * AbstractTestUtil, which does not require that any methods be overridden. Tests + * that require more than this minimal functionality must override this method. + *

+ * This method is expected to always return the same instance, even when invoked on different instances of the test class. This is because the associated + * {@link ITestProjectManager} is stateful and required by {@link #beforeAllTests()}, {@link #afterAllTests()}, and {@link #getTestSources()}. + * + * @return the test class utility for this test. + */ + protected abstract AbstractTestUtil getTestUtil(); + +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractUtilTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractUtilTest.java index 3ee301be93..a435e3c40d 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractUtilTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractUtilTest.java @@ -1,75 +1,75 @@ -/******************************************************************************* - * 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 java.util.Collections.singleton; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IStorage; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.Path; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.IResourceDescription.Delta; -import org.eclipse.xtext.ui.resource.IStorage2UriMapper; -import org.eclipse.xtext.ui.resource.Storage2UriMapperImpl; -import org.eclipse.xtext.util.Pair; -import org.eclipse.xtext.util.Tuples; - - -/** - * A base class for util test classes, which prepares common required mocks. - */ -@SuppressWarnings("nls") -public abstract class AbstractUtilTest extends AbstractXtextTest { - - public static final String TEST_PROJECT_NAME = "TestProjectName"; - private static final String DUMMY_PATH = TEST_PROJECT_NAME + "/TEST/"; - - // CHECKSTYLE:CHECK-OFF VisibilityModifierCheck - protected static Delta delta; - protected static IResourceDescription oldDesc; - protected static IResourceDescription newDesc; - protected static URI uriCorrect; - protected static Resource resource; - protected static IStorage2UriMapper mapperCorrect; - protected static IFile file; - - // CHECKSTYLE:CHECK-ON VisibilityModifierCheck - - /** - * Prepare mocks for all tests. - */ - public static void prepareMocksBase() { - oldDesc = mock(IResourceDescription.class); - newDesc = mock(IResourceDescription.class); - delta = mock(Delta.class); - resource = mock(Resource.class); - uriCorrect = mock(URI.class); - when(uriCorrect.isPlatformResource()).thenReturn(true); - when(uriCorrect.isFile()).thenReturn(true); - when(uriCorrect.toFileString()).thenReturn(DUMMY_PATH); - when(uriCorrect.toPlatformString(true)).thenReturn(DUMMY_PATH); - when(delta.getNew()).thenReturn(newDesc); - when(delta.getOld()).thenReturn(oldDesc); - when(delta.getUri()).thenReturn(uriCorrect); - when(resource.getURI()).thenReturn(uriCorrect); - file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(uriCorrect.toPlatformString(true))); - Iterable> storages = singleton(Tuples. create(file, file.getProject())); - mapperCorrect = mock(Storage2UriMapperImpl.class); - when(mapperCorrect.getStorages(uriCorrect)).thenReturn(storages); - } - -} +/******************************************************************************* + * 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 java.util.Collections.singleton; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Path; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceDescription.Delta; +import org.eclipse.xtext.ui.resource.IStorage2UriMapper; +import org.eclipse.xtext.ui.resource.Storage2UriMapperImpl; +import org.eclipse.xtext.util.Pair; +import org.eclipse.xtext.util.Tuples; + + +/** + * A base class for util test classes, which prepares common required mocks. + */ +@SuppressWarnings("nls") +public abstract class AbstractUtilTest extends AbstractXtextTest { + + public static final String TEST_PROJECT_NAME = "TestProjectName"; + private static final String DUMMY_PATH = TEST_PROJECT_NAME + "/TEST/"; + + // CHECKSTYLE:CHECK-OFF VisibilityModifierCheck + protected static Delta delta; + protected static IResourceDescription oldDesc; + protected static IResourceDescription newDesc; + protected static URI uriCorrect; + protected static Resource resource; + protected static IStorage2UriMapper mapperCorrect; + protected static IFile file; + + // CHECKSTYLE:CHECK-ON VisibilityModifierCheck + + /** + * Prepare mocks for all tests. + */ + public static void prepareMocksBase() { + oldDesc = mock(IResourceDescription.class); + newDesc = mock(IResourceDescription.class); + delta = mock(Delta.class); + resource = mock(Resource.class); + uriCorrect = mock(URI.class); + when(uriCorrect.isPlatformResource()).thenReturn(true); + when(uriCorrect.isFile()).thenReturn(true); + when(uriCorrect.toFileString()).thenReturn(DUMMY_PATH); + when(uriCorrect.toPlatformString(true)).thenReturn(DUMMY_PATH); + when(delta.getNew()).thenReturn(newDesc); + when(delta.getOld()).thenReturn(oldDesc); + when(delta.getUri()).thenReturn(uriCorrect); + when(resource.getURI()).thenReturn(uriCorrect); + file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(uriCorrect.toPlatformString(true))); + Iterable> storages = singleton(Tuples. create(file, file.getProject())); + mapperCorrect = mock(Storage2UriMapperImpl.class); + when(mapperCorrect.getStorages(uriCorrect)).thenReturn(storages); + } + +} diff --git a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractValidationTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractValidationTest.java index c248425e23..b8e426ebc9 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractValidationTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractValidationTest.java @@ -1,1218 +1,1218 @@ -/******************************************************************************* - * 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.eclipse.xtext.validation.ValidationMessageAcceptor.INSIGNIFICANT_INDEX; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.eclipse.emf.common.util.BasicDiagnostic; -import org.eclipse.emf.common.util.Diagnostic; -import org.eclipse.emf.common.util.EList; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.osgi.util.NLS; -import org.eclipse.xtext.EcoreUtil2; -import org.eclipse.xtext.diagnostics.AbstractDiagnostic; -import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; -import org.eclipse.xtext.nodemodel.ICompositeNode; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.resource.XtextResource; -import org.eclipse.xtext.resource.XtextSyntaxDiagnostic; -import org.eclipse.xtext.util.CancelIndicator; -import org.eclipse.xtext.validation.AbstractValidationDiagnostic; -import org.eclipse.xtext.validation.FeatureBasedDiagnostic; -import org.eclipse.xtext.validation.RangeBasedDiagnostic; -import org.eclipse.xtext.xbase.lib.Pair; - -import com.avaloq.tools.ddk.xtext.test.XtextTestSource; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.google.inject.Provider; - - -/** - * Base class for validation tests. - */ -@SuppressWarnings({"nls", "PMD.ExcessiveClassLength"}) -// CHECKSTYLE:CHECK-OFF MultipleStringLiterals -// CHECKSTYLE:OFF MagicNumber -public abstract class AbstractValidationTest extends AbstractXtextMarkerBasedTest { - - public static final int NO_ERRORS = 0; - - static final String NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE = "Expecting no errors on resource"; - - private static final int SEVERITY_UNDEFINED = -1; - private static final Map CODE_TO_NAME = ImmutableMap.of(Diagnostic.INFO, "INFO", Diagnostic.WARNING, "WARNING", Diagnostic.ERROR, "ERROR"); - - private static final String LINE_BREAK = "\n"; - private static final String DOT_AND_LINEBREAK = "'." + LINE_BREAK; - - /** - * All diagnostics of the current testing file. - */ - private Diagnostic fileDiagnostics; - - /** - * During validation of a source we monitor diagnostics, that are found in the source but were not expected by the test. - * If the validation test is strict, then we will display these unexpected diagnostics as test error. - */ - private final Set unexpectedDiagnostics = Sets.newLinkedHashSet(); - private final Set unexpectedResourceDiagnostics = Sets.newLinkedHashSet(); - - /** - * validation results calculated during test setUp. - * - * @return the diagnostic for the primary test source file - */ - private Diagnostic getPrimaryDiagnostics() { - Object obj = getTestInformation().getTestObject(Diagnostic.class); - assertNotNull(obj, "getPrimaryDiagnostics(): Diagnostics of primary source not null."); - return (Diagnostic) obj; - } - - /** - * Returns the unexpectedDiagnostics. - * - * @return the unexpectedDiagnostics - */ - protected Set getUnexpectedDiagnostics() { - return unexpectedDiagnostics; - } - - /** - * Returns the unexpectedDiagnostics. - * - * @return the unexpectedDiagnostics - */ - protected Set getUnexpectedResourceDiagnostics() { - return unexpectedResourceDiagnostics; - } - - /** - * Assertion testing for {@link AbstractValidationDiagnostic validation issues} at a given source position. - */ - protected class XtextDiagnosticAssertion extends AbstractModelAssertion { - - /** Issue code of the diagnostic. */ - private final String issueCode; - /** Issue message of the diagnostic. */ - private final String message; - /** - * Indicates whether the assertion must find the issue. - * Assertion creates an error if the existence of issue code for the target eobject doesn't correspond to the value of issueMustBeFound. - */ - private final boolean issueMustBeFound; - - private final int expectedSeverity; - - protected XtextDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound) { - this(issueCode, issueMustBeFound, SEVERITY_UNDEFINED, null); - } - - protected XtextDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound, final int severity, final String message) { - this.issueCode = issueCode; - this.issueMustBeFound = issueMustBeFound; - this.expectedSeverity = severity; - this.message = message; - } - - /** - * Check if the given issue code is found among issue codes for the object, located at the given position. - * - * @param root - * root object of the document - * @param pos - * position to locate the target object - */ - @Override - public void apply(final EObject root, final Integer pos) { - final Diagnostic diagnostics = validate(root); - final BasicDiagnostic diagnosticsOnTargetPosition = new BasicDiagnostic(); - boolean issueFound = false; - int actualSeverity = SEVERITY_UNDEFINED; - boolean expectedSeverityMatches = false; - boolean expectedMessageMatches = false; - String actualMessage = ""; - - for (AbstractValidationDiagnostic avd : Iterables.filter(diagnostics.getChildren(), AbstractValidationDiagnostic.class)) { - if (diagnosticPositionEquals(pos, avd)) { - // Add issue to the list of issues at the given position - diagnosticsOnTargetPosition.add(avd); - if (avd.getIssueCode().equals(issueCode)) { - issueFound = true; - actualSeverity = avd.getSeverity(); - // True if the expected severity is not set, or if matches with the actual one - expectedSeverityMatches = expectedSeverity == SEVERITY_UNDEFINED || expectedSeverity == actualSeverity; - actualMessage = avd.getMessage(); - // True if message matches with actual message or message is null - expectedMessageMatches = message == null || actualMessage.equals(message); - if (issueMustBeFound) { - // Remove the diagnostic from the list of non-expected diagnostics - getUnexpectedDiagnostics().remove(avd); - // Don't need to display error messages - if (expectedSeverityMatches && expectedMessageMatches) { - return; - } - } - } - } - } - - // Create error message - createErrorMessage(pos, diagnosticsOnTargetPosition, issueFound, expectedSeverityMatches, actualSeverity, expectedMessageMatches, actualMessage); - } - - /** - * Create an error message (if needed) based on the given input parameters. - * - * @param pos - * position in the source to associate the message with - * @param diagnosticsOnTargetPosition - * diagnostics on the specifies position - * @param issueFound - * specifies whether an issue has been found at the given position - * @param expectedSeverityMatches - * true if expected severity equals actual one, false otherwise - * @param actualSeverity - * actual severity - * @param expectedMessageMatches - * expected message matches - * @param actualMessage - * actual message - */ - private void createErrorMessage(final Integer pos, final BasicDiagnostic diagnosticsOnTargetPosition, final boolean issueFound, final boolean expectedSeverityMatches, final int actualSeverity, final boolean expectedMessageMatches, final String actualMessage) { - StringBuilder errorMessage = new StringBuilder(180); - if (issueMustBeFound && !issueFound) { - errorMessage.append("Expected issue not found. Code '").append(issueCode).append('\n'); - } else if (!issueMustBeFound && issueFound) { - errorMessage.append("There should be no issue with the code '").append(issueCode).append(DOT_AND_LINEBREAK); - } - if (issueFound && !expectedMessageMatches) { - errorMessage.append("Expected message does not match. Expected: '").append(message).append("', Actual: '").append(actualMessage).append('\n'); - } - // If the expected issue has been found, but the actual severity does not match with expected one - if (issueMustBeFound && issueFound && !expectedSeverityMatches) { - errorMessage.append("Severity does not match. Expected: ").append(CODE_TO_NAME.get(expectedSeverity)).append(". Actual: ").append(CODE_TO_NAME.get(actualSeverity)).append(".\n"); - } - // Memorize error message - if (errorMessage.length() > 0) { - if (!diagnosticsOnTargetPosition.getChildren().isEmpty()) { - errorMessage.append(" All issues at this position:\n"); - errorMessage.append(diagnosticsToString(diagnosticsOnTargetPosition, false)); - } - memorizeErrorOnPosition(pos, errorMessage.toString()); - } - } - - /** - * Compare if the position of the given diagnostic equals to the given position in text. - * - * @param pos - * position in text - * @param avd - * diagnostic that we check, if it has the same position as the given position in text - * @return - * TRUE if diagnostic has the same position as the given one, FALSE otherwise. - */ - protected boolean diagnosticPositionEquals(final Integer pos, final AbstractValidationDiagnostic avd) { - if (avd instanceof FeatureBasedDiagnostic && ((FeatureBasedDiagnostic) avd).getFeature() != null) { - List nodes = NodeModelUtils.findNodesForFeature(avd.getSourceEObject(), ((FeatureBasedDiagnostic) avd).getFeature()); - if (nodes.isEmpty()) { - INode node = NodeModelUtils.getNode(avd.getSourceEObject()); - INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(node); - if (firstNonHiddenLeafNode == null) { - return issueMustBeFound; - } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { - return true; - } - } else { - int avdIndex = ((FeatureBasedDiagnostic) avd).getIndex(); - for (int i = 0; i < nodes.size(); i++) { - if (avdIndex == INSIGNIFICANT_INDEX || avdIndex == i) { - INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(nodes.get(i)); - if (firstNonHiddenLeafNode == null) { - return issueMustBeFound; - } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { - return true; - } - } - } - } - } else if (avd instanceof RangeBasedDiagnostic) { - if (((RangeBasedDiagnostic) avd).getOffset() == pos) { - return true; - } - } else { - INode node = NodeModelUtils.getNode(avd.getSourceEObject()); - INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(node); - if (firstNonHiddenLeafNode == null) { - return issueMustBeFound; - } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { - return true; - } - } - return false; - } - } - - /** - * Assertion testing for {@link AbstractValidationDiagnostic validation issues} at a given source position. - */ - private class ResourceDiagnosticAssertion extends AbstractModelAssertion { - - /** Issue code of the diagnostic. */ - private final String issueCode; - /** Issue message of the diagnostic. */ - private final String message; - /** - * Indicates whether the assertion must find the issue. - * Assertion creates an error if the existence of issue code for the target eobject doesn't correspond to the value of issueMustBeFound. - */ - private final boolean issueMustBeFound; - - private final int expectedSeverity; - - protected ResourceDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound, final int severity, final String message) { - this.issueCode = issueCode; - this.issueMustBeFound = issueMustBeFound; - this.expectedSeverity = severity; - this.message = message; - } - - /** - * Check if the given issue code is found among issue codes for the object, located at the given position. - * - * @param root - * root object of the document - * @param pos - * position to locate the target object - */ - @Override - public void apply(final EObject root, final Integer pos) { - Iterable diagnostics = null; - switch (expectedSeverity) { - case Diagnostic.ERROR: - diagnostics = root.eResource().getErrors(); - break; - case Diagnostic.WARNING: - diagnostics = root.eResource().getWarnings(); - break; - case SEVERITY_UNDEFINED: - diagnostics = Iterables.concat(root.eResource().getErrors(), root.eResource().getWarnings()); - break; - } - final List diagnosticsOnTargetPosition = Lists.newArrayList(); - boolean issueFound = false; - int actualSeverity = expectedSeverity; - boolean expectedMessageMatches = false; - String actualMessage = ""; - - for (AbstractDiagnostic diag : Iterables.filter(diagnostics, AbstractDiagnostic.class)) { - if (diagnosticPositionEquals(pos, diag)) { - // Add issue to the list of issues at the given position - diagnosticsOnTargetPosition.add(diag); - if (diag.getCode() != null && diag.getCode().equals(issueCode)) { - issueFound = true; - if (expectedSeverity == SEVERITY_UNDEFINED) { - actualSeverity = root.eResource().getErrors().contains(diag) ? Diagnostic.ERROR : Diagnostic.WARNING; - } - actualMessage = diag.getMessage(); - // True if message matches with actual message or message is null - expectedMessageMatches = message == null || actualMessage.equals(message); - // Don't need to display error messages - if (issueMustBeFound) { - // Remove the diagnostic from the list of non-expected diagnostics - getUnexpectedResourceDiagnostics().remove(diag); - // Don't need to display error messages - if (expectedMessageMatches) { - return; - } - } - } - } - } - - // Create error message - createErrorMessage(pos, diagnosticsOnTargetPosition, issueFound, true, actualSeverity, expectedMessageMatches, actualMessage); - } - - /** - * Create an error message (if needed) based on the given input parameters. - * - * @param pos - * position in the source to associate the message with - * @param diagnosticsOnTargetPosition - * diagnostics on the specifies position - * @param issueFound - * specifies whether an issue has been found at the given position - * @param expectedSeverityMatches - * true if expected severity equals actual one, false otherwise - * @param actualSeverity - * actual severity - * @param expectedMessageMatches - * expected message matches - * @param actualMessage - * actual message - */ - private void createErrorMessage(final Integer pos, final List diagnosticsOnTargetPosition, final boolean issueFound, final boolean expectedSeverityMatches, final int actualSeverity, final boolean expectedMessageMatches, final String actualMessage) { - StringBuilder errorMessage = new StringBuilder(175); - if (issueMustBeFound && !issueFound) { - errorMessage.append("Expected issue not found. Code '").append(issueCode).append('\n'); - } else if (!issueMustBeFound && issueFound) { - errorMessage.append("There should be no issue with the code '").append(issueCode).append(DOT_AND_LINEBREAK); - } - if (issueFound && !expectedMessageMatches) { - errorMessage.append("Expected message does not match. Expected: '").append(message).append("', Actual: '").append(actualMessage).append('\n'); - } - // If the expected issue has been found, but the actual severity does not match with expected one - if (issueMustBeFound && issueFound && !expectedSeverityMatches) { - errorMessage.append("Severity does not match. Expected: ").append(CODE_TO_NAME.get(expectedSeverity)).append(". Actual: ").append(CODE_TO_NAME.get(actualSeverity)).append(".\n"); - } - // Memorize error message - if (errorMessage.length() > 0) { - if (!diagnosticsOnTargetPosition.isEmpty()) { - errorMessage.append(" All issues at this position:\n"); - errorMessage.append(diagnosticsToString(diagnosticsOnTargetPosition, false)); - } - memorizeErrorOnPosition(pos, errorMessage.toString()); - } - } - - /** - * Compare if the position of the given diagnostic equals to the given position in text. - * - * @param pos - * position in text - * @param diagnostic - * diagnostic that we check, if it has the same position as the given position in text - * @return - * {@code true} if diagnostic has the same position as the given one, {@code false} otherwise. - */ - private boolean diagnosticPositionEquals(final Integer pos, final AbstractDiagnostic diagnostic) { - return diagnostic.getOffset() == pos; - } - } - - /** - * Get a cached version of an object associated with the root object for a given key. - * - * @param - * type of the associated object - * @param root - * root EObject - * @param key - * key identifying the type of the associated object - * @param provider - * provider to deliver an object if there is no cached version - * @return - * cached version of the associated object - */ - protected T getCached(final EObject root, final String key, final Provider provider) { - XtextResource res = (XtextResource) root.eResource(); - return res.getCache().get(key, res, provider); - } - - /** - * Validate the model. - * - * @param root - * root EObject to validate - * @return - * validation results - */ - protected Diagnostic validate(final EObject root) { - return getCached(root, "DIAGNOSTIC", () -> getXtextTestUtil().getDiagnostician().validate(root)); - } - - /** - * Display the path from root object to the target EObject. - * - * @param eObject - * object to display the object path for - * @param offset - * string offset that is added in the beginning of each line - * @return - * object hierarchy as string (each object on a single line) - */ - private String pathFromRootAsString(final EObject eObject, final String offset) { - List hierarchy = Lists.newLinkedList(); - - EObject currentObject = eObject; - while (currentObject != null) { - hierarchy.add(0, offset + currentObject.toString()); - currentObject = currentObject.eContainer(); - } - - return String.join("\n", hierarchy); - } - - /** - * Persist list diagnostics into string to display the list of issue codes. - * - * @param diagnostics - * list of diagnostics - * @param displayPathToTargetObject - * if true, the path through the object hierarchy is printed out up to the root node - * @return - * string with list of issue codes, separated with a line break - */ - // TODO (ACF-4153) generalize for all kinds of errors and move to AbstractXtextTest - private String diagnosticsToString(final Diagnostic diagnostics, final boolean displayPathToTargetObject) { - StringBuilder sb = new StringBuilder(); - for (Diagnostic diagnostic : diagnostics.getChildren()) { - if (diagnostic instanceof AbstractValidationDiagnostic) { - AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; - sb.append(" "); - sb.append(avd.getIssueCode()); - if (displayPathToTargetObject) { - sb.append(" at line: "); - ICompositeNode compositeNode = NodeModelUtils.findActualNodeFor(avd.getSourceEObject()); - if (compositeNode != null) { - sb.append(compositeNode.getStartLine()); - } else { - sb.append("Unknown"); - } - sb.append(" on \n"); - sb.append(pathFromRootAsString(avd.getSourceEObject(), " ")); - } - sb.append(LINE_BREAK); - } - } - return sb.toString(); - } - - /** - * Persist list diagnostics into string to display the list of issue codes. - * - * @param diagnostics - * list of diagnostics - * @param displayPathToTargetObject - * if true, the path through the object hierarchy is printed out up to the root node - * @return - * string with list of issue codes, separated with a line break - */ - // TODO (ACF-4153) generalize for all kinds of errors and move to AbstractXtextTest - private String diagnosticsToString(final List diagnostics, final boolean displayPathToTargetObject) { - StringBuilder sb = new StringBuilder(25); - for (Resource.Diagnostic diagnostic : diagnostics) { - if (diagnostic instanceof AbstractDiagnostic) { - AbstractDiagnostic diag = (AbstractDiagnostic) diagnostic; - sb.append(" "); - sb.append(diag.getCode()); - if (displayPathToTargetObject) { - sb.append(" at line: "); - sb.append(diag.getLine()); - sb.append(" on \n"); - sb.append(" "); - sb.append(diag.getUriToProblem()); - } - sb.append(LINE_BREAK); - } - } - return sb.toString(); - } - - @Override - protected void beforeAllTests() { - super.beforeAllTests(); - if (getTestSource() != null) { - Diagnostic primaryDiagnostics = getXtextTestUtil().getDiagnostician().validate(getSemanticModel()); - getTestInformation().putTestObject(Diagnostic.class, primaryDiagnostics); - } - } - - /** - * Register a new validation marker with the given issue code. Expects an info. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String info(final String issueCode) { - return info(issueCode, null); - } - - /** - * Register a new validation marker with the given issue code and message. Expects an info. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @param message - * the expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String info(final String issueCode, final String message) { - return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.INFO, message)); - } - - /** - * Register a new validation marker with the given issue code. Expects a warning if the condition is {@code true}, no diagnostic otherwise. - * - * @param condition - * the condition when the marker is expected - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String warningIf(final boolean condition, final String issueCode) { - if (condition) { - return warning(issueCode); - } else { - return noDiagnostic(issueCode); - } - } - - /** - * Register a new validation marker with the given issue code. Expects a warning. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String warning(final String issueCode) { - return warning(issueCode, null); - } - - /** - * Register a new validation marker with the given issue code and message. Expects a warning. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @param message - * the expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String warning(final String issueCode, final String message) { - return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.WARNING, message)); - } - - /** - * Register a new validation marker with the given issue code. Expects an error if the condition is {@code true}, no diagnostic otherwise. - * - * @param condition - * the condition when the marker is expected - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String errorIf(final boolean condition, final String issueCode) { - if (condition) { - return error(issueCode); - } else { - return noDiagnostic(issueCode); - } - } - - /** - * Register a new validation marker with the given issue code. Expects an error. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String error(final String issueCode) { - return error(issueCode, null); - } - - /** - * Register a new validation marker with the given issue code and message. Expects an error. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @param message - * the expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String error(final String issueCode, final String message) { - return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.ERROR, message)); - } - - /** - * Register a new validation marker with the given issue code. - * The issue is expected to be found in the test file. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String diagnostic(final String issueCode) { - return diagnostic(issueCode, null); - } - - /** - * Register a new validation marker with the given issue code and message. - * The issue and message are expected to be found in the test file. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @param message - * the expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String diagnostic(final String issueCode, final String message) { - return addAssertion(new XtextDiagnosticAssertion(issueCode, true, SEVERITY_UNDEFINED, message)); - } - - /** - * Register a new linking error validation marker. - * The issue is expected to be found in the test file. - * - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String linkingError() { - return linkingError(null); - } - - /** - * Register a new linking error validation marker with the given message. - * The issue is expected to be found in the test file. - * - * @param message - * issuethe expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String linkingError(final String message) { - return addAssertion(new ResourceDiagnosticAssertion(org.eclipse.xtext.diagnostics.Diagnostic.LINKING_DIAGNOSTIC, true, Diagnostic.ERROR, message)); - } - - /** - * Register a new resource validation marker with the given issue code and message. - * The issue is expected to be found in the test file. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @param message - * the expected issue message - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String resourceDiagnostic(final String issueCode, final String message) { - return addAssertion(new ResourceDiagnosticAssertion(issueCode, true, Diagnostic.ERROR, message)); - } - - /** - * Register a new validation marker with the given issue code. - * The issue is expected NOT to be found in the test file. - * - * @param issueCode - * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) - * @return - * unique marker that can be used in the input string to mark a position that should be validated - */ - protected String noDiagnostic(final String issueCode) { - return addAssertion(new XtextDiagnosticAssertion(issueCode, false)); - } - - @Override - protected void beforeApplyAssertions(final XtextTestSource testSource) { - super.beforeApplyAssertions(testSource); - EObject root = testSource.getModel(); - // Get all diagnostics of the current testing file - EcoreUtil2.resolveLazyCrossReferences(root.eResource(), CancelIndicator.NullImpl); - fileDiagnostics = validate(root); - getUnexpectedDiagnostics().addAll(fileDiagnostics.getChildren()); - getUnexpectedResourceDiagnostics().addAll(root.eResource().getErrors()); - getUnexpectedResourceDiagnostics().addAll(root.eResource().getWarnings()); - } - - @Override - protected String getAdditionalErrorMessageInformation() { - return diagnosticsToString(fileDiagnostics, true); - } - - @Override - protected void afterValidate() { - super.afterValidate(); - // Garbage collection - getUnexpectedDiagnostics().clear(); - getUnexpectedResourceDiagnostics().clear(); - } - - /** - * Assert that diagnosticList contains a diagnostic of the given issueCode. - * - * @param issueCode - * the code of the issue to look for - */ - protected void assertDiagnostic(final String issueCode) { - assertDiagnostic(getPrimaryDiagnostics(), issueCode); - } - - /** - * Assert that the given EObject model contains a diagnostic of the given issueCode. - * - * @param model - * the model in which to look for issues, may be {@code null} - * @param issueCode - * the code of the issue to look for - */ - protected void assertDiagnostic(final EObject model, final String issueCode) { - assertNotNull(model, "Issue with code '" + issueCode + "' cannot be found because the model is null"); - assertDiagnostic(getXtextTestUtil().getDiagnostician().validate(model), issueCode); - } - - /** - * Assert that diagnosticList does not contain a diagnostic of the given issueCode. - * - * @param issueCode - * the code of the issue to look for - */ - protected void assertNoDiagnostic(final String issueCode) { - assertNoDiagnostic(getPrimaryDiagnostics(), issueCode); - } - - /** - * Assert that the given EObject model does not contain a diagnostic of the given issueCode. - * - * @param model - * the model in which to look for issues, may be {@code null} - * @param issueCode - * the code of the issue to look for - */ - protected void assertNoDiagnostic(final EObject model, final String issueCode) { - assertNotNull(model, "Issue with code '" + issueCode + "' cannot be found because the model is null"); - assertNoDiagnostic(getXtextTestUtil().getDiagnostician().validate(model), issueCode); - } - - /** - * Assert that diagnosticList does not contain any diagnostic. - */ - protected void assertNoDiagnostics() { - assertNoDiagnostics(getPrimaryDiagnostics()); - } - - /** - * Assert that the given EObject model does not contain any diagnostic. - * - * @param model - * the model in which to look for issues, may be {@code null} - */ - protected void assertNoDiagnostics(final EObject model) { - assertNotNull(model, "Assertion cannot be checked because the model is null"); - assertNoDiagnostics(getXtextTestUtil().getDiagnostician().validate(model)); - } - - /** - * Assert that diagnosticList contains a diagnostic with the given message. - * - * @param message - * the message of the issue to look for - */ - protected void assertDiagnosticMessage(final String message) { - assertDiagnosticMessage(getPrimaryDiagnostics(), message); - } - - /** - * Assert that the given EObject model contains a diagnostic with the given message. - * - * @param model - * the model in which to look for issues, may be {@code null} - * @param message - * the message of the issue to look for - */ - protected void assertDiagnosticMessage(final EObject model, final String message) { - assertNotNull(model, "Message '" + message + "' cannot be found because the model is null"); - assertDiagnosticMessage(getXtextTestUtil().getDiagnostician().validate(model), message); - } - - /** - * Assert that diagnosticList contains a diagnostic with the given message. - * - * @param diagnostics - * the diagnostic to check for issues - * @param message - * the message of the issue to look for - */ - private static void assertDiagnosticMessage(final Diagnostic diagnostics, final String message) { - for (Diagnostic diagnostic : diagnostics.getChildren()) { - if (diagnostic.getMessage().equals(message)) { - return; - } - } - fail("Issue with message ' " + message + "' not found"); - } - - /** - * Assert that diagnosticList contains a diagnostic of the given issueCode. - * - * @param diagnostics - * the diagnostic to check for issues - * @param issueCode - * the code of the issue to look for - */ - private void assertDiagnostic(final Diagnostic diagnostics, final String issueCode) { - for (Diagnostic diagnostic : diagnostics.getChildren()) { - if (diagnostic instanceof AbstractValidationDiagnostic && ((AbstractValidationDiagnostic) diagnostic).getIssueCode().equals(issueCode)) { - return; - } - } - fail("Issue with code '" + issueCode + "' not found"); - } - - /** - * Assert that diagnosticList contains a diagnostic of the given issueCode on a given EObject. - * For performance reasons one can validate the root object and afterwards use this method - * to check that a particular diagnostic exists on one of the child objects of the validated model. - * - * @param diagnostics - * the diagnostic to check for issues - * @param targetObject - * the object that should have a diagnostic with the given issueCode - * @param issueCode - * the code of the issue to look for - */ - protected void assertDiagnosticOnObject(final Diagnostic diagnostics, final EObject targetObject, final String issueCode) { - for (Diagnostic diagnostic : diagnostics.getChildren()) { - if (diagnostic instanceof AbstractValidationDiagnostic) { - AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; - if (avd.getSourceEObject() == targetObject && avd.getIssueCode().equals(issueCode)) { - return; - } - } - } - fail("Issue with code '" + issueCode + "' not found"); - } - - /** - * Assert that diagnosticList does not contain a diagnostic of the given issueCode. - * - * @param diagnostics - * the diagnostic to check for issues - * @param issueCode - * the code of the issue to look for - */ - private void assertNoDiagnostic(final Diagnostic diagnostics, final String issueCode) { - for (Diagnostic diagnostic : diagnostics.getChildren()) { - if (((AbstractValidationDiagnostic) diagnostic).getIssueCode().equals(issueCode)) { - fail("Issue with code '" + issueCode + "' found"); - return; - } - } - } - - /** - * Assert that diagnosticList does not contain any diagnostic. - * - * @param diagnostics - * the diagnostic to check for issues - */ - private void assertNoDiagnostics(final Diagnostic diagnostics) { - assertEquals(diagnostics.getCode(), Diagnostic.OK, "Diagnostics should be in OK state."); - assertTrue(diagnostics.getChildren().isEmpty(), "There should be no diagnostics. Instead found " + diagnostics.getChildren().size()); - } - - /** - * Assert no errors on resource exist. - * - * @param object - * the object - */ - public static void assertNoErrorsOnResource(final EObject object) { - final EList errors = object.eResource().getErrors(); - if (!errors.isEmpty()) { - fail(AbstractValidationTest.NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE + "; found " + Lists.transform(errors, Resource.Diagnostic::getMessage)); //$NON-NLS-1$ - } - } - - /** - * Assert no errors on resource with the given message exist. - * - * @param object - * the object - * @param messages - * the messages - */ - public static void assertNoErrorsOnResource(final EObject object, final String... messages) { - List messageList = Arrays.asList(messages); - final EList errors = object.eResource().getErrors(); - for (String errorMessage : Lists.transform(errors, Resource.Diagnostic::getMessage)) { - assertFalse(messageList.contains(errorMessage), NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE + " with message '" + errorMessage + "'."); - } - } - - /** - * Assert no linking errors on resource with the given message exist. - * - * @param object - * the object - * @param referenceType - * the type of the referenced elements - * @param referenceNames - * the names of the referenced elements - */ - public static void assertNoLinkingErrorsOnResource(final EObject object, final String referenceType, final String... referenceNames) { - final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); - final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); - for (final String referenceName : referenceNames) { - boolean found = false; - for (final String errMessage : errorMessages) { - if (errMessage.startsWith(referenceName)) { - found = true; - break; - } - } - assertFalse(found, NLS.bind("Expecting no linking errors on resource for \"{0}\".", referenceName)); - } - } - - /** - * Assert linking errors on resource with the given message exist. - * - * @param object - * the object - * @param referenceType - * the type of the referenced elements - * @param referenceNames - * the names of the referenced elements - */ - public static void assertLinkingErrorsOnResourceExist(final EObject object, final String referenceType, final String... referenceNames) { - final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); - final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); - for (final String referenceName : referenceNames) { - boolean found = false; - for (final String errMessage : errorMessages) { - if (errMessage.contains(referenceName)) { - found = true; - break; - } - } - assertTrue(found, NLS.bind("Expected linking error on \"{0}\" but could not find it", referenceName)); - } - } - - /** - * Expect the given linking error messages on the resource of the given model. - * - * @param object - * the object, must not be {@code null} - * @param errorStrings - * the expected linking error error messages, must not be {@code null} - */ - public static void assertLinkingErrorsWithCustomMessageOnResourceExist(final EObject object, final String... errorStrings) { - final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); - final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); - for (final String s : errorStrings) { - assertTrue(errorMessages.contains(s), NLS.bind("Expected linking error \"{0}\" but could not find it", s)); - } - } - - /** - * Assert no linking errors on resource with the given message exist. - * - * @param object - * the object, must not be {@code null} - * @param messages - * the linking error messages, must not be {@code null} - */ - public static void assertNoLinkingErrorsWithCustomMessageOnResource(final EObject object, final String... messages) { - List messageList = Arrays.asList(messages); - final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); - for (String errorMessage : Lists.transform(linkingErrors, Resource.Diagnostic::getMessage)) { - assertFalse(messageList.contains(errorMessage), NLS.bind("Expecting no linking errors on resource with message \"{0}\".", errorMessage)); - } - } - - /** - * Expect given error messages on the resource of given model. - * - * @param object - * the object - * @param errorStrings - * the error strings - */ - public static void assertErrorsOnResourceExist(final EObject object, final String... errorStrings) { - final EList errors = object.eResource().getErrors(); - final List errorMessages = Lists.transform(errors, Resource.Diagnostic::getMessage); - for (final String s : errorStrings) { - assertTrue(errorMessages.contains(s), NLS.bind("Expected error \"{0}\" but could not find it", s)); - } - } - - /** - * Validates if there is a syntax error present in the source content. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - */ - protected void assertNoSyntaxErrorsOnResource(final String sourceFileName, final CharSequence sourceContent) { - final XtextTestSource testSource = createTestSource(sourceFileName, sourceContent.toString()); - final List errors = testSource.getModel().eResource().getErrors().stream().filter(error -> error instanceof XtextSyntaxDiagnostic).collect(Collectors.toList()); - if (!errors.isEmpty()) { - StringBuilder sb = new StringBuilder("Syntax error is present in the test source.\nList of all found syntax errors:"); - errors.forEach(err -> sb.append("\n\t ").append(err.getMessage())); - throw new AssertionError(sb.toString()); - } - } - - /** - * Memorize the position and issue code of each resource error that appears in the file. - * - * @param root - * root node of the model to be analyzed - */ - protected void memorizeUnexpectedResourceErrors() { - for (Resource.Diagnostic diagnostic : getUnexpectedResourceDiagnostics()) { - if (diagnostic instanceof AbstractDiagnostic) { - AbstractDiagnostic diag = (AbstractDiagnostic) diagnostic; - // Create error message - StringBuilder sb = new StringBuilder(35); - sb.append("Unexpected diagnostic found. Code '"); - sb.append(diag.getCode()); - sb.append(DOT_AND_LINEBREAK); - // Retrieve the position and add the error - memorizeErrorOnPosition(diag.getOffset(), sb.toString()); - } else { - // Create error message - StringBuilder sb = new StringBuilder(30); - sb.append("Unexpected diagnostic found. '"); - sb.append(diagnostic.toString()); - sb.append(DOT_AND_LINEBREAK); - // Add error message - memorizeErrorOnPosition(0, sb.toString()); - } - } - } - - /** - * Memorize the position and issue code of each unexpected diagnostic that appears in the file. - * A diagnostic is considered as expected if a marker with the issue code in the test file was set. - */ - protected void memorizeUnexpectedErrors() { - for (Diagnostic diagnostic : getUnexpectedDiagnostics()) { - if (diagnostic instanceof AbstractValidationDiagnostic) { - AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; - // Create error message - StringBuilder sb = new StringBuilder(30); - sb.append("Unexpected issue found. Code '"); - sb.append(avd.getIssueCode()); - sb.append(DOT_AND_LINEBREAK); - // Retrieve the position and add the error - if (avd instanceof FeatureBasedDiagnostic && ((FeatureBasedDiagnostic) avd).getFeature() != null) { - List nodes = NodeModelUtils.findNodesForFeature(avd.getSourceEObject(), ((FeatureBasedDiagnostic) avd).getFeature()); - if (nodes.isEmpty()) { - INode node = NodeModelUtils.getNode(avd.getSourceEObject()); - memorizeErrorOnPosition(getXtextTestUtil().findFirstNonHiddenLeafNode(node).getTotalOffset(), sb.toString()); - } else { - for (INode node : nodes) { - memorizeErrorOnPosition(getXtextTestUtil().findFirstNonHiddenLeafNode(node).getTotalOffset(), sb.toString()); - } - } - } else if (avd instanceof RangeBasedDiagnostic) { - memorizeErrorOnPosition(((RangeBasedDiagnostic) avd).getOffset(), sb.toString()); - } else { - memorizeErrorOnPosition(NodeModelUtils.getNode(avd.getSourceEObject()).getTotalOffset(), sb.toString()); - } - } else { - // Create error message - StringBuilder sb = new StringBuilder(30); - sb.append("Unexpected diagnostic found. '"); - sb.append(diagnostic.toString()); - sb.append(DOT_AND_LINEBREAK); - // Add error message - memorizeErrorOnPosition(0, sb.toString()); - } - } - } - - /** - * Strictly validates a source given by a file name and content. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceType - * defines if the source is a kernel or customer source, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - */ - protected void validateStrictly(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { - XtextTestSource testSource = processMarkers(sourceFileName, sourceType, sourceContent); - memorizeUnexpectedErrors(); - memorizeUnexpectedResourceErrors(); - processErrorsFound(testSource.getContent()); - afterValidate(); - } - - /** - * Strictly validate a kernel source given by a {@link Pair} of file name and content. - * All not expected diagnostics are considered as an error. - * - * @param sourceFileNameAndContent - * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} - */ - protected void validateKernelSourceStrictly(final Pair sourceFileNameAndContent) { - validateKernelSourceStrictly(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); - } - - /** - * Strictly validate a customer source given by a {@link Pair} of file name and content. - * All not expected diagnostics are considered as an error. - * - * @param sourceFileNameAndContent - * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} - */ - protected void validateCustomerSourceStrictly(final Pair sourceFileNameAndContent) { - validateCustomerSourceStrictly(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); - } - - /** - * Strictly validate a kernel source given by a file name and content. - * All not expected diagnostics are considered as an error. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - */ - protected void validateKernelSourceStrictly(final String sourceFileName, final CharSequence sourceContent) { - validateStrictly(sourceFileName, TestSourceType.CLIENT_ALL, sourceContent); - } - - /** - * Strictly validate a customer source given by a file name and content. - * All not expected diagnostics are considered as an error. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - */ - protected void validateCustomerSourceStrictly(final String sourceFileName, final CharSequence sourceContent) { - validateStrictly(sourceFileName, TestSourceType.CLIENT_CUSTOMER, sourceContent); - } -} +/******************************************************************************* + * 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.eclipse.xtext.validation.ValidationMessageAcceptor.INSIGNIFICANT_INDEX; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.emf.common.util.BasicDiagnostic; +import org.eclipse.emf.common.util.Diagnostic; +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.osgi.util.NLS; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.diagnostics.AbstractDiagnostic; +import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.resource.XtextSyntaxDiagnostic; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.validation.AbstractValidationDiagnostic; +import org.eclipse.xtext.validation.FeatureBasedDiagnostic; +import org.eclipse.xtext.validation.RangeBasedDiagnostic; +import org.eclipse.xtext.xbase.lib.Pair; + +import com.avaloq.tools.ddk.xtext.test.XtextTestSource; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.inject.Provider; + + +/** + * Base class for validation tests. + */ +@SuppressWarnings({"nls", "PMD.ExcessiveClassLength"}) +// CHECKSTYLE:CHECK-OFF MultipleStringLiterals +// CHECKSTYLE:OFF MagicNumber +public abstract class AbstractValidationTest extends AbstractXtextMarkerBasedTest { + + public static final int NO_ERRORS = 0; + + static final String NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE = "Expecting no errors on resource"; + + private static final int SEVERITY_UNDEFINED = -1; + private static final Map CODE_TO_NAME = ImmutableMap.of(Diagnostic.INFO, "INFO", Diagnostic.WARNING, "WARNING", Diagnostic.ERROR, "ERROR"); + + private static final String LINE_BREAK = "\n"; + private static final String DOT_AND_LINEBREAK = "'." + LINE_BREAK; + + /** + * All diagnostics of the current testing file. + */ + private Diagnostic fileDiagnostics; + + /** + * During validation of a source we monitor diagnostics, that are found in the source but were not expected by the test. + * If the validation test is strict, then we will display these unexpected diagnostics as test error. + */ + private final Set unexpectedDiagnostics = Sets.newLinkedHashSet(); + private final Set unexpectedResourceDiagnostics = Sets.newLinkedHashSet(); + + /** + * validation results calculated during test setUp. + * + * @return the diagnostic for the primary test source file + */ + private Diagnostic getPrimaryDiagnostics() { + Object obj = getTestInformation().getTestObject(Diagnostic.class); + assertNotNull(obj, "getPrimaryDiagnostics(): Diagnostics of primary source not null."); + return (Diagnostic) obj; + } + + /** + * Returns the unexpectedDiagnostics. + * + * @return the unexpectedDiagnostics + */ + protected Set getUnexpectedDiagnostics() { + return unexpectedDiagnostics; + } + + /** + * Returns the unexpectedDiagnostics. + * + * @return the unexpectedDiagnostics + */ + protected Set getUnexpectedResourceDiagnostics() { + return unexpectedResourceDiagnostics; + } + + /** + * Assertion testing for {@link AbstractValidationDiagnostic validation issues} at a given source position. + */ + protected class XtextDiagnosticAssertion extends AbstractModelAssertion { + + /** Issue code of the diagnostic. */ + private final String issueCode; + /** Issue message of the diagnostic. */ + private final String message; + /** + * Indicates whether the assertion must find the issue. + * Assertion creates an error if the existence of issue code for the target eobject doesn't correspond to the value of issueMustBeFound. + */ + private final boolean issueMustBeFound; + + private final int expectedSeverity; + + protected XtextDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound) { + this(issueCode, issueMustBeFound, SEVERITY_UNDEFINED, null); + } + + protected XtextDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound, final int severity, final String message) { + this.issueCode = issueCode; + this.issueMustBeFound = issueMustBeFound; + this.expectedSeverity = severity; + this.message = message; + } + + /** + * Check if the given issue code is found among issue codes for the object, located at the given position. + * + * @param root + * root object of the document + * @param pos + * position to locate the target object + */ + @Override + public void apply(final EObject root, final Integer pos) { + final Diagnostic diagnostics = validate(root); + final BasicDiagnostic diagnosticsOnTargetPosition = new BasicDiagnostic(); + boolean issueFound = false; + int actualSeverity = SEVERITY_UNDEFINED; + boolean expectedSeverityMatches = false; + boolean expectedMessageMatches = false; + String actualMessage = ""; + + for (AbstractValidationDiagnostic avd : Iterables.filter(diagnostics.getChildren(), AbstractValidationDiagnostic.class)) { + if (diagnosticPositionEquals(pos, avd)) { + // Add issue to the list of issues at the given position + diagnosticsOnTargetPosition.add(avd); + if (avd.getIssueCode().equals(issueCode)) { + issueFound = true; + actualSeverity = avd.getSeverity(); + // True if the expected severity is not set, or if matches with the actual one + expectedSeverityMatches = expectedSeverity == SEVERITY_UNDEFINED || expectedSeverity == actualSeverity; + actualMessage = avd.getMessage(); + // True if message matches with actual message or message is null + expectedMessageMatches = message == null || actualMessage.equals(message); + if (issueMustBeFound) { + // Remove the diagnostic from the list of non-expected diagnostics + getUnexpectedDiagnostics().remove(avd); + // Don't need to display error messages + if (expectedSeverityMatches && expectedMessageMatches) { + return; + } + } + } + } + } + + // Create error message + createErrorMessage(pos, diagnosticsOnTargetPosition, issueFound, expectedSeverityMatches, actualSeverity, expectedMessageMatches, actualMessage); + } + + /** + * Create an error message (if needed) based on the given input parameters. + * + * @param pos + * position in the source to associate the message with + * @param diagnosticsOnTargetPosition + * diagnostics on the specifies position + * @param issueFound + * specifies whether an issue has been found at the given position + * @param expectedSeverityMatches + * true if expected severity equals actual one, false otherwise + * @param actualSeverity + * actual severity + * @param expectedMessageMatches + * expected message matches + * @param actualMessage + * actual message + */ + private void createErrorMessage(final Integer pos, final BasicDiagnostic diagnosticsOnTargetPosition, final boolean issueFound, final boolean expectedSeverityMatches, final int actualSeverity, final boolean expectedMessageMatches, final String actualMessage) { + StringBuilder errorMessage = new StringBuilder(180); + if (issueMustBeFound && !issueFound) { + errorMessage.append("Expected issue not found. Code '").append(issueCode).append('\n'); + } else if (!issueMustBeFound && issueFound) { + errorMessage.append("There should be no issue with the code '").append(issueCode).append(DOT_AND_LINEBREAK); + } + if (issueFound && !expectedMessageMatches) { + errorMessage.append("Expected message does not match. Expected: '").append(message).append("', Actual: '").append(actualMessage).append('\n'); + } + // If the expected issue has been found, but the actual severity does not match with expected one + if (issueMustBeFound && issueFound && !expectedSeverityMatches) { + errorMessage.append("Severity does not match. Expected: ").append(CODE_TO_NAME.get(expectedSeverity)).append(". Actual: ").append(CODE_TO_NAME.get(actualSeverity)).append(".\n"); + } + // Memorize error message + if (errorMessage.length() > 0) { + if (!diagnosticsOnTargetPosition.getChildren().isEmpty()) { + errorMessage.append(" All issues at this position:\n"); + errorMessage.append(diagnosticsToString(diagnosticsOnTargetPosition, false)); + } + memorizeErrorOnPosition(pos, errorMessage.toString()); + } + } + + /** + * Compare if the position of the given diagnostic equals to the given position in text. + * + * @param pos + * position in text + * @param avd + * diagnostic that we check, if it has the same position as the given position in text + * @return + * TRUE if diagnostic has the same position as the given one, FALSE otherwise. + */ + protected boolean diagnosticPositionEquals(final Integer pos, final AbstractValidationDiagnostic avd) { + if (avd instanceof FeatureBasedDiagnostic && ((FeatureBasedDiagnostic) avd).getFeature() != null) { + List nodes = NodeModelUtils.findNodesForFeature(avd.getSourceEObject(), ((FeatureBasedDiagnostic) avd).getFeature()); + if (nodes.isEmpty()) { + INode node = NodeModelUtils.getNode(avd.getSourceEObject()); + INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(node); + if (firstNonHiddenLeafNode == null) { + return issueMustBeFound; + } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { + return true; + } + } else { + int avdIndex = ((FeatureBasedDiagnostic) avd).getIndex(); + for (int i = 0; i < nodes.size(); i++) { + if (avdIndex == INSIGNIFICANT_INDEX || avdIndex == i) { + INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(nodes.get(i)); + if (firstNonHiddenLeafNode == null) { + return issueMustBeFound; + } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { + return true; + } + } + } + } + } else if (avd instanceof RangeBasedDiagnostic) { + if (((RangeBasedDiagnostic) avd).getOffset() == pos) { + return true; + } + } else { + INode node = NodeModelUtils.getNode(avd.getSourceEObject()); + INode firstNonHiddenLeafNode = getXtextTestUtil().findFirstNonHiddenLeafNode(node); + if (firstNonHiddenLeafNode == null) { + return issueMustBeFound; + } else if (firstNonHiddenLeafNode.getTotalOffset() == pos) { + return true; + } + } + return false; + } + } + + /** + * Assertion testing for {@link AbstractValidationDiagnostic validation issues} at a given source position. + */ + private class ResourceDiagnosticAssertion extends AbstractModelAssertion { + + /** Issue code of the diagnostic. */ + private final String issueCode; + /** Issue message of the diagnostic. */ + private final String message; + /** + * Indicates whether the assertion must find the issue. + * Assertion creates an error if the existence of issue code for the target eobject doesn't correspond to the value of issueMustBeFound. + */ + private final boolean issueMustBeFound; + + private final int expectedSeverity; + + protected ResourceDiagnosticAssertion(final String issueCode, final boolean issueMustBeFound, final int severity, final String message) { + this.issueCode = issueCode; + this.issueMustBeFound = issueMustBeFound; + this.expectedSeverity = severity; + this.message = message; + } + + /** + * Check if the given issue code is found among issue codes for the object, located at the given position. + * + * @param root + * root object of the document + * @param pos + * position to locate the target object + */ + @Override + public void apply(final EObject root, final Integer pos) { + Iterable diagnostics = null; + switch (expectedSeverity) { + case Diagnostic.ERROR: + diagnostics = root.eResource().getErrors(); + break; + case Diagnostic.WARNING: + diagnostics = root.eResource().getWarnings(); + break; + case SEVERITY_UNDEFINED: + diagnostics = Iterables.concat(root.eResource().getErrors(), root.eResource().getWarnings()); + break; + } + final List diagnosticsOnTargetPosition = Lists.newArrayList(); + boolean issueFound = false; + int actualSeverity = expectedSeverity; + boolean expectedMessageMatches = false; + String actualMessage = ""; + + for (AbstractDiagnostic diag : Iterables.filter(diagnostics, AbstractDiagnostic.class)) { + if (diagnosticPositionEquals(pos, diag)) { + // Add issue to the list of issues at the given position + diagnosticsOnTargetPosition.add(diag); + if (diag.getCode() != null && diag.getCode().equals(issueCode)) { + issueFound = true; + if (expectedSeverity == SEVERITY_UNDEFINED) { + actualSeverity = root.eResource().getErrors().contains(diag) ? Diagnostic.ERROR : Diagnostic.WARNING; + } + actualMessage = diag.getMessage(); + // True if message matches with actual message or message is null + expectedMessageMatches = message == null || actualMessage.equals(message); + // Don't need to display error messages + if (issueMustBeFound) { + // Remove the diagnostic from the list of non-expected diagnostics + getUnexpectedResourceDiagnostics().remove(diag); + // Don't need to display error messages + if (expectedMessageMatches) { + return; + } + } + } + } + } + + // Create error message + createErrorMessage(pos, diagnosticsOnTargetPosition, issueFound, true, actualSeverity, expectedMessageMatches, actualMessage); + } + + /** + * Create an error message (if needed) based on the given input parameters. + * + * @param pos + * position in the source to associate the message with + * @param diagnosticsOnTargetPosition + * diagnostics on the specifies position + * @param issueFound + * specifies whether an issue has been found at the given position + * @param expectedSeverityMatches + * true if expected severity equals actual one, false otherwise + * @param actualSeverity + * actual severity + * @param expectedMessageMatches + * expected message matches + * @param actualMessage + * actual message + */ + private void createErrorMessage(final Integer pos, final List diagnosticsOnTargetPosition, final boolean issueFound, final boolean expectedSeverityMatches, final int actualSeverity, final boolean expectedMessageMatches, final String actualMessage) { + StringBuilder errorMessage = new StringBuilder(175); + if (issueMustBeFound && !issueFound) { + errorMessage.append("Expected issue not found. Code '").append(issueCode).append('\n'); + } else if (!issueMustBeFound && issueFound) { + errorMessage.append("There should be no issue with the code '").append(issueCode).append(DOT_AND_LINEBREAK); + } + if (issueFound && !expectedMessageMatches) { + errorMessage.append("Expected message does not match. Expected: '").append(message).append("', Actual: '").append(actualMessage).append('\n'); + } + // If the expected issue has been found, but the actual severity does not match with expected one + if (issueMustBeFound && issueFound && !expectedSeverityMatches) { + errorMessage.append("Severity does not match. Expected: ").append(CODE_TO_NAME.get(expectedSeverity)).append(". Actual: ").append(CODE_TO_NAME.get(actualSeverity)).append(".\n"); + } + // Memorize error message + if (errorMessage.length() > 0) { + if (!diagnosticsOnTargetPosition.isEmpty()) { + errorMessage.append(" All issues at this position:\n"); + errorMessage.append(diagnosticsToString(diagnosticsOnTargetPosition, false)); + } + memorizeErrorOnPosition(pos, errorMessage.toString()); + } + } + + /** + * Compare if the position of the given diagnostic equals to the given position in text. + * + * @param pos + * position in text + * @param diagnostic + * diagnostic that we check, if it has the same position as the given position in text + * @return + * {@code true} if diagnostic has the same position as the given one, {@code false} otherwise. + */ + private boolean diagnosticPositionEquals(final Integer pos, final AbstractDiagnostic diagnostic) { + return diagnostic.getOffset() == pos; + } + } + + /** + * Get a cached version of an object associated with the root object for a given key. + * + * @param + * type of the associated object + * @param root + * root EObject + * @param key + * key identifying the type of the associated object + * @param provider + * provider to deliver an object if there is no cached version + * @return + * cached version of the associated object + */ + protected T getCached(final EObject root, final String key, final Provider provider) { + XtextResource res = (XtextResource) root.eResource(); + return res.getCache().get(key, res, provider); + } + + /** + * Validate the model. + * + * @param root + * root EObject to validate + * @return + * validation results + */ + protected Diagnostic validate(final EObject root) { + return getCached(root, "DIAGNOSTIC", () -> getXtextTestUtil().getDiagnostician().validate(root)); + } + + /** + * Display the path from root object to the target EObject. + * + * @param eObject + * object to display the object path for + * @param offset + * string offset that is added in the beginning of each line + * @return + * object hierarchy as string (each object on a single line) + */ + private String pathFromRootAsString(final EObject eObject, final String offset) { + List hierarchy = Lists.newLinkedList(); + + EObject currentObject = eObject; + while (currentObject != null) { + hierarchy.add(0, offset + currentObject.toString()); + currentObject = currentObject.eContainer(); + } + + return String.join("\n", hierarchy); + } + + /** + * Persist list diagnostics into string to display the list of issue codes. + * + * @param diagnostics + * list of diagnostics + * @param displayPathToTargetObject + * if true, the path through the object hierarchy is printed out up to the root node + * @return + * string with list of issue codes, separated with a line break + */ + // TODO (ACF-4153) generalize for all kinds of errors and move to AbstractXtextTest + private String diagnosticsToString(final Diagnostic diagnostics, final boolean displayPathToTargetObject) { + StringBuilder sb = new StringBuilder(); + for (Diagnostic diagnostic : diagnostics.getChildren()) { + if (diagnostic instanceof AbstractValidationDiagnostic) { + AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; + sb.append(" "); + sb.append(avd.getIssueCode()); + if (displayPathToTargetObject) { + sb.append(" at line: "); + ICompositeNode compositeNode = NodeModelUtils.findActualNodeFor(avd.getSourceEObject()); + if (compositeNode != null) { + sb.append(compositeNode.getStartLine()); + } else { + sb.append("Unknown"); + } + sb.append(" on \n"); + sb.append(pathFromRootAsString(avd.getSourceEObject(), " ")); + } + sb.append(LINE_BREAK); + } + } + return sb.toString(); + } + + /** + * Persist list diagnostics into string to display the list of issue codes. + * + * @param diagnostics + * list of diagnostics + * @param displayPathToTargetObject + * if true, the path through the object hierarchy is printed out up to the root node + * @return + * string with list of issue codes, separated with a line break + */ + // TODO (ACF-4153) generalize for all kinds of errors and move to AbstractXtextTest + private String diagnosticsToString(final List diagnostics, final boolean displayPathToTargetObject) { + StringBuilder sb = new StringBuilder(25); + for (Resource.Diagnostic diagnostic : diagnostics) { + if (diagnostic instanceof AbstractDiagnostic) { + AbstractDiagnostic diag = (AbstractDiagnostic) diagnostic; + sb.append(" "); + sb.append(diag.getCode()); + if (displayPathToTargetObject) { + sb.append(" at line: "); + sb.append(diag.getLine()); + sb.append(" on \n"); + sb.append(" "); + sb.append(diag.getUriToProblem()); + } + sb.append(LINE_BREAK); + } + } + return sb.toString(); + } + + @Override + protected void beforeAllTests() { + super.beforeAllTests(); + if (getTestSource() != null) { + Diagnostic primaryDiagnostics = getXtextTestUtil().getDiagnostician().validate(getSemanticModel()); + getTestInformation().putTestObject(Diagnostic.class, primaryDiagnostics); + } + } + + /** + * Register a new validation marker with the given issue code. Expects an info. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String info(final String issueCode) { + return info(issueCode, null); + } + + /** + * Register a new validation marker with the given issue code and message. Expects an info. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @param message + * the expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String info(final String issueCode, final String message) { + return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.INFO, message)); + } + + /** + * Register a new validation marker with the given issue code. Expects a warning if the condition is {@code true}, no diagnostic otherwise. + * + * @param condition + * the condition when the marker is expected + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String warningIf(final boolean condition, final String issueCode) { + if (condition) { + return warning(issueCode); + } else { + return noDiagnostic(issueCode); + } + } + + /** + * Register a new validation marker with the given issue code. Expects a warning. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String warning(final String issueCode) { + return warning(issueCode, null); + } + + /** + * Register a new validation marker with the given issue code and message. Expects a warning. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @param message + * the expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String warning(final String issueCode, final String message) { + return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.WARNING, message)); + } + + /** + * Register a new validation marker with the given issue code. Expects an error if the condition is {@code true}, no diagnostic otherwise. + * + * @param condition + * the condition when the marker is expected + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String errorIf(final boolean condition, final String issueCode) { + if (condition) { + return error(issueCode); + } else { + return noDiagnostic(issueCode); + } + } + + /** + * Register a new validation marker with the given issue code. Expects an error. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String error(final String issueCode) { + return error(issueCode, null); + } + + /** + * Register a new validation marker with the given issue code and message. Expects an error. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @param message + * the expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String error(final String issueCode, final String message) { + return addAssertion(new XtextDiagnosticAssertion(issueCode, true, Diagnostic.ERROR, message)); + } + + /** + * Register a new validation marker with the given issue code. + * The issue is expected to be found in the test file. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String diagnostic(final String issueCode) { + return diagnostic(issueCode, null); + } + + /** + * Register a new validation marker with the given issue code and message. + * The issue and message are expected to be found in the test file. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @param message + * the expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String diagnostic(final String issueCode, final String message) { + return addAssertion(new XtextDiagnosticAssertion(issueCode, true, SEVERITY_UNDEFINED, message)); + } + + /** + * Register a new linking error validation marker. + * The issue is expected to be found in the test file. + * + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String linkingError() { + return linkingError(null); + } + + /** + * Register a new linking error validation marker with the given message. + * The issue is expected to be found in the test file. + * + * @param message + * issuethe expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String linkingError(final String message) { + return addAssertion(new ResourceDiagnosticAssertion(org.eclipse.xtext.diagnostics.Diagnostic.LINKING_DIAGNOSTIC, true, Diagnostic.ERROR, message)); + } + + /** + * Register a new resource validation marker with the given issue code and message. + * The issue is expected to be found in the test file. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @param message + * the expected issue message + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String resourceDiagnostic(final String issueCode, final String message) { + return addAssertion(new ResourceDiagnosticAssertion(issueCode, true, Diagnostic.ERROR, message)); + } + + /** + * Register a new validation marker with the given issue code. + * The issue is expected NOT to be found in the test file. + * + * @param issueCode + * issue code (usually found as static constant of the JavaValidator class of the DSL being tested) + * @return + * unique marker that can be used in the input string to mark a position that should be validated + */ + protected String noDiagnostic(final String issueCode) { + return addAssertion(new XtextDiagnosticAssertion(issueCode, false)); + } + + @Override + protected void beforeApplyAssertions(final XtextTestSource testSource) { + super.beforeApplyAssertions(testSource); + EObject root = testSource.getModel(); + // Get all diagnostics of the current testing file + EcoreUtil2.resolveLazyCrossReferences(root.eResource(), CancelIndicator.NullImpl); + fileDiagnostics = validate(root); + getUnexpectedDiagnostics().addAll(fileDiagnostics.getChildren()); + getUnexpectedResourceDiagnostics().addAll(root.eResource().getErrors()); + getUnexpectedResourceDiagnostics().addAll(root.eResource().getWarnings()); + } + + @Override + protected String getAdditionalErrorMessageInformation() { + return diagnosticsToString(fileDiagnostics, true); + } + + @Override + protected void afterValidate() { + super.afterValidate(); + // Garbage collection + getUnexpectedDiagnostics().clear(); + getUnexpectedResourceDiagnostics().clear(); + } + + /** + * Assert that diagnosticList contains a diagnostic of the given issueCode. + * + * @param issueCode + * the code of the issue to look for + */ + protected void assertDiagnostic(final String issueCode) { + assertDiagnostic(getPrimaryDiagnostics(), issueCode); + } + + /** + * Assert that the given EObject model contains a diagnostic of the given issueCode. + * + * @param model + * the model in which to look for issues, may be {@code null} + * @param issueCode + * the code of the issue to look for + */ + protected void assertDiagnostic(final EObject model, final String issueCode) { + assertNotNull(model, "Issue with code '" + issueCode + "' cannot be found because the model is null"); + assertDiagnostic(getXtextTestUtil().getDiagnostician().validate(model), issueCode); + } + + /** + * Assert that diagnosticList does not contain a diagnostic of the given issueCode. + * + * @param issueCode + * the code of the issue to look for + */ + protected void assertNoDiagnostic(final String issueCode) { + assertNoDiagnostic(getPrimaryDiagnostics(), issueCode); + } + + /** + * Assert that the given EObject model does not contain a diagnostic of the given issueCode. + * + * @param model + * the model in which to look for issues, may be {@code null} + * @param issueCode + * the code of the issue to look for + */ + protected void assertNoDiagnostic(final EObject model, final String issueCode) { + assertNotNull(model, "Issue with code '" + issueCode + "' cannot be found because the model is null"); + assertNoDiagnostic(getXtextTestUtil().getDiagnostician().validate(model), issueCode); + } + + /** + * Assert that diagnosticList does not contain any diagnostic. + */ + protected void assertNoDiagnostics() { + assertNoDiagnostics(getPrimaryDiagnostics()); + } + + /** + * Assert that the given EObject model does not contain any diagnostic. + * + * @param model + * the model in which to look for issues, may be {@code null} + */ + protected void assertNoDiagnostics(final EObject model) { + assertNotNull(model, "Assertion cannot be checked because the model is null"); + assertNoDiagnostics(getXtextTestUtil().getDiagnostician().validate(model)); + } + + /** + * Assert that diagnosticList contains a diagnostic with the given message. + * + * @param message + * the message of the issue to look for + */ + protected void assertDiagnosticMessage(final String message) { + assertDiagnosticMessage(getPrimaryDiagnostics(), message); + } + + /** + * Assert that the given EObject model contains a diagnostic with the given message. + * + * @param model + * the model in which to look for issues, may be {@code null} + * @param message + * the message of the issue to look for + */ + protected void assertDiagnosticMessage(final EObject model, final String message) { + assertNotNull(model, "Message '" + message + "' cannot be found because the model is null"); + assertDiagnosticMessage(getXtextTestUtil().getDiagnostician().validate(model), message); + } + + /** + * Assert that diagnosticList contains a diagnostic with the given message. + * + * @param diagnostics + * the diagnostic to check for issues + * @param message + * the message of the issue to look for + */ + private static void assertDiagnosticMessage(final Diagnostic diagnostics, final String message) { + for (Diagnostic diagnostic : diagnostics.getChildren()) { + if (diagnostic.getMessage().equals(message)) { + return; + } + } + fail("Issue with message ' " + message + "' not found"); + } + + /** + * Assert that diagnosticList contains a diagnostic of the given issueCode. + * + * @param diagnostics + * the diagnostic to check for issues + * @param issueCode + * the code of the issue to look for + */ + private void assertDiagnostic(final Diagnostic diagnostics, final String issueCode) { + for (Diagnostic diagnostic : diagnostics.getChildren()) { + if (diagnostic instanceof AbstractValidationDiagnostic && ((AbstractValidationDiagnostic) diagnostic).getIssueCode().equals(issueCode)) { + return; + } + } + fail("Issue with code '" + issueCode + "' not found"); + } + + /** + * Assert that diagnosticList contains a diagnostic of the given issueCode on a given EObject. + * For performance reasons one can validate the root object and afterwards use this method + * to check that a particular diagnostic exists on one of the child objects of the validated model. + * + * @param diagnostics + * the diagnostic to check for issues + * @param targetObject + * the object that should have a diagnostic with the given issueCode + * @param issueCode + * the code of the issue to look for + */ + protected void assertDiagnosticOnObject(final Diagnostic diagnostics, final EObject targetObject, final String issueCode) { + for (Diagnostic diagnostic : diagnostics.getChildren()) { + if (diagnostic instanceof AbstractValidationDiagnostic) { + AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; + if (avd.getSourceEObject() == targetObject && avd.getIssueCode().equals(issueCode)) { + return; + } + } + } + fail("Issue with code '" + issueCode + "' not found"); + } + + /** + * Assert that diagnosticList does not contain a diagnostic of the given issueCode. + * + * @param diagnostics + * the diagnostic to check for issues + * @param issueCode + * the code of the issue to look for + */ + private void assertNoDiagnostic(final Diagnostic diagnostics, final String issueCode) { + for (Diagnostic diagnostic : diagnostics.getChildren()) { + if (((AbstractValidationDiagnostic) diagnostic).getIssueCode().equals(issueCode)) { + fail("Issue with code '" + issueCode + "' found"); + return; + } + } + } + + /** + * Assert that diagnosticList does not contain any diagnostic. + * + * @param diagnostics + * the diagnostic to check for issues + */ + private void assertNoDiagnostics(final Diagnostic diagnostics) { + assertEquals(diagnostics.getCode(), Diagnostic.OK, "Diagnostics should be in OK state."); + assertTrue(diagnostics.getChildren().isEmpty(), "There should be no diagnostics. Instead found " + diagnostics.getChildren().size()); + } + + /** + * Assert no errors on resource exist. + * + * @param object + * the object + */ + public static void assertNoErrorsOnResource(final EObject object) { + final EList errors = object.eResource().getErrors(); + if (!errors.isEmpty()) { + fail(AbstractValidationTest.NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE + "; found " + Lists.transform(errors, Resource.Diagnostic::getMessage)); //$NON-NLS-1$ + } + } + + /** + * Assert no errors on resource with the given message exist. + * + * @param object + * the object + * @param messages + * the messages + */ + public static void assertNoErrorsOnResource(final EObject object, final String... messages) { + List messageList = Arrays.asList(messages); + final EList errors = object.eResource().getErrors(); + for (String errorMessage : Lists.transform(errors, Resource.Diagnostic::getMessage)) { + assertFalse(messageList.contains(errorMessage), NO_ERRORS_FOUND_ON_RESOURCE_MESSAGE + " with message '" + errorMessage + "'."); + } + } + + /** + * Assert no linking errors on resource with the given message exist. + * + * @param object + * the object + * @param referenceType + * the type of the referenced elements + * @param referenceNames + * the names of the referenced elements + */ + public static void assertNoLinkingErrorsOnResource(final EObject object, final String referenceType, final String... referenceNames) { + final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); + final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); + for (final String referenceName : referenceNames) { + boolean found = false; + for (final String errMessage : errorMessages) { + if (errMessage.startsWith(referenceName)) { + found = true; + break; + } + } + assertFalse(found, NLS.bind("Expecting no linking errors on resource for \"{0}\".", referenceName)); + } + } + + /** + * Assert linking errors on resource with the given message exist. + * + * @param object + * the object + * @param referenceType + * the type of the referenced elements + * @param referenceNames + * the names of the referenced elements + */ + public static void assertLinkingErrorsOnResourceExist(final EObject object, final String referenceType, final String... referenceNames) { + final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); + final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); + for (final String referenceName : referenceNames) { + boolean found = false; + for (final String errMessage : errorMessages) { + if (errMessage.contains(referenceName)) { + found = true; + break; + } + } + assertTrue(found, NLS.bind("Expected linking error on \"{0}\" but could not find it", referenceName)); + } + } + + /** + * Expect the given linking error messages on the resource of the given model. + * + * @param object + * the object, must not be {@code null} + * @param errorStrings + * the expected linking error error messages, must not be {@code null} + */ + public static void assertLinkingErrorsWithCustomMessageOnResourceExist(final EObject object, final String... errorStrings) { + final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); + final List errorMessages = Lists.transform(linkingErrors, Resource.Diagnostic::getMessage); + for (final String s : errorStrings) { + assertTrue(errorMessages.contains(s), NLS.bind("Expected linking error \"{0}\" but could not find it", s)); + } + } + + /** + * Assert no linking errors on resource with the given message exist. + * + * @param object + * the object, must not be {@code null} + * @param messages + * the linking error messages, must not be {@code null} + */ + public static void assertNoLinkingErrorsWithCustomMessageOnResource(final EObject object, final String... messages) { + List messageList = Arrays.asList(messages); + final List linkingErrors = object.eResource().getErrors().stream().filter(error -> error instanceof XtextLinkingDiagnostic).collect(Collectors.toList()); + for (String errorMessage : Lists.transform(linkingErrors, Resource.Diagnostic::getMessage)) { + assertFalse(messageList.contains(errorMessage), NLS.bind("Expecting no linking errors on resource with message \"{0}\".", errorMessage)); + } + } + + /** + * Expect given error messages on the resource of given model. + * + * @param object + * the object + * @param errorStrings + * the error strings + */ + public static void assertErrorsOnResourceExist(final EObject object, final String... errorStrings) { + final EList errors = object.eResource().getErrors(); + final List errorMessages = Lists.transform(errors, Resource.Diagnostic::getMessage); + for (final String s : errorStrings) { + assertTrue(errorMessages.contains(s), NLS.bind("Expected error \"{0}\" but could not find it", s)); + } + } + + /** + * Validates if there is a syntax error present in the source content. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + */ + protected void assertNoSyntaxErrorsOnResource(final String sourceFileName, final CharSequence sourceContent) { + final XtextTestSource testSource = createTestSource(sourceFileName, sourceContent.toString()); + final List errors = testSource.getModel().eResource().getErrors().stream().filter(error -> error instanceof XtextSyntaxDiagnostic).collect(Collectors.toList()); + if (!errors.isEmpty()) { + StringBuilder sb = new StringBuilder("Syntax error is present in the test source.\nList of all found syntax errors:"); + errors.forEach(err -> sb.append("\n\t ").append(err.getMessage())); + throw new AssertionError(sb.toString()); + } + } + + /** + * Memorize the position and issue code of each resource error that appears in the file. + * + * @param root + * root node of the model to be analyzed + */ + protected void memorizeUnexpectedResourceErrors() { + for (Resource.Diagnostic diagnostic : getUnexpectedResourceDiagnostics()) { + if (diagnostic instanceof AbstractDiagnostic) { + AbstractDiagnostic diag = (AbstractDiagnostic) diagnostic; + // Create error message + StringBuilder sb = new StringBuilder(35); + sb.append("Unexpected diagnostic found. Code '"); + sb.append(diag.getCode()); + sb.append(DOT_AND_LINEBREAK); + // Retrieve the position and add the error + memorizeErrorOnPosition(diag.getOffset(), sb.toString()); + } else { + // Create error message + StringBuilder sb = new StringBuilder(30); + sb.append("Unexpected diagnostic found. '"); + sb.append(diagnostic.toString()); + sb.append(DOT_AND_LINEBREAK); + // Add error message + memorizeErrorOnPosition(0, sb.toString()); + } + } + } + + /** + * Memorize the position and issue code of each unexpected diagnostic that appears in the file. + * A diagnostic is considered as expected if a marker with the issue code in the test file was set. + */ + protected void memorizeUnexpectedErrors() { + for (Diagnostic diagnostic : getUnexpectedDiagnostics()) { + if (diagnostic instanceof AbstractValidationDiagnostic) { + AbstractValidationDiagnostic avd = (AbstractValidationDiagnostic) diagnostic; + // Create error message + StringBuilder sb = new StringBuilder(30); + sb.append("Unexpected issue found. Code '"); + sb.append(avd.getIssueCode()); + sb.append(DOT_AND_LINEBREAK); + // Retrieve the position and add the error + if (avd instanceof FeatureBasedDiagnostic && ((FeatureBasedDiagnostic) avd).getFeature() != null) { + List nodes = NodeModelUtils.findNodesForFeature(avd.getSourceEObject(), ((FeatureBasedDiagnostic) avd).getFeature()); + if (nodes.isEmpty()) { + INode node = NodeModelUtils.getNode(avd.getSourceEObject()); + memorizeErrorOnPosition(getXtextTestUtil().findFirstNonHiddenLeafNode(node).getTotalOffset(), sb.toString()); + } else { + for (INode node : nodes) { + memorizeErrorOnPosition(getXtextTestUtil().findFirstNonHiddenLeafNode(node).getTotalOffset(), sb.toString()); + } + } + } else if (avd instanceof RangeBasedDiagnostic) { + memorizeErrorOnPosition(((RangeBasedDiagnostic) avd).getOffset(), sb.toString()); + } else { + memorizeErrorOnPosition(NodeModelUtils.getNode(avd.getSourceEObject()).getTotalOffset(), sb.toString()); + } + } else { + // Create error message + StringBuilder sb = new StringBuilder(30); + sb.append("Unexpected diagnostic found. '"); + sb.append(diagnostic.toString()); + sb.append(DOT_AND_LINEBREAK); + // Add error message + memorizeErrorOnPosition(0, sb.toString()); + } + } + } + + /** + * Strictly validates a source given by a file name and content. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceType + * defines if the source is a kernel or customer source, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + */ + protected void validateStrictly(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { + XtextTestSource testSource = processMarkers(sourceFileName, sourceType, sourceContent); + memorizeUnexpectedErrors(); + memorizeUnexpectedResourceErrors(); + processErrorsFound(testSource.getContent()); + afterValidate(); + } + + /** + * Strictly validate a kernel source given by a {@link Pair} of file name and content. + * All not expected diagnostics are considered as an error. + * + * @param sourceFileNameAndContent + * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} + */ + protected void validateKernelSourceStrictly(final Pair sourceFileNameAndContent) { + validateKernelSourceStrictly(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); + } + + /** + * Strictly validate a customer source given by a {@link Pair} of file name and content. + * All not expected diagnostics are considered as an error. + * + * @param sourceFileNameAndContent + * the file name and content, given as the key and value of the pair, respectively, must not be {@code null} + */ + protected void validateCustomerSourceStrictly(final Pair sourceFileNameAndContent) { + validateCustomerSourceStrictly(sourceFileNameAndContent.getKey(), sourceFileNameAndContent.getValue()); + } + + /** + * Strictly validate a kernel source given by a file name and content. + * All not expected diagnostics are considered as an error. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + */ + protected void validateKernelSourceStrictly(final String sourceFileName, final CharSequence sourceContent) { + validateStrictly(sourceFileName, TestSourceType.CLIENT_ALL, sourceContent); + } + + /** + * Strictly validate a customer source given by a file name and content. + * All not expected diagnostics are considered as an error. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + */ + protected void validateCustomerSourceStrictly(final String sourceFileName, final CharSequence sourceContent) { + validateStrictly(sourceFileName, TestSourceType.CLIENT_CUSTOMER, sourceContent); + } +} 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 index 0b443eb511..29a544bd67 100644 --- 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 @@ -1,203 +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(); - } -} +/******************************************************************************* + * 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/AbstractXtextMarkerBasedTest.java b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextMarkerBasedTest.java index e4bccc5a69..38a5ab84c0 100644 --- a/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextMarkerBasedTest.java +++ b/com.avaloq.tools.ddk.xtext.test.core/src/com/avaloq/tools/ddk/xtext/test/jupiter/AbstractXtextMarkerBasedTest.java @@ -1,656 +1,656 @@ -/******************************************************************************* - * 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.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.SortedMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.CrossReference; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.xbase.lib.Procedures; - -import com.avaloq.tools.ddk.xtext.test.TagCompilationParticipant; -import com.avaloq.tools.ddk.xtext.test.XtextTestSource; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.collect.Sets; - - -/** - * Abstract class that supports Xtend based test implementations to use markers in the test sources. - */ -@SuppressWarnings("nls") -public abstract class AbstractXtextMarkerBasedTest extends AbstractXtextTest { - - private static final String INVALID_TEST_CONFIGURATION = "Invalid test configuration. Missing org.eclipse.xtend.lib in MANIFEST.MF in this plugin?"; //$NON-NLS-1$ - private static final String LINE_BREAK = "\n"; - private static final String MARKER_START_GUARD = "##"; - private static final String MARKER_END_GUARD = "#"; - private static final String SPLITTING_LINE = "-------------------------------------------------\n"; - protected static final Pattern PATTERN = Pattern.compile(MARKER_START_GUARD + "(\\d+)" + MARKER_END_GUARD); - - /** The tag id. */ - private int localMarkerIdCounter; - - private final Map assertions = Maps.newHashMap(); - private final SortedMap errorsOnPosition = Maps.newTreeMap(); - /** Used Tags to find Duplicates. */ - private final Set usedTags = Sets.newHashSet(); - - /** - * Indicates if a testing source is a kernel or customer source. - */ - protected enum TestSourceType { - CLIENT_ALL, - CLIENT_CUSTOMER - } - - // -------------------------------------------------------------------------- - // AbstractModelAssertion - // -------------------------------------------------------------------------- - - /** - * Interface for testing assertions on a given source position. - */ - protected abstract class AbstractModelAssertion implements Procedures.Procedure2 { - - @Override - public abstract void apply(EObject semanticModel, Integer pos); - - } - - // -------------------------------------------------------------------------- - // Methods of testing framework - // -------------------------------------------------------------------------- - - @Override - protected void afterEachTest() { - getMarkerTagsInfo().clearTags(localMarkerIdCounter); - super.afterEachTest(); - assertions.clear(); - usedTags.clear(); - errorsOnPosition.clear(); - } - - @Override - protected void beforeEachTest() { - localMarkerIdCounter = 0; - super.beforeEachTest(); - assertFalse(getMarkerTagsInfo().isInvalidTestClass(), INVALID_TEST_CONFIGURATION); - } - - // -------------------------------------------------------------------------- - // Methods to be used by the actual testing classes - // -------------------------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - String processedContent = processContentAndRegisterOffsets(sourceFileName, sourceContent); - super.addKernelSourceToWorkspace(sourceFileName, processedContent); - } - - /** - * {@inheritDoc} - */ - @Override - protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - String processedContent = processContentAndRegisterOffsets(CUSTOMER_SOURCE_PREFIX + sourceFileName, sourceContent); - super.addCustomerSourceToWorkspace(sourceFileName, processedContent); - } - - /** - * Removes tags and stores them in the test info object. - * - * @param sourceFileName - * Source file name, must not be {@code null} - * @param sourceContent - * Content of the test source (may contain tags), must not be {@code null} - * @return Content without tags, never {@code null} - */ - private String processContentAndRegisterOffsets(final String sourceFileName, final CharSequence sourceContent) { - Map offsets = Maps.newHashMap(); - String content = removeMarkersFromContent(sourceContent, offsets); - for (Entry tag : offsets.entrySet()) { - getMarkerTagsInfo().registerRequiredSourceTag(tag.getKey(), sourceFileName, tag.getValue()); - } - return content; - } - - /** - * Removes the Xtend markers from a source. - * - * @param sourceContent - * the source content, not {@code null} - * @param tagToOffset - * Map to be populated with tag to offset pairs, must not be {@code null} - * @return the content without markers, never {@code null} - */ - private String removeMarkersFromContent(final CharSequence sourceContent, final Map tagToOffset) { - StringBuffer withoutMarkers = new StringBuffer(sourceContent.length()); - Matcher m = PATTERN.matcher(sourceContent); - while (m.find()) { - m.appendReplacement(withoutMarkers, ""); - tagToOffset.put(Integer.parseInt(m.group(1)), withoutMarkers.length()); - } - m.appendTail(withoutMarkers); - return withoutMarkers.toString(); - } - - /** - * Returns the model for the given source name and string. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, must not be {@code null} - * @param sourceContent - * source, must not be {@code null} - * @return Model element for the parsed source, may be {@code null} - */ - protected EObject getModel(final String sourceFileName, final CharSequence sourceContent) { - Map offsets = Maps.newHashMap(); - String content = removeMarkersFromContent(sourceContent, offsets); - EObject root = null; - try { - root = getXtextTestUtil().getModel(sourceFileName, content); - INode node = NodeModelUtils.getNode(root); - for (Entry tag : offsets.entrySet()) { - INode leafNode = NodeModelUtils.findLeafNodeAtOffset(node, tag.getValue() + 1); - EObject context = NodeModelUtils.findActualSemanticObjectFor(leafNode); - // Search for cross reference - CrossReference crossReference = null; - while (leafNode != null) { - if (leafNode.getGrammarElement() instanceof CrossReference) { - crossReference = (CrossReference) leafNode.getGrammarElement(); - break; - } - leafNode = leafNode.getParent(); - } - getMarkerTagsInfo().registerLocalTag(tag.getKey(), context, crossReference); - } - } catch (IOException e) { - fail("Exception while creating model from input string: " + e.getMessage()); //$NON-NLS-1$ - } - return root; - } - - /** - * Does the same as get model, but returns void. - * - * @param sourceFileName - * the file name that should be associated with the parsed content - * @param sourceContent - * source - */ - protected void registerModel(final String sourceFileName, final CharSequence sourceContent) { - getModel(sourceFileName, sourceContent); - } - - /** - * Validate a kernel source given by a file name and content. - * All not expected diagnostics are ignored. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, not {@code null} - * @param sourceContent - * source, not {@code null} - */ - protected void validateKernelSource(final String sourceFileName, final CharSequence sourceContent) { - validate(sourceFileName, TestSourceType.CLIENT_ALL, sourceContent); - } - - /** - * Validate a customer source given by a file name and content. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, not {@code null} - * @param sourceContent - * source, not {@code null} - */ - protected void validateCustomerSource(final String sourceFileName, final CharSequence sourceContent) { - validate(sourceFileName, TestSourceType.CLIENT_CUSTOMER, sourceContent); - } - - // -------------------------------------------------------------------------- - // Methods to be used by more specific abstract classes - // -------------------------------------------------------------------------- - - /** - * Add assertion to the list of assertions and return the corresponding string marker. - * - * @param assertion - * assertion to be added, not {@code null} - * @return - * string marker corresponding to the added assertion - */ - protected String addAssertion(final AbstractModelAssertion assertion) { - // Assertions are given a local tag as they are local markers affecting and declared in only one source - Integer markerId = getTag(); - assertions.put(markerId, assertion); - return mark(markerId); - } - - /** - * Creates a mark with the given id. Use this method in tests to insert marks. - *

- * If the given mark id is 0 then it means that the test was not well configured. All the id-s that are generated by {@link #getTag} start with 1, all the - * global ids start with {@value com.avaloq.tools.asmd.testbase.scoping.TagCompilationParticipant#COUNTER_BASE}. Since it is unlikely to get local ids wrong, - * the most common reason for global ids to be wrong is that they were not initialized. Global ids get initialized with active annotation - * {@link com.avaloq.tools.asmd.testbase.scoping.Tag} that executes {@link com.avaloq.tools.asmd.testbase.scoping.TagCompilationParticipant}. Active - * annotations do not get executed if {@code org.eclipse.xtend.lib} is missing in {@code MANIFEST.MF}. Thus we report this common mistake to the user via an - * assertion. - *

- * - * @param id - * Mark id - * @return Mark text to be inserted in the source file, never {@code null} - */ - protected String mark(final int id) { - assertFalse(usedTags.contains(id), "Tag with " + id + " used to mark more than one location."); //$NON-NLS-1$ //$NON-NLS-2$ - usedTags.add(id); - if (id < 1) { - getMarkerTagsInfo().setTestClassInvalid(); - throw new AssertionError(INVALID_TEST_CONFIGURATION); - } - return MARKER_START_GUARD + id + MARKER_END_GUARD; - } - - /** - * Memorize an error that was detected during the validation of the current testing file. - * - * @param position - * position of the error in file, not {@code null} - * @param error - * string with error, not {@code null} - */ - protected void memorizeErrorOnPosition(final Integer position, final String error) { - if (!errorsOnPosition.containsKey(position)) { - errorsOnPosition.put(position, new StringBuilder()); - } - errorsOnPosition.get(position).append(error); - } - - /** - * Processes all the inserted markers in a given source and creates a new {@link XtextTestSource} without the markers. - * - * @param sourceFileName - * the name of the source, may be {@code null} - * @param sourceType - * the type of the source, may be {@code null} - * @param sourceContent - * the content of the source, must not be {@code null} - * @return the {@link XtextTestSource} created. - */ - protected XtextTestSource processMarkers(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { - StringBuilder withoutMarkers = new StringBuilder(); - final Multimap positionToAssertionMap = LinkedHashMultimap.create(); - Matcher m = PATTERN.matcher(sourceContent); - int lastEnd = 0; - while (m.find()) { - withoutMarkers.append(sourceContent.subSequence(lastEnd, m.start())); - lastEnd = m.end(); - int markerId = Integer.parseInt(m.group(1)); - // save the position of the marker only if we are dealing with an assertion marker - AbstractModelAssertion assertionMarker = assertions.get(markerId); - if (assertionMarker != null) { - positionToAssertionMap.put(withoutMarkers.length(), assertionMarker); - } - } - // Add the rest part of input string - withoutMarkers.append(sourceContent.subSequence(lastEnd, sourceContent.length())); - - // Calculate source name - String fullSourceFileName = ""; - if (sourceType == TestSourceType.CLIENT_CUSTOMER) { - fullSourceFileName = CUSTOMER_SOURCE_PREFIX; - } - fullSourceFileName = fullSourceFileName.concat(sourceFileName); - - EObject semanticModel; - String withoutMarkersAsString = withoutMarkers.toString(); - XtextTestSource testSource = createTestSource(fullSourceFileName, withoutMarkersAsString); - semanticModel = testSource.getModel(); - beforeApplyAssertions(testSource); - // Run validations on markers - for (Map.Entry entry : positionToAssertionMap.entries()) { - entry.getValue().apply(semanticModel, entry.getKey()); - } - return testSource; - } - - /** - * Validate a source given by a file name and content. - * - * @param sourceFileName - * the file name that should be associated with the parsed content, not {@code null} - * @param sourceType - * defines if the source is a kernel or customer source, not {@code null} - * @param sourceContent - * source, not {@code null} - */ - protected void validate(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { - XtextTestSource testSource = processMarkers(sourceFileName, sourceType, sourceContent); - processErrorsFound(testSource.getContent()); - afterValidate(); - } - - /** - * Processes all the diagnostics in a given source. - * - * @param sourceWithoutMarkers - * the source to process, must not be {@code null} - */ - protected void processErrorsFound(final String sourceWithoutMarkers) { - if (!errorsOnPosition.isEmpty()) { - // CHECKSTYLE:OFF MagicNumber - StringBuilder sb = new StringBuilder(50); - // CHECKSTYLE:ON - sb.append(memorizedErrorsToString(sourceWithoutMarkers)); - sb.append(SPLITTING_LINE); - sb.append("List of all found diagnostics:\n"); - sb.append(getAdditionalErrorMessageInformation()); - assertEquals(sourceWithoutMarkers, sb.toString(), "Errors found. Consider compare view."); - } - } - - /** - * Inject memorized errors into the input file on positions where they were detected. - * - * @param source - * text of the input testing source, not {@code null} - * @return - * input testing source with injected errors, never {@code null} - */ - private String memorizedErrorsToString(final String source) { - StringBuilder result = new StringBuilder(); - StringBuilder errorBuffer = new StringBuilder(); - // Sort positions - List positions = Lists.newArrayList(errorsOnPosition.keySet()); - - int posIdx = 0; - int sourceIdx = 0; - - while (sourceIdx < source.length()) { - int lineBreakIndex = source.indexOf('\n', sourceIdx); - if (lineBreakIndex < 0) { - lineBreakIndex = source.length(); - } - while (posIdx < positions.size() && positions.get(posIdx) < lineBreakIndex) { - int nextPos = positions.get(posIdx); - result.append(source.substring(sourceIdx, nextPos)); - result.append("'); - // Add error message to buffer - errorBuffer.append(SPLITTING_LINE); - errorBuffer.append("FAILURE "); - errorBuffer.append(posIdx + 1); - errorBuffer.append(": "); - errorBuffer.append(errorsOnPosition.get(nextPos)); - sourceIdx = nextPos; - posIdx++; - } - if (errorBuffer.length() > 0) { - errorBuffer.append(SPLITTING_LINE); - } - result.append(source.substring(sourceIdx, lineBreakIndex)); - result.append(errorBuffer); - errorBuffer = new StringBuilder(); - result.append(LINE_BREAK); - sourceIdx = lineBreakIndex + 1; - } - result.append(source.substring(sourceIdx)); - result.append(errorBuffer); - return result.toString(); - } - - /** - * Searches an object for the given tag. First checks local tags. If not found then searches this tag in the required sources. - * - * @param tag - * Tag - * @return EObject or {@code null} - */ - protected EObject getObjectForTag(final int tag) { - EObject object = getMarkerTagsInfo().getModel(tag); - if (object == null) { - // Not in source under test - String sourceName = getMarkerTagsInfo().getSource(tag); - if (sourceName != null) { - INode node = NodeModelUtils.findActualNodeFor(getTestSource(sourceName).getModel()); - INode leafNode = NodeModelUtils.findLeafNodeAtOffset(node, getMarkerTagsInfo().getOffset(tag)); - object = NodeModelUtils.findActualSemanticObjectFor(leafNode); - } - } - assertNotNull(object, "Tag " + tag + " should mark an object. Use «mark(TAG)» in a code snippet."); //$NON-NLS-1$//$NON-NLS-2$ - return object; - } - - /** - * Return the offset for the given tag. - * - * @param tag - * The tag for which to find the offset - * @return the offset found or {@code null} if the given tag is not marking an object - */ - protected Integer getOffsetForTag(final int tag) { - Integer offset = getMarkerTagsInfo().getOffset(tag); - assertNotNull(offset, "Tag " + tag + " should mark an object. Use «mark(TAG)» in a code snippet."); //$NON-NLS-1$//$NON-NLS-2$ - return offset; - } - - /** - * Generate a unique tag id. - *

- * Use this method for local tags only. Global tags must use @Tag annotation to ensure the same value over multiple instances of the test class. - *

- * - * @return Tag id - */ - public int getTag() { - localMarkerIdCounter++; - assertTrue(localMarkerIdCounter < TagCompilationParticipant.COUNTER_BASE, "Too many local tags. Must be less than " + TagCompilationParticipant.COUNTER_BASE //$NON-NLS-1$ - + " per test method"); //$NON-NLS-1$ - return localMarkerIdCounter; - } - - /** - * Return the {@link MarkerTagsInfo} associated to one test class. - * - * @return the associated {@link MarkerTagsInfo} - */ - protected MarkerTagsInfo getMarkerTagsInfo() { - MarkerTagsInfo info = (MarkerTagsInfo) getTestInformation().getTestObject(MarkerTagsInfo.class); - if (info == null) { - info = new MarkerTagsInfo(); - getTestInformation().putTestObject(MarkerTagsInfo.class, info); - } - return info; - } - - /** - * This class preserves information about tags in the required sources - * for all tests within one test class. Tags for current test are also stored here. - * One may prefer in the future to be able to clean tags for the current test after the test. - */ - protected class MarkerTagsInfo { - - // For sources under test - /** The tag to model. */ - private final Map tagToModel = Maps.newHashMap(); - - /** The tag to cross reference. */ - private final Map tagToCrossReference = Maps.newHashMap(); - - // For sources added early (only object referencing, no need to search cross references) - /** The tag to source. */ - private final Map tagToSource = Maps.newHashMap(); - - /** The tag to offset. */ - private final Map tagToOffset = Maps.newHashMap(); - - private boolean invalidTestClass; - - /** - * Registers one tag for the source under test. - * This source is actually used for testing and we need both: outgoing cross references as well as declared elements. - * - * @param tag - * New id for the tag - * @param context - * Current object that will correspond to this tag - * @param crossReference - * the cross reference {@code null} for declaration or the corresponding cross reference in the grammar for a reference - */ - public void registerLocalTag(final int tag, final EObject context, final CrossReference crossReference) { - tagToModel.put(tag, context); - if (crossReference != null) { - tagToCrossReference.put(tag, crossReference); - } - } - - /** - * Register a tag in the required source. Only declarations are supported. - * - * @param tag - * New tag (must be unique) - * @param sourceName - * Source name - * @param offset - * Offset within the source - */ - public void registerRequiredSourceTag(final int tag, final String sourceName, final int offset) { - tagToSource.put(tag, sourceName); - tagToOffset.put(tag, offset); - } - - /** - * Returns the context model element for local tags. - * - * @param tag - * Tag - * @return Model element - */ - public EObject getModel(final int tag) { - return tagToModel.get(tag); - } - - /** - * Returns cross references for local tags. - * - * @param tag - * Tag - * @return Cross reference grammar element - */ - public CrossReference getCrossReference(final int tag) { - return tagToCrossReference.get(tag); - } - - /** - * Returns source name for global tag. - * - * @param tag - * Tag - * @return Source name - */ - public String getSource(final int tag) { - return tagToSource.get(tag); - } - - /** - * Returns offsets for global tags. - * - * @param tag - * Tag - * @return Offset - */ - public Integer getOffset(final int tag) { - return tagToOffset.get(tag); - } - - /** - * Clear local tags from current test. Clears all tags up to the id passed, but not outside the range reserved for local tags. - * - * @param maxId - * the tag id - */ - public void clearTags(final long maxId) { - for (int i = 1; i <= Math.min(maxId, TagCompilationParticipant.COUNTER_BASE - 1); i++) { - tagToCrossReference.remove(i); - tagToModel.remove(i); - tagToOffset.remove(i); - tagToSource.remove(i); - } - } - - public boolean isInvalidTestClass() { - return invalidTestClass; - } - - /** - * Marks the current test class as invalid. All tests within this class will report same error. - */ - public void setTestClassInvalid() { - this.invalidTestClass = true; - } - } - - /** - * Before apply assertions. - * - * @param testSource - * the test source, not {@code null} - */ - protected void beforeApplyAssertions(final XtextTestSource testSource) { - } - - /** - * Gets additional error message information. - * - * @return additional error message information, never {@code null} - */ - protected String getAdditionalErrorMessageInformation() { - return ""; - } - - /** - * Returns an unmodifiable view of the tags generated by {@link getTag()}. - * - * @return the unmodifiable view of the {@link #usedTags} set - */ - public Set getUsedTagsItems() { - return Collections.unmodifiableSet(usedTags); - } - - /** - * Processing after validations. - */ - protected void afterValidate() { - } -} +/******************************************************************************* + * 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.CrossReference; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.xbase.lib.Procedures; + +import com.avaloq.tools.ddk.xtext.test.TagCompilationParticipant; +import com.avaloq.tools.ddk.xtext.test.XtextTestSource; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + + +/** + * Abstract class that supports Xtend based test implementations to use markers in the test sources. + */ +@SuppressWarnings("nls") +public abstract class AbstractXtextMarkerBasedTest extends AbstractXtextTest { + + private static final String INVALID_TEST_CONFIGURATION = "Invalid test configuration. Missing org.eclipse.xtend.lib in MANIFEST.MF in this plugin?"; //$NON-NLS-1$ + private static final String LINE_BREAK = "\n"; + private static final String MARKER_START_GUARD = "##"; + private static final String MARKER_END_GUARD = "#"; + private static final String SPLITTING_LINE = "-------------------------------------------------\n"; + protected static final Pattern PATTERN = Pattern.compile(MARKER_START_GUARD + "(\\d+)" + MARKER_END_GUARD); + + /** The tag id. */ + private int localMarkerIdCounter; + + private final Map assertions = Maps.newHashMap(); + private final SortedMap errorsOnPosition = Maps.newTreeMap(); + /** Used Tags to find Duplicates. */ + private final Set usedTags = Sets.newHashSet(); + + /** + * Indicates if a testing source is a kernel or customer source. + */ + protected enum TestSourceType { + CLIENT_ALL, + CLIENT_CUSTOMER + } + + // -------------------------------------------------------------------------- + // AbstractModelAssertion + // -------------------------------------------------------------------------- + + /** + * Interface for testing assertions on a given source position. + */ + protected abstract class AbstractModelAssertion implements Procedures.Procedure2 { + + @Override + public abstract void apply(EObject semanticModel, Integer pos); + + } + + // -------------------------------------------------------------------------- + // Methods of testing framework + // -------------------------------------------------------------------------- + + @Override + protected void afterEachTest() { + getMarkerTagsInfo().clearTags(localMarkerIdCounter); + super.afterEachTest(); + assertions.clear(); + usedTags.clear(); + errorsOnPosition.clear(); + } + + @Override + protected void beforeEachTest() { + localMarkerIdCounter = 0; + super.beforeEachTest(); + assertFalse(getMarkerTagsInfo().isInvalidTestClass(), INVALID_TEST_CONFIGURATION); + } + + // -------------------------------------------------------------------------- + // Methods to be used by the actual testing classes + // -------------------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + String processedContent = processContentAndRegisterOffsets(sourceFileName, sourceContent); + super.addKernelSourceToWorkspace(sourceFileName, processedContent); + } + + /** + * {@inheritDoc} + */ + @Override + protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + String processedContent = processContentAndRegisterOffsets(CUSTOMER_SOURCE_PREFIX + sourceFileName, sourceContent); + super.addCustomerSourceToWorkspace(sourceFileName, processedContent); + } + + /** + * Removes tags and stores them in the test info object. + * + * @param sourceFileName + * Source file name, must not be {@code null} + * @param sourceContent + * Content of the test source (may contain tags), must not be {@code null} + * @return Content without tags, never {@code null} + */ + private String processContentAndRegisterOffsets(final String sourceFileName, final CharSequence sourceContent) { + Map offsets = Maps.newHashMap(); + String content = removeMarkersFromContent(sourceContent, offsets); + for (Entry tag : offsets.entrySet()) { + getMarkerTagsInfo().registerRequiredSourceTag(tag.getKey(), sourceFileName, tag.getValue()); + } + return content; + } + + /** + * Removes the Xtend markers from a source. + * + * @param sourceContent + * the source content, not {@code null} + * @param tagToOffset + * Map to be populated with tag to offset pairs, must not be {@code null} + * @return the content without markers, never {@code null} + */ + private String removeMarkersFromContent(final CharSequence sourceContent, final Map tagToOffset) { + StringBuffer withoutMarkers = new StringBuffer(sourceContent.length()); + Matcher m = PATTERN.matcher(sourceContent); + while (m.find()) { + m.appendReplacement(withoutMarkers, ""); + tagToOffset.put(Integer.parseInt(m.group(1)), withoutMarkers.length()); + } + m.appendTail(withoutMarkers); + return withoutMarkers.toString(); + } + + /** + * Returns the model for the given source name and string. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, must not be {@code null} + * @param sourceContent + * source, must not be {@code null} + * @return Model element for the parsed source, may be {@code null} + */ + protected EObject getModel(final String sourceFileName, final CharSequence sourceContent) { + Map offsets = Maps.newHashMap(); + String content = removeMarkersFromContent(sourceContent, offsets); + EObject root = null; + try { + root = getXtextTestUtil().getModel(sourceFileName, content); + INode node = NodeModelUtils.getNode(root); + for (Entry tag : offsets.entrySet()) { + INode leafNode = NodeModelUtils.findLeafNodeAtOffset(node, tag.getValue() + 1); + EObject context = NodeModelUtils.findActualSemanticObjectFor(leafNode); + // Search for cross reference + CrossReference crossReference = null; + while (leafNode != null) { + if (leafNode.getGrammarElement() instanceof CrossReference) { + crossReference = (CrossReference) leafNode.getGrammarElement(); + break; + } + leafNode = leafNode.getParent(); + } + getMarkerTagsInfo().registerLocalTag(tag.getKey(), context, crossReference); + } + } catch (IOException e) { + fail("Exception while creating model from input string: " + e.getMessage()); //$NON-NLS-1$ + } + return root; + } + + /** + * Does the same as get model, but returns void. + * + * @param sourceFileName + * the file name that should be associated with the parsed content + * @param sourceContent + * source + */ + protected void registerModel(final String sourceFileName, final CharSequence sourceContent) { + getModel(sourceFileName, sourceContent); + } + + /** + * Validate a kernel source given by a file name and content. + * All not expected diagnostics are ignored. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, not {@code null} + * @param sourceContent + * source, not {@code null} + */ + protected void validateKernelSource(final String sourceFileName, final CharSequence sourceContent) { + validate(sourceFileName, TestSourceType.CLIENT_ALL, sourceContent); + } + + /** + * Validate a customer source given by a file name and content. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, not {@code null} + * @param sourceContent + * source, not {@code null} + */ + protected void validateCustomerSource(final String sourceFileName, final CharSequence sourceContent) { + validate(sourceFileName, TestSourceType.CLIENT_CUSTOMER, sourceContent); + } + + // -------------------------------------------------------------------------- + // Methods to be used by more specific abstract classes + // -------------------------------------------------------------------------- + + /** + * Add assertion to the list of assertions and return the corresponding string marker. + * + * @param assertion + * assertion to be added, not {@code null} + * @return + * string marker corresponding to the added assertion + */ + protected String addAssertion(final AbstractModelAssertion assertion) { + // Assertions are given a local tag as they are local markers affecting and declared in only one source + Integer markerId = getTag(); + assertions.put(markerId, assertion); + return mark(markerId); + } + + /** + * Creates a mark with the given id. Use this method in tests to insert marks. + *

+ * If the given mark id is 0 then it means that the test was not well configured. All the id-s that are generated by {@link #getTag} start with 1, all the + * global ids start with {@value com.avaloq.tools.asmd.testbase.scoping.TagCompilationParticipant#COUNTER_BASE}. Since it is unlikely to get local ids wrong, + * the most common reason for global ids to be wrong is that they were not initialized. Global ids get initialized with active annotation + * {@link com.avaloq.tools.asmd.testbase.scoping.Tag} that executes {@link com.avaloq.tools.asmd.testbase.scoping.TagCompilationParticipant}. Active + * annotations do not get executed if {@code org.eclipse.xtend.lib} is missing in {@code MANIFEST.MF}. Thus we report this common mistake to the user via an + * assertion. + *

+ * + * @param id + * Mark id + * @return Mark text to be inserted in the source file, never {@code null} + */ + protected String mark(final int id) { + assertFalse(usedTags.contains(id), "Tag with " + id + " used to mark more than one location."); //$NON-NLS-1$ //$NON-NLS-2$ + usedTags.add(id); + if (id < 1) { + getMarkerTagsInfo().setTestClassInvalid(); + throw new AssertionError(INVALID_TEST_CONFIGURATION); + } + return MARKER_START_GUARD + id + MARKER_END_GUARD; + } + + /** + * Memorize an error that was detected during the validation of the current testing file. + * + * @param position + * position of the error in file, not {@code null} + * @param error + * string with error, not {@code null} + */ + protected void memorizeErrorOnPosition(final Integer position, final String error) { + if (!errorsOnPosition.containsKey(position)) { + errorsOnPosition.put(position, new StringBuilder()); + } + errorsOnPosition.get(position).append(error); + } + + /** + * Processes all the inserted markers in a given source and creates a new {@link XtextTestSource} without the markers. + * + * @param sourceFileName + * the name of the source, may be {@code null} + * @param sourceType + * the type of the source, may be {@code null} + * @param sourceContent + * the content of the source, must not be {@code null} + * @return the {@link XtextTestSource} created. + */ + protected XtextTestSource processMarkers(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { + StringBuilder withoutMarkers = new StringBuilder(); + final Multimap positionToAssertionMap = LinkedHashMultimap.create(); + Matcher m = PATTERN.matcher(sourceContent); + int lastEnd = 0; + while (m.find()) { + withoutMarkers.append(sourceContent.subSequence(lastEnd, m.start())); + lastEnd = m.end(); + int markerId = Integer.parseInt(m.group(1)); + // save the position of the marker only if we are dealing with an assertion marker + AbstractModelAssertion assertionMarker = assertions.get(markerId); + if (assertionMarker != null) { + positionToAssertionMap.put(withoutMarkers.length(), assertionMarker); + } + } + // Add the rest part of input string + withoutMarkers.append(sourceContent.subSequence(lastEnd, sourceContent.length())); + + // Calculate source name + String fullSourceFileName = ""; + if (sourceType == TestSourceType.CLIENT_CUSTOMER) { + fullSourceFileName = CUSTOMER_SOURCE_PREFIX; + } + fullSourceFileName = fullSourceFileName.concat(sourceFileName); + + EObject semanticModel; + String withoutMarkersAsString = withoutMarkers.toString(); + XtextTestSource testSource = createTestSource(fullSourceFileName, withoutMarkersAsString); + semanticModel = testSource.getModel(); + beforeApplyAssertions(testSource); + // Run validations on markers + for (Map.Entry entry : positionToAssertionMap.entries()) { + entry.getValue().apply(semanticModel, entry.getKey()); + } + return testSource; + } + + /** + * Validate a source given by a file name and content. + * + * @param sourceFileName + * the file name that should be associated with the parsed content, not {@code null} + * @param sourceType + * defines if the source is a kernel or customer source, not {@code null} + * @param sourceContent + * source, not {@code null} + */ + protected void validate(final String sourceFileName, final TestSourceType sourceType, final CharSequence sourceContent) { + XtextTestSource testSource = processMarkers(sourceFileName, sourceType, sourceContent); + processErrorsFound(testSource.getContent()); + afterValidate(); + } + + /** + * Processes all the diagnostics in a given source. + * + * @param sourceWithoutMarkers + * the source to process, must not be {@code null} + */ + protected void processErrorsFound(final String sourceWithoutMarkers) { + if (!errorsOnPosition.isEmpty()) { + // CHECKSTYLE:OFF MagicNumber + StringBuilder sb = new StringBuilder(50); + // CHECKSTYLE:ON + sb.append(memorizedErrorsToString(sourceWithoutMarkers)); + sb.append(SPLITTING_LINE); + sb.append("List of all found diagnostics:\n"); + sb.append(getAdditionalErrorMessageInformation()); + assertEquals(sourceWithoutMarkers, sb.toString(), "Errors found. Consider compare view."); + } + } + + /** + * Inject memorized errors into the input file on positions where they were detected. + * + * @param source + * text of the input testing source, not {@code null} + * @return + * input testing source with injected errors, never {@code null} + */ + private String memorizedErrorsToString(final String source) { + StringBuilder result = new StringBuilder(); + StringBuilder errorBuffer = new StringBuilder(); + // Sort positions + List positions = Lists.newArrayList(errorsOnPosition.keySet()); + + int posIdx = 0; + int sourceIdx = 0; + + while (sourceIdx < source.length()) { + int lineBreakIndex = source.indexOf('\n', sourceIdx); + if (lineBreakIndex < 0) { + lineBreakIndex = source.length(); + } + while (posIdx < positions.size() && positions.get(posIdx) < lineBreakIndex) { + int nextPos = positions.get(posIdx); + result.append(source.substring(sourceIdx, nextPos)); + result.append("'); + // Add error message to buffer + errorBuffer.append(SPLITTING_LINE); + errorBuffer.append("FAILURE "); + errorBuffer.append(posIdx + 1); + errorBuffer.append(": "); + errorBuffer.append(errorsOnPosition.get(nextPos)); + sourceIdx = nextPos; + posIdx++; + } + if (errorBuffer.length() > 0) { + errorBuffer.append(SPLITTING_LINE); + } + result.append(source.substring(sourceIdx, lineBreakIndex)); + result.append(errorBuffer); + errorBuffer = new StringBuilder(); + result.append(LINE_BREAK); + sourceIdx = lineBreakIndex + 1; + } + result.append(source.substring(sourceIdx)); + result.append(errorBuffer); + return result.toString(); + } + + /** + * Searches an object for the given tag. First checks local tags. If not found then searches this tag in the required sources. + * + * @param tag + * Tag + * @return EObject or {@code null} + */ + protected EObject getObjectForTag(final int tag) { + EObject object = getMarkerTagsInfo().getModel(tag); + if (object == null) { + // Not in source under test + String sourceName = getMarkerTagsInfo().getSource(tag); + if (sourceName != null) { + INode node = NodeModelUtils.findActualNodeFor(getTestSource(sourceName).getModel()); + INode leafNode = NodeModelUtils.findLeafNodeAtOffset(node, getMarkerTagsInfo().getOffset(tag)); + object = NodeModelUtils.findActualSemanticObjectFor(leafNode); + } + } + assertNotNull(object, "Tag " + tag + " should mark an object. Use «mark(TAG)» in a code snippet."); //$NON-NLS-1$//$NON-NLS-2$ + return object; + } + + /** + * Return the offset for the given tag. + * + * @param tag + * The tag for which to find the offset + * @return the offset found or {@code null} if the given tag is not marking an object + */ + protected Integer getOffsetForTag(final int tag) { + Integer offset = getMarkerTagsInfo().getOffset(tag); + assertNotNull(offset, "Tag " + tag + " should mark an object. Use «mark(TAG)» in a code snippet."); //$NON-NLS-1$//$NON-NLS-2$ + return offset; + } + + /** + * Generate a unique tag id. + *

+ * Use this method for local tags only. Global tags must use @Tag annotation to ensure the same value over multiple instances of the test class. + *

+ * + * @return Tag id + */ + public int getTag() { + localMarkerIdCounter++; + assertTrue(localMarkerIdCounter < TagCompilationParticipant.COUNTER_BASE, "Too many local tags. Must be less than " + TagCompilationParticipant.COUNTER_BASE //$NON-NLS-1$ + + " per test method"); //$NON-NLS-1$ + return localMarkerIdCounter; + } + + /** + * Return the {@link MarkerTagsInfo} associated to one test class. + * + * @return the associated {@link MarkerTagsInfo} + */ + protected MarkerTagsInfo getMarkerTagsInfo() { + MarkerTagsInfo info = (MarkerTagsInfo) getTestInformation().getTestObject(MarkerTagsInfo.class); + if (info == null) { + info = new MarkerTagsInfo(); + getTestInformation().putTestObject(MarkerTagsInfo.class, info); + } + return info; + } + + /** + * This class preserves information about tags in the required sources + * for all tests within one test class. Tags for current test are also stored here. + * One may prefer in the future to be able to clean tags for the current test after the test. + */ + protected class MarkerTagsInfo { + + // For sources under test + /** The tag to model. */ + private final Map tagToModel = Maps.newHashMap(); + + /** The tag to cross reference. */ + private final Map tagToCrossReference = Maps.newHashMap(); + + // For sources added early (only object referencing, no need to search cross references) + /** The tag to source. */ + private final Map tagToSource = Maps.newHashMap(); + + /** The tag to offset. */ + private final Map tagToOffset = Maps.newHashMap(); + + private boolean invalidTestClass; + + /** + * Registers one tag for the source under test. + * This source is actually used for testing and we need both: outgoing cross references as well as declared elements. + * + * @param tag + * New id for the tag + * @param context + * Current object that will correspond to this tag + * @param crossReference + * the cross reference {@code null} for declaration or the corresponding cross reference in the grammar for a reference + */ + public void registerLocalTag(final int tag, final EObject context, final CrossReference crossReference) { + tagToModel.put(tag, context); + if (crossReference != null) { + tagToCrossReference.put(tag, crossReference); + } + } + + /** + * Register a tag in the required source. Only declarations are supported. + * + * @param tag + * New tag (must be unique) + * @param sourceName + * Source name + * @param offset + * Offset within the source + */ + public void registerRequiredSourceTag(final int tag, final String sourceName, final int offset) { + tagToSource.put(tag, sourceName); + tagToOffset.put(tag, offset); + } + + /** + * Returns the context model element for local tags. + * + * @param tag + * Tag + * @return Model element + */ + public EObject getModel(final int tag) { + return tagToModel.get(tag); + } + + /** + * Returns cross references for local tags. + * + * @param tag + * Tag + * @return Cross reference grammar element + */ + public CrossReference getCrossReference(final int tag) { + return tagToCrossReference.get(tag); + } + + /** + * Returns source name for global tag. + * + * @param tag + * Tag + * @return Source name + */ + public String getSource(final int tag) { + return tagToSource.get(tag); + } + + /** + * Returns offsets for global tags. + * + * @param tag + * Tag + * @return Offset + */ + public Integer getOffset(final int tag) { + return tagToOffset.get(tag); + } + + /** + * Clear local tags from current test. Clears all tags up to the id passed, but not outside the range reserved for local tags. + * + * @param maxId + * the tag id + */ + public void clearTags(final long maxId) { + for (int i = 1; i <= Math.min(maxId, TagCompilationParticipant.COUNTER_BASE - 1); i++) { + tagToCrossReference.remove(i); + tagToModel.remove(i); + tagToOffset.remove(i); + tagToSource.remove(i); + } + } + + public boolean isInvalidTestClass() { + return invalidTestClass; + } + + /** + * Marks the current test class as invalid. All tests within this class will report same error. + */ + public void setTestClassInvalid() { + this.invalidTestClass = true; + } + } + + /** + * Before apply assertions. + * + * @param testSource + * the test source, not {@code null} + */ + protected void beforeApplyAssertions(final XtextTestSource testSource) { + } + + /** + * Gets additional error message information. + * + * @return additional error message information, never {@code null} + */ + protected String getAdditionalErrorMessageInformation() { + return ""; + } + + /** + * Returns an unmodifiable view of the tags generated by {@link getTag()}. + * + * @return the unmodifiable view of the {@link #usedTags} set + */ + public Set getUsedTagsItems() { + return Collections.unmodifiableSet(usedTags); + } + + /** + * Processing after validations. + */ + protected void afterValidate() { + } +} 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 5f843bc4e3..ebd4473987 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 @@ -1,113 +1,113 @@ -/******************************************************************************* - * 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 org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.resource.XtextResource; - -import com.avaloq.tools.ddk.xtext.test.XtextTestSource; - - -/** - * Provides a test class specific custom test framework for xtext languages. Provides a language specific {@link AbstractXtextTestUtil}. - * All exceptions are wrapped and handed over to the JUnit framework. - */ -public abstract class AbstractXtextTest extends AbstractTest { - - /** - * Returns a language specific {@link AbstractXtextTestUtil}. Test classes specify which TestUtil to use by implementing this method. - * - * @return a language specific {@link AbstractXtextTestUtil} - */ - protected abstract AbstractXtextTestUtil getXtextTestUtil(); - - @Override - protected final AbstractXtextTestUtil getTestUtil() { - return getXtextTestUtil(); - } - - @Override - protected XtextTestSource getTestSource() { - return (XtextTestSource) super.getTestSource(); - } - - @Override - protected XtextTestSource createTestSource(final String sourceFileName, final String content) { - XtextTestSource testSource = new XtextTestSource(sourceFileName, content, getTestUtil().getResourceSet()); - getTestProjectManager().addSourceToProject(testSource); - return testSource; - } - - /** - * The default implementation returns the name of the source model calling {@link getTestSourceModelName} and adds the default file extension for the grammar - * of this test. A test class needs to override this, if the name of the main test source file differs from the default. - * - * @return the name of the main test source file - */ - @Override - protected String getTestSourceFileName() { - return this.getTestSourceModelName() + '.' + getXtextTestUtil().getFileExtension(); - } - - /** - * Returns the xtext resource loaded by {@link loadPrimarySource}. - * - * @return - * the xtext resource loaded by {@link loadPrimarySource}. - */ - protected XtextResource getXtextTestResource() { - return getTestSource(getTestSourceFileName()).getXtextResource(); - } - - /** - * Returns the semantic model from the xtext resource loaded by {@link loadPrimarySource}. - * - * @return - * the semantic model from the xtext resource loaded by {@link loadPrimarySource}. - */ - protected EObject getSemanticModel() { - return getXtextTestResource().getParseResult().getRootASTElement(); - } - - /** - * {@inheritDoc} - */ - @Override - protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - refreshSourceContent(sourceFileName, sourceContent.toString()); - super.addKernelSourceToWorkspace(sourceFileName, sourceContent); - } - - /** - * {@inheritDoc} - */ - @Override - protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { - refreshSourceContent(CUSTOMER_SOURCE_PREFIX + sourceFileName, sourceContent.toString()); - super.addCustomerSourceToWorkspace(sourceFileName, sourceContent); - } - - /** - * Refresh the source content if it had already been added previously to the workspace. - * - * @param sourceFileName - * the source file name - * @param content - * the content - */ - private void refreshSourceContent(final String sourceFileName, final String content) { - XtextTestSource xtextTestSource = getTestSource(sourceFileName); - if (xtextTestSource != null) { - xtextTestSource.setContent(content); - } - } - -} +/******************************************************************************* + * 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 org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.resource.XtextResource; + +import com.avaloq.tools.ddk.xtext.test.XtextTestSource; + + +/** + * Provides a test class specific custom test framework for xtext languages. Provides a language specific {@link AbstractXtextTestUtil}. + * All exceptions are wrapped and handed over to the JUnit framework. + */ +public abstract class AbstractXtextTest extends AbstractTest { + + /** + * Returns a language specific {@link AbstractXtextTestUtil}. Test classes specify which TestUtil to use by implementing this method. + * + * @return a language specific {@link AbstractXtextTestUtil} + */ + protected abstract AbstractXtextTestUtil getXtextTestUtil(); + + @Override + protected final AbstractXtextTestUtil getTestUtil() { + return getXtextTestUtil(); + } + + @Override + protected XtextTestSource getTestSource() { + return (XtextTestSource) super.getTestSource(); + } + + @Override + protected XtextTestSource createTestSource(final String sourceFileName, final String content) { + XtextTestSource testSource = new XtextTestSource(sourceFileName, content, getTestUtil().getResourceSet()); + getTestProjectManager().addSourceToProject(testSource); + return testSource; + } + + /** + * The default implementation returns the name of the source model calling {@link getTestSourceModelName} and adds the default file extension for the grammar + * of this test. A test class needs to override this, if the name of the main test source file differs from the default. + * + * @return the name of the main test source file + */ + @Override + protected String getTestSourceFileName() { + return this.getTestSourceModelName() + '.' + getXtextTestUtil().getFileExtension(); + } + + /** + * Returns the xtext resource loaded by {@link loadPrimarySource}. + * + * @return + * the xtext resource loaded by {@link loadPrimarySource}. + */ + protected XtextResource getXtextTestResource() { + return getTestSource(getTestSourceFileName()).getXtextResource(); + } + + /** + * Returns the semantic model from the xtext resource loaded by {@link loadPrimarySource}. + * + * @return + * the semantic model from the xtext resource loaded by {@link loadPrimarySource}. + */ + protected EObject getSemanticModel() { + return getXtextTestResource().getParseResult().getRootASTElement(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void addKernelSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + refreshSourceContent(sourceFileName, sourceContent.toString()); + super.addKernelSourceToWorkspace(sourceFileName, sourceContent); + } + + /** + * {@inheritDoc} + */ + @Override + protected void addCustomerSourceToWorkspace(final String sourceFileName, final CharSequence sourceContent) { + refreshSourceContent(CUSTOMER_SOURCE_PREFIX + sourceFileName, sourceContent.toString()); + super.addCustomerSourceToWorkspace(sourceFileName, sourceContent); + } + + /** + * Refresh the source content if it had already been added previously to the workspace. + * + * @param sourceFileName + * the source file name + * @param content + * the content + */ + private void refreshSourceContent(final String sourceFileName, final String content) { + XtextTestSource xtextTestSource = getTestSource(sourceFileName); + if (xtextTestSource != null) { + xtextTestSource.setContent(content); + } + } + +} 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 index 7a28733d39..1cfcb716cb 100644 --- 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 @@ -1,408 +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; - } -} +/******************************************************************************* + * 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; + } +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/builder/XtextBuilderInjectorProvider.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/builder/XtextBuilderInjectorProvider.java index 5ef2d3250c..029139723d 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/builder/XtextBuilderInjectorProvider.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/builder/XtextBuilderInjectorProvider.java @@ -1,39 +1,39 @@ -/******************************************************************************* - * Copyright (c) 2016 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 Evolution AG - initial API and implementation - *******************************************************************************/ - -package com.avaloq.tools.ddk.xtext.builder; - -import static org.mockito.Mockito.mock; - -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.xtext.builder.impl.BuildScheduler; -import org.eclipse.xtext.testing.IInjectorProvider; - -import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Injector; - - -public class XtextBuilderInjectorProvider implements IInjectorProvider { - - @Override - public Injector getInjector() { - return Guice.createInjector(new AbstractModule() { - @SuppressWarnings({"deprecation", "restriction"}) - @Override - protected void configure() { - bind(BuildScheduler.class).toInstance(mock(BuildScheduler.class)); - bind(IWorkspace.class).toInstance(mock(IWorkspace.class)); - } - }); - } - -} +/******************************************************************************* + * Copyright (c) 2016 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 Evolution AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.xtext.builder; + +import static org.mockito.Mockito.mock; + +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.xtext.builder.impl.BuildScheduler; +import org.eclipse.xtext.testing.IInjectorProvider; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; + + +public class XtextBuilderInjectorProvider implements IInjectorProvider { + + @Override + public Injector getInjector() { + return Guice.createInjector(new AbstractModule() { + @SuppressWarnings({"deprecation", "restriction"}) + @Override + protected void configure() { + bind(BuildScheduler.class).toInstance(mock(BuildScheduler.class)); + bind(IWorkspace.class).toInstance(mock(IWorkspace.class)); + } + }); + } + +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/AbstractFormatterTest.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/AbstractFormatterTest.java index 3898446b3f..59d425a0d5 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/AbstractFormatterTest.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/AbstractFormatterTest.java @@ -1,76 +1,76 @@ -/******************************************************************************* - * 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.jupiter.formatter; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.formatting.INodeModelFormatter; -import org.eclipse.xtext.formatting.INodeModelFormatter.IFormattedRegion; -import org.eclipse.xtext.nodemodel.ICompositeNode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.resource.SaveOptions; -import org.eclipse.xtext.serializer.ISerializer; - -import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; - - -/** - * Base class for formatting tests. - * The assertXyz methods are essentially copied from {@link com.avaloq.tools.ddk.xtext.test.formatting.AbstractAcfFormattingTest}. - */ -@SuppressWarnings("nls") -public abstract class AbstractFormatterTest extends AbstractXtextTest { - - /** - * Loads a model from a string representation of a source. - * - * @param input - * String representing a serialized model - * @return Loaded model - */ - private EObject getModel(final String input) throws IOException { - return getXtextTestUtil().getModel("mytestmodel." + getXtextTestUtil().getFileExtension(), input); - } - - /** - * Gets the Guice injected serializer. - * - * @return Serializer the DI serializer - */ - protected ISerializer getSerializer() { - return getXtextTestUtil().getSerializer(); - } - - // test formatting based on the ParseTreeConstructorin - protected void assertFormattedPTC(final String expected, final String model) throws IOException { - EObject m = getModel(model); - String res = getSerializer().serialize(m, SaveOptions.newBuilder().format().getOptions()); - assertEquals(expected, res, "Serialization not equal"); - } - - protected void assertPreserved(final String model) throws IOException { - EObject m = getModel(model); - String res = getSerializer().serialize(m, SaveOptions.newBuilder().getOptions()); - assertEquals(model, res, "Preserved node model"); - } - - // test formatting based on the NodeModel - protected void assertFormattedNM(final String expected, final String model, final int offset, final int length) throws IOException { - ICompositeNode node = NodeModelUtils.getNode(getModel(model)).getRootNode(); - IFormattedRegion r = getXtextTestUtil().get(INodeModelFormatter.class).format(node, offset, length); - String actual = model.substring(0, r.getOffset()) + r.getFormattedText() + model.substring(r.getLength() + r.getOffset()); - assertEquals(expected, actual, "Formatting based on the NodeModel"); - } - -} +/******************************************************************************* + * 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.jupiter.formatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.formatting.INodeModelFormatter; +import org.eclipse.xtext.formatting.INodeModelFormatter.IFormattedRegion; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.SaveOptions; +import org.eclipse.xtext.serializer.ISerializer; + +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; + + +/** + * Base class for formatting tests. + * The assertXyz methods are essentially copied from {@link com.avaloq.tools.ddk.xtext.test.formatting.AbstractAcfFormattingTest}. + */ +@SuppressWarnings("nls") +public abstract class AbstractFormatterTest extends AbstractXtextTest { + + /** + * Loads a model from a string representation of a source. + * + * @param input + * String representing a serialized model + * @return Loaded model + */ + private EObject getModel(final String input) throws IOException { + return getXtextTestUtil().getModel("mytestmodel." + getXtextTestUtil().getFileExtension(), input); + } + + /** + * Gets the Guice injected serializer. + * + * @return Serializer the DI serializer + */ + protected ISerializer getSerializer() { + return getXtextTestUtil().getSerializer(); + } + + // test formatting based on the ParseTreeConstructorin + protected void assertFormattedPTC(final String expected, final String model) throws IOException { + EObject m = getModel(model); + String res = getSerializer().serialize(m, SaveOptions.newBuilder().format().getOptions()); + assertEquals(expected, res, "Serialization not equal"); + } + + protected void assertPreserved(final String model) throws IOException { + EObject m = getModel(model); + String res = getSerializer().serialize(m, SaveOptions.newBuilder().getOptions()); + assertEquals(model, res, "Preserved node model"); + } + + // test formatting based on the NodeModel + protected void assertFormattedNM(final String expected, final String model, final int offset, final int length) throws IOException { + ICompositeNode node = NodeModelUtils.getNode(getModel(model)).getRootNode(); + IFormattedRegion r = getXtextTestUtil().get(INodeModelFormatter.class).format(node, offset, length); + String actual = model.substring(0, r.getOffset()) + r.getFormattedText() + model.substring(r.getLength() + r.getOffset()); + assertEquals(expected, actual, "Formatting based on the NodeModel"); + } + +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTest.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTest.java index 7398c4366c..63fb4f7a4c 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTest.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTest.java @@ -1,507 +1,507 @@ -/******************************************************************************* - * 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.jupiter.formatter; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; - -import org.eclipse.xtext.resource.SaveOptions; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Decl; -import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.FormatterTestLanguageFactory; -import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.TestLinewrapMinMax; - - -/** - * This class tests the Acs Formatter framework. The tests are basically a copy - * of the Xtext Formatter tests. - */ -@SuppressWarnings("nls") -public class FormatterTest extends AbstractFormatterTest { - @Override - protected FormatterTestUtil getXtextTestUtil() { - return FormatterTestUtil.getInstance(); - } - - /** - * This test class does not have a test source file. {@inheritDoc} - */ - @Override - protected String getTestSourceFileName() { - return null; - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrap() throws IOException { - String model = "test linewrap float val; int x; double y;"; - String expected = "test linewrap\nfloat val;\nint x;\ndouble y;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void keepComments() throws IOException { - // String model = "test linewrap float val; int x; double y;"; - String model = "// begincomment \ntest linewrap// comment1\n" + "float val;//comment2\n" + "int x;" + "double y; //yoyoyo!\n// endcomment."; - final String exp = "// begincomment \ntest linewrap// comment1\n" + "float val;//comment2\n" + "int x;\n" + "double y; //yoyoyo!\n// endcomment."; - assertFormattedPTC(exp, model); - assertFormattedNM(exp, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test verifies that {@link com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Line} elements are aligned at the specified column. - * - * @throws IOException - */ - @Test - public void column() throws IOException { - String model = "test column item int x;"; - String expected = "test\n column\n\titem int x;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test verifies that elements can be aligned at column 0 and also that - * a minimum padding is always inserted. - * - * @throws IOException - */ - @Test - public void columnMinimumPadding() throws IOException { - String model = " test column name item int x;"; - String expected = "test\n column name\n\n\titem int x;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model.trim()); - } - - /** - * This test verifies that {@link com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Line} elements are aligned with the specified offset. - * - * @throws IOException - */ - @Test - public void offset() throws IOException { - String model = "test offset value v pair p1 p2"; - String expected = "test\noffset\n\tvalue v\n\t\tpair p1 p2"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test verifies right padding does pad and that there is always minimum padding " ". - * - * @throws IOException - */ - @Test - public void rightPadding() throws IOException { - String model = "test padding long_name n2;"; - String expected = "test\npadding long_name n2 ;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void indentation() throws IOException { - String model = "test indentation { float val; double y; indentation { int x; } }"; - String expected = "test indentation {\n float val;\n double y;\n indentation {\n int x;\n }\n}"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void association() throws IOException { - String model = "test indentation { var = [0,1,2,3,4]; }"; - String expected = "test indentation {\n var=[ 0, 1, 2, 3, 4 ];\n}"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void indentationAndComments() throws IOException { - String model = "test /* xxx */ indentation { float val; // some float\n double /* oo */ y; indentation { // some block\n int x; // xxx\n } } // final comment"; - String expected = "test /* xxx */ indentation {\n float val; // some float\n double /* oo */ y;\n indentation { // some block\n int x; // xxx\n }\n} // final comment"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * It has been adapted to - * - * @throws IOException - */ - @Test - public void indentationAndLineWrap() throws IOException { - String model = "test indentation { void func(x:int,y:int,s:javalangString, foo:javasqlDate, blupp:mylongtype, msads:adshdjkhsakdasdkslajdlsask, x:x, a:b, c:d ); }"; - String expected = "test indentation {\n void func(x:int,y:int,\n\t\ts:javalangString,\n\t\tfoo:javasqlDate,\n\t\tblupp:mylongtype,\n\t\tmsads:adshdjkhsakdasdkslajdlsask,\n\t\tx:x,a:b,c:d);\n}"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void between1() throws IOException { - String model = "test indentation { indentation { x x; }; }"; - String expected = "test indentation {\n indentation {\n x x;\n };\n}"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void between2() throws IOException { - String model = "test indentation { indentation { x x; } }"; - String expected = "test indentation {\n indentation {\n x x;\n }\n}"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRule() throws IOException { - String model = "test linewrap fqn ab; fqn xx.yy.zz;"; - String expected = "test linewrap\nfqn\nab;\nfqn\nxx.yy.zz;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRulePartial1() throws IOException { - String model = "test linewrap fqn ab . xx .yy .zz;"; - String expected = "test linewrap fqn ab.xx.yy.zz;"; - assertFormattedNM(expected, model, 22, 2); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRulePartial2() throws IOException { - String model = "test linewrap fqn ab . xx .yy .zz;fqn xxx;"; - String expected = "test linewrap fqn\nab.xx.yy.zz;fqn xxx;"; - assertFormattedNM(expected, model, 15, 10); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRulePartial3() throws IOException { - String model = "test linewrap fqn ab . xx .yy .zz;fqn xxx;"; - String expected = "test linewrap fqn ab.xx.yy.zz;\nfqn xxx;"; - assertFormattedNM(expected, model, 25, 12); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void formatSegment1() throws IOException { - String model = "test\nindentation {\n indentation { x x ; } }"; - String expected = "test\nindentation {\n indentation {\n x x;\n } }"; - assertFormattedNM(expected, model, 30, 18); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void formatSegment2() throws IOException { - String model = "test indentation {\n indentation { x x ; } }"; - // String expected = - // "test\nindentation {\n indentation {\n x x;\n } }"; - assertFormattedNM(model, model, 7, 10); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void formatSegment3() throws IOException { - String model = " test indentation {\n indentation { x x ; } }"; - String expected = "test indentation {\n indentation {\n x x;\n }\n}"; - assertFormattedNM(expected, model, 0, model.length()); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRuleRef1() throws IOException { - String model = "test linewrap fqn ab .cd .ef; fqnref ab. cd. ef;"; - String expected = "test linewrap\nfqn\nab.cd.ef;\nfqnref\nab.cd.ef;"; - // assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRuleRef2() throws IOException { - String model = "test linewrap fqn ab.cd.ef; fqnref ab.cd.ef;"; - String expected = "test linewrap\nfqn\nab.cd.ef;\nfqnref\nab.cd.ef;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest but it is modified - * because I think the expected behavior in the original test is wrong. - * - * @throws IOException - */ - @Test - public void linewrapDatatypeRuleComments() throws IOException { - String model = "test linewrap/* 1 */fqn/* 2 */ab.cd.ef/* 3 */;/* 4 */fqnref/* 5 */ab.cd.ef/* 6 */;/* 7 */"; - // The expected model string differs from Xtext's - - // Xtext does not expect a line wrap after the keyword "linewrap" - // Xtext does not expect a line wrap prior to fqnref assignment - // etc... - String expected = "test linewrap/* 1 */ fqn/* 2 */\nab.cd.ef/* 3 */;/* 4 */ fqnref\n/* 5 */ ab.cd.ef/* 6 */;/* 7 */"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void enumeration() throws IOException { - String model = "test linewrap enum lit1,lit2,lit3,lit1;"; - String expected = "test linewrap\nenum lit1 ,\nlit2,\nlit3,\nlit1;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=312559 - @Test - public void suppressedWhitespace() throws IOException { - String model = "test linewrap `f%%a` post;"; - String expected = "test linewrap\n`f%< b >%a` post;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - // TODO: investigate whether to include test or not - currently this test - // would fail - @Disabled - public void suppressedLinewrap() throws IOException { - String model = "test linewrap\n`foo%abcd%foo%< b\n>%abcd%foo%abcd%foo%abcd%" + "foo%abcd%foo%abcd%foo%abcd%foo%abcd%foo%abcd%foo%xx%foo%abcd%foo%abcd%" - + "foo%abcd%foo%<\nb >%foo%abcd` post;"; - assertFormattedPTC(model, model); - assertFormattedNM(model, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapMin() throws IOException { - String model = "test wrapminmax foo bar;"; - String expected = "test wrapminmax\n\nfoo bar;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapMax() throws IOException { - String model = "test wrapminmax\n\n\n\n\n\n\n\n\n\n\n\n\nfoo bar;"; - String expected = "test wrapminmax\n\n\n\n\nfoo bar;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapKeep() throws IOException { - String model = "test wrapminmax\n\n\n\nfoo bar;"; - assertFormattedPTC(model, model); - assertFormattedNM(model, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void linewrapDefault() { - FormatterTestLanguageFactory f = FormatterTestLanguageFactory.eINSTANCE; - TestLinewrapMinMax m = f.createTestLinewrapMinMax(); - Decl d = f.createDecl(); - d.getType().add("xxx"); - d.getName().add("yyy"); - m.getItems().add(d); - String actual = getSerializer().serialize(m, SaveOptions.newBuilder().format().getOptions()); - String expected = "test wrapminmax\n\n\nxxx yyy;"; - assertEquals(expected, actual, "Default Linewrap"); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void space() throws IOException { - String model = "test linewrap space foo;"; - String expected = "test linewrap\nspace foo;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } - - /** - * This test is copied from - * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. - * - * @throws IOException - */ - @Test - public void datatypeRules() throws IOException { - String model = "test linewrap datatypes abc kw1 bcd def kw3;"; - String expected = "test linewrap\ndatatypes abc\nkw1\nbcd\ndef\nkw3;"; - assertFormattedPTC(expected, model); - assertFormattedNM(expected, model, 0, model.length()); - assertPreserved(model); - } -} +/******************************************************************************* + * 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.jupiter.formatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.eclipse.xtext.resource.SaveOptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Decl; +import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.FormatterTestLanguageFactory; +import com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.TestLinewrapMinMax; + + +/** + * This class tests the Acs Formatter framework. The tests are basically a copy + * of the Xtext Formatter tests. + */ +@SuppressWarnings("nls") +public class FormatterTest extends AbstractFormatterTest { + @Override + protected FormatterTestUtil getXtextTestUtil() { + return FormatterTestUtil.getInstance(); + } + + /** + * This test class does not have a test source file. {@inheritDoc} + */ + @Override + protected String getTestSourceFileName() { + return null; + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrap() throws IOException { + String model = "test linewrap float val; int x; double y;"; + String expected = "test linewrap\nfloat val;\nint x;\ndouble y;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void keepComments() throws IOException { + // String model = "test linewrap float val; int x; double y;"; + String model = "// begincomment \ntest linewrap// comment1\n" + "float val;//comment2\n" + "int x;" + "double y; //yoyoyo!\n// endcomment."; + final String exp = "// begincomment \ntest linewrap// comment1\n" + "float val;//comment2\n" + "int x;\n" + "double y; //yoyoyo!\n// endcomment."; + assertFormattedPTC(exp, model); + assertFormattedNM(exp, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test verifies that {@link com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Line} elements are aligned at the specified column. + * + * @throws IOException + */ + @Test + public void column() throws IOException { + String model = "test column item int x;"; + String expected = "test\n column\n\titem int x;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test verifies that elements can be aligned at column 0 and also that + * a minimum padding is always inserted. + * + * @throws IOException + */ + @Test + public void columnMinimumPadding() throws IOException { + String model = " test column name item int x;"; + String expected = "test\n column name\n\n\titem int x;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model.trim()); + } + + /** + * This test verifies that {@link com.avaloq.tools.ddk.xtext.formatter.formatterTestLanguage.Line} elements are aligned with the specified offset. + * + * @throws IOException + */ + @Test + public void offset() throws IOException { + String model = "test offset value v pair p1 p2"; + String expected = "test\noffset\n\tvalue v\n\t\tpair p1 p2"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test verifies right padding does pad and that there is always minimum padding " ". + * + * @throws IOException + */ + @Test + public void rightPadding() throws IOException { + String model = "test padding long_name n2;"; + String expected = "test\npadding long_name n2 ;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void indentation() throws IOException { + String model = "test indentation { float val; double y; indentation { int x; } }"; + String expected = "test indentation {\n float val;\n double y;\n indentation {\n int x;\n }\n}"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void association() throws IOException { + String model = "test indentation { var = [0,1,2,3,4]; }"; + String expected = "test indentation {\n var=[ 0, 1, 2, 3, 4 ];\n}"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void indentationAndComments() throws IOException { + String model = "test /* xxx */ indentation { float val; // some float\n double /* oo */ y; indentation { // some block\n int x; // xxx\n } } // final comment"; + String expected = "test /* xxx */ indentation {\n float val; // some float\n double /* oo */ y;\n indentation { // some block\n int x; // xxx\n }\n} // final comment"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * It has been adapted to + * + * @throws IOException + */ + @Test + public void indentationAndLineWrap() throws IOException { + String model = "test indentation { void func(x:int,y:int,s:javalangString, foo:javasqlDate, blupp:mylongtype, msads:adshdjkhsakdasdkslajdlsask, x:x, a:b, c:d ); }"; + String expected = "test indentation {\n void func(x:int,y:int,\n\t\ts:javalangString,\n\t\tfoo:javasqlDate,\n\t\tblupp:mylongtype,\n\t\tmsads:adshdjkhsakdasdkslajdlsask,\n\t\tx:x,a:b,c:d);\n}"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void between1() throws IOException { + String model = "test indentation { indentation { x x; }; }"; + String expected = "test indentation {\n indentation {\n x x;\n };\n}"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void between2() throws IOException { + String model = "test indentation { indentation { x x; } }"; + String expected = "test indentation {\n indentation {\n x x;\n }\n}"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRule() throws IOException { + String model = "test linewrap fqn ab; fqn xx.yy.zz;"; + String expected = "test linewrap\nfqn\nab;\nfqn\nxx.yy.zz;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRulePartial1() throws IOException { + String model = "test linewrap fqn ab . xx .yy .zz;"; + String expected = "test linewrap fqn ab.xx.yy.zz;"; + assertFormattedNM(expected, model, 22, 2); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRulePartial2() throws IOException { + String model = "test linewrap fqn ab . xx .yy .zz;fqn xxx;"; + String expected = "test linewrap fqn\nab.xx.yy.zz;fqn xxx;"; + assertFormattedNM(expected, model, 15, 10); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRulePartial3() throws IOException { + String model = "test linewrap fqn ab . xx .yy .zz;fqn xxx;"; + String expected = "test linewrap fqn ab.xx.yy.zz;\nfqn xxx;"; + assertFormattedNM(expected, model, 25, 12); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void formatSegment1() throws IOException { + String model = "test\nindentation {\n indentation { x x ; } }"; + String expected = "test\nindentation {\n indentation {\n x x;\n } }"; + assertFormattedNM(expected, model, 30, 18); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void formatSegment2() throws IOException { + String model = "test indentation {\n indentation { x x ; } }"; + // String expected = + // "test\nindentation {\n indentation {\n x x;\n } }"; + assertFormattedNM(model, model, 7, 10); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void formatSegment3() throws IOException { + String model = " test indentation {\n indentation { x x ; } }"; + String expected = "test indentation {\n indentation {\n x x;\n }\n}"; + assertFormattedNM(expected, model, 0, model.length()); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRuleRef1() throws IOException { + String model = "test linewrap fqn ab .cd .ef; fqnref ab. cd. ef;"; + String expected = "test linewrap\nfqn\nab.cd.ef;\nfqnref\nab.cd.ef;"; + // assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRuleRef2() throws IOException { + String model = "test linewrap fqn ab.cd.ef; fqnref ab.cd.ef;"; + String expected = "test linewrap\nfqn\nab.cd.ef;\nfqnref\nab.cd.ef;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest but it is modified + * because I think the expected behavior in the original test is wrong. + * + * @throws IOException + */ + @Test + public void linewrapDatatypeRuleComments() throws IOException { + String model = "test linewrap/* 1 */fqn/* 2 */ab.cd.ef/* 3 */;/* 4 */fqnref/* 5 */ab.cd.ef/* 6 */;/* 7 */"; + // The expected model string differs from Xtext's - + // Xtext does not expect a line wrap after the keyword "linewrap" + // Xtext does not expect a line wrap prior to fqnref assignment + // etc... + String expected = "test linewrap/* 1 */ fqn/* 2 */\nab.cd.ef/* 3 */;/* 4 */ fqnref\n/* 5 */ ab.cd.ef/* 6 */;/* 7 */"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void enumeration() throws IOException { + String model = "test linewrap enum lit1,lit2,lit3,lit1;"; + String expected = "test linewrap\nenum lit1 ,\nlit2,\nlit3,\nlit1;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=312559 + @Test + public void suppressedWhitespace() throws IOException { + String model = "test linewrap `f%%a` post;"; + String expected = "test linewrap\n`f%< b >%a` post;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + // TODO: investigate whether to include test or not - currently this test + // would fail + @Disabled + public void suppressedLinewrap() throws IOException { + String model = "test linewrap\n`foo%abcd%foo%< b\n>%abcd%foo%abcd%foo%abcd%" + "foo%abcd%foo%abcd%foo%abcd%foo%abcd%foo%abcd%foo%xx%foo%abcd%foo%abcd%" + + "foo%abcd%foo%<\nb >%foo%abcd` post;"; + assertFormattedPTC(model, model); + assertFormattedNM(model, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapMin() throws IOException { + String model = "test wrapminmax foo bar;"; + String expected = "test wrapminmax\n\nfoo bar;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapMax() throws IOException { + String model = "test wrapminmax\n\n\n\n\n\n\n\n\n\n\n\n\nfoo bar;"; + String expected = "test wrapminmax\n\n\n\n\nfoo bar;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapKeep() throws IOException { + String model = "test wrapminmax\n\n\n\nfoo bar;"; + assertFormattedPTC(model, model); + assertFormattedNM(model, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void linewrapDefault() { + FormatterTestLanguageFactory f = FormatterTestLanguageFactory.eINSTANCE; + TestLinewrapMinMax m = f.createTestLinewrapMinMax(); + Decl d = f.createDecl(); + d.getType().add("xxx"); + d.getName().add("yyy"); + m.getItems().add(d); + String actual = getSerializer().serialize(m, SaveOptions.newBuilder().format().getOptions()); + String expected = "test wrapminmax\n\n\nxxx yyy;"; + assertEquals(expected, actual, "Default Linewrap"); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void space() throws IOException { + String model = "test linewrap space foo;"; + String expected = "test linewrap\nspace foo;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } + + /** + * This test is copied from + * org.eclipse.xtext.nodemodel.impl.formatter.FormatterTest. + * + * @throws IOException + */ + @Test + public void datatypeRules() throws IOException { + String model = "test linewrap datatypes abc kw1 bcd def kw3;"; + String expected = "test linewrap\ndatatypes abc\nkw1\nbcd\ndef\nkw3;"; + assertFormattedPTC(expected, model); + assertFormattedNM(expected, model, 0, model.length()); + assertPreserved(model); + } +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTestUtil.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTestUtil.java index 1d63fb44b5..b7394d0f49 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTestUtil.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/jupiter/formatter/FormatterTestUtil.java @@ -1,46 +1,46 @@ -/******************************************************************************* - * 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.jupiter.formatter; - -import com.avaloq.tools.ddk.xtext.formatter.FormatterTestLanguageStandaloneSetup; -import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; -import com.google.inject.Injector; - - -public final class FormatterTestUtil extends AbstractXtextTestUtil { - private final Injector injector = new FormatterTestLanguageStandaloneSetup().createInjectorAndDoEMFRegistration(); - - private FormatterTestUtil() { - // private constructor - } - - /** - * The singleton instance. - */ - private static final class InstanceHolder { - // Initialize-on-demand holder pattern. - private static final FormatterTestUtil INSTANCE = new FormatterTestUtil(); - - public static FormatterTestUtil get() { - return INSTANCE; - } - } - - public static FormatterTestUtil getInstance() { - return InstanceHolder.get(); - } - - @Override - protected Injector getInjector() { - return injector; - } - -} +/******************************************************************************* + * 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.jupiter.formatter; + +import com.avaloq.tools.ddk.xtext.formatter.FormatterTestLanguageStandaloneSetup; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTestUtil; +import com.google.inject.Injector; + + +public final class FormatterTestUtil extends AbstractXtextTestUtil { + private final Injector injector = new FormatterTestLanguageStandaloneSetup().createInjectorAndDoEMFRegistration(); + + private FormatterTestUtil() { + // private constructor + } + + /** + * The singleton instance. + */ + private static final class InstanceHolder { + // Initialize-on-demand holder pattern. + private static final FormatterTestUtil INSTANCE = new FormatterTestUtil(); + + public static FormatterTestUtil get() { + return INSTANCE; + } + } + + public static FormatterTestUtil getInstance() { + return InstanceHolder.get(); + } + + @Override + protected Injector getInjector() { + return injector; + } + +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/naming/QualifiedNameSegmentTreeLookupTest.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/naming/QualifiedNameSegmentTreeLookupTest.java index 9fccb22092..5490343e01 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/naming/QualifiedNameSegmentTreeLookupTest.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/naming/QualifiedNameSegmentTreeLookupTest.java @@ -1,242 +1,242 @@ -/******************************************************************************* - * Copyright (c) 2016 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.naming; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.naming.QualifiedName; -import org.junit.jupiter.api.Test; - -import com.google.common.collect.ImmutableSet; - - -@SuppressWarnings({"nls", "unused", "PMD.JUnitAssertionsShouldIncludeMessage"}) -// CHECKSTYLE:CHECK-OFF MultipleStringLiteralsCheck -public class QualifiedNameSegmentTreeLookupTest { - private static final Logger LOGGER = LogManager.getLogger(QualifiedNameSegmentTreeLookupTest.class); - - private final QualifiedNameSegmentTreeLookup lookup = new QualifiedNameSegmentTreeLookup(URI.class, true); - - @Test - public void testEmpty() { - assertNull(lookup.get(QualifiedName.EMPTY)); - } - - @Test - public void testExact() { - QualifiedName name = name("foo"); - Collection values = Collections.singletonList(uri(name)); - lookup.putAll(name, values); - assertContentEquals(values, lookup.get(name)); - - name = name("foo.bar"); - values = Collections.singletonList(uri(name)); - lookup.putAll(name, values); - assertContentEquals(values, lookup.get(name)); - - name = name("bar"); - values = Collections.singletonList(uri(name)); - lookup.putAll(name, values); - assertContentEquals(values, lookup.get(name)); - } - - @Test - public void testTopLevelPatternWithoutWildcard() { - URI value1 = put("foo"); - URI value2 = put("bar"); - URI value3 = put("foo2"); - - assertContentEquals(ImmutableSet.of(value1), lookup.get(pattern("foo"), false)); - assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("bar"), false)); - assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("foo2"), false)); - } - - @Test - public void testTopLevelPatternWithWildcard() { - URI value1 = put("foo"); - URI value2 = put("foo2"); - URI value3 = put("bar"); - - assertContentEquals(ImmutableSet.of(value1, value2), lookup.get(pattern("f*"), true)); - assertContentEquals(ImmutableSet.of(value1, value2), lookup.get(pattern("foo*"), true)); - assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("b*"), true)); - } - - @Test - public void testNestedPatternMatchesWithoutWildcard() { - URI value1 = put("foo"); - URI value2 = put("foo.bar"); - URI value3 = put("foo2"); - - assertContentEquals(ImmutableSet.of(value1), lookup.get(pattern("foo"), true)); - assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("foo.bar"), true)); - assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("foo2"), true)); - } - - @Test - public void testNestedPatternMatchesWithWildcard() { - URI value1 = put("foo"); - URI value2 = put("foo.bar"); - URI value3 = put("foo.baz"); - URI value4 = put("foo.baz.bazz"); - URI value5 = put("foo2"); - - assertContentEquals(ImmutableSet.of(value1, value5), lookup.get(pattern("f*"), true)); - assertContentEquals(ImmutableSet.of(value2, value3), lookup.get(pattern("foo.*"), true)); - assertContentEquals(ImmutableSet.of(value2, value3), lookup.get(pattern("foo.ba*"), true)); - assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("foo.bar*"), true)); - } - - @Test - public void testNestedPatternMatchesWithRecursiveWildcard() { - URI value1 = put("foo"); - URI value2 = put("foo.bar"); - URI value3 = put("foo.bar.baz"); - URI value4 = put("foo.bar.baz.quux"); - URI value5 = put("foo.foo"); - URI value6 = put("foo2"); - - assertContentEquals(ImmutableSet.of(value2, value3, value4, value5), lookup.get(pattern("foo.**"), true)); - assertContentEquals(ImmutableSet.of(value2, value3, value4), lookup.get(pattern("foo.b**"), true)); - } - - @Test - public void testUnmatchedNestedPattern() { - URI value1 = put("foo"); - URI value2 = put("foo.bar"); - URI value3 = put("foo.bar.baz"); - URI value4 = put("foo.bar.baz.quux"); - URI value5 = put("foo.foo"); - URI value6 = put("foo2"); - - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("e*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("g*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foa.*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("fon.b*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.c*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.baq.b*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.bar.a*"), true)); - assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.bar.bazz*"), true)); - } - - @Test - public void testOutOfOrderInsertion() { - QualifiedName name1 = name("foo.bar"); - Collection value1 = Collections.singletonList(uri(name1)); - lookup.putAll(name1, value1); - assertContentEquals(value1, lookup.get(name1)); - - QualifiedName name2 = name("foo"); - Collection value2 = Collections.singletonList(uri(name2)); - lookup.putAll(name2, value2); - assertContentEquals(value2, lookup.get(name2)); - } - - @Test - public void testLoadStore() throws IOException, ClassNotFoundException { - List nameList = List.of(name("foo"), name("foo.bar"), name("bar")); - for (QualifiedName qn : nameList) { - lookup.put(qn, uri(qn)); - } - ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); - ObjectOutputStream objectOutStream = new ObjectOutputStream(byteOutStream); - objectOutStream.writeObject(lookup); - byte[] data = byteOutStream.toByteArray(); - - ObjectInputStream inStream = new ObjectInputStream(new ByteArrayInputStream(data)); - Object readBack = inStream.readObject(); - - assertEquals(readBack.getClass(), lookup.getClass()); - - @SuppressWarnings("unchecked") - QualifiedNameLookup readBackLookup = (QualifiedNameLookup) readBack; - - for (QualifiedName qn : nameList) { - assertEquals(lookup.get(qn), readBackLookup.get(qn)); - } - } - - @Test - public void testGetMappings() { - final QualifiedName a = name("A"); - final QualifiedName b = name("B"); - final QualifiedName c = name("A.C"); - final QualifiedName d = name("A.D"); - final QualifiedName e = name("B.E"); - final QualifiedName f = name("B.F"); - final QualifiedName g = name("A.C.G"); - final QualifiedName h = name("A.C.H"); - final QualifiedName i = name("A.D.I"); - final QualifiedName j = name("A.D.J"); - - List nameList = List.of(a, b, c, d, e, f, g, h, i, j); - for (QualifiedName qn : nameList) { - lookup.put(qn, uri(qn)); - } - - URI value = URI.createURI("scheme:/host"); - - lookup.put(c, value); - lookup.put(g, value); - lookup.put(h, value); - lookup.put(f, value); - lookup.put(b, value); - - Collection result = lookup.getMappings(value); - Collection expected = List.of(c, g, h, f, b); - - assertContentEquals(expected, result); - - URI noSuchValue = URI.createURI("scheme:/anotherHost"); - expected = lookup.getMappings(noSuchValue); - assertEquals(expected.size(), 0); - } - - private QualifiedName name(final String str) { - return QualifiedNames.safeQualifiedName(str); - } - - private QualifiedNamePattern pattern(final String str) { - return pattern(name(str)); - } - - private QualifiedNamePattern pattern(final QualifiedName name) { - return QualifiedNamePattern.create(name); - } - - public URI uri(final QualifiedName name) { - return URI.createURI("scheme:/" + name); - } - - private URI put(final String name) { - QualifiedName qname = name(name); - URI value = uri(qname); - lookup.put(qname, value); - return value; - } - - public void assertContentEquals(final Collection expected, final Collection actual) { - assertEquals(ImmutableSet.copyOf(expected), ImmutableSet.copyOf(actual)); - } -} +/******************************************************************************* + * Copyright (c) 2016 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.naming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableSet; + + +@SuppressWarnings({"nls", "unused", "PMD.JUnitAssertionsShouldIncludeMessage"}) +// CHECKSTYLE:CHECK-OFF MultipleStringLiteralsCheck +public class QualifiedNameSegmentTreeLookupTest { + private static final Logger LOGGER = LogManager.getLogger(QualifiedNameSegmentTreeLookupTest.class); + + private final QualifiedNameSegmentTreeLookup lookup = new QualifiedNameSegmentTreeLookup(URI.class, true); + + @Test + public void testEmpty() { + assertNull(lookup.get(QualifiedName.EMPTY)); + } + + @Test + public void testExact() { + QualifiedName name = name("foo"); + Collection values = Collections.singletonList(uri(name)); + lookup.putAll(name, values); + assertContentEquals(values, lookup.get(name)); + + name = name("foo.bar"); + values = Collections.singletonList(uri(name)); + lookup.putAll(name, values); + assertContentEquals(values, lookup.get(name)); + + name = name("bar"); + values = Collections.singletonList(uri(name)); + lookup.putAll(name, values); + assertContentEquals(values, lookup.get(name)); + } + + @Test + public void testTopLevelPatternWithoutWildcard() { + URI value1 = put("foo"); + URI value2 = put("bar"); + URI value3 = put("foo2"); + + assertContentEquals(ImmutableSet.of(value1), lookup.get(pattern("foo"), false)); + assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("bar"), false)); + assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("foo2"), false)); + } + + @Test + public void testTopLevelPatternWithWildcard() { + URI value1 = put("foo"); + URI value2 = put("foo2"); + URI value3 = put("bar"); + + assertContentEquals(ImmutableSet.of(value1, value2), lookup.get(pattern("f*"), true)); + assertContentEquals(ImmutableSet.of(value1, value2), lookup.get(pattern("foo*"), true)); + assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("b*"), true)); + } + + @Test + public void testNestedPatternMatchesWithoutWildcard() { + URI value1 = put("foo"); + URI value2 = put("foo.bar"); + URI value3 = put("foo2"); + + assertContentEquals(ImmutableSet.of(value1), lookup.get(pattern("foo"), true)); + assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("foo.bar"), true)); + assertContentEquals(ImmutableSet.of(value3), lookup.get(pattern("foo2"), true)); + } + + @Test + public void testNestedPatternMatchesWithWildcard() { + URI value1 = put("foo"); + URI value2 = put("foo.bar"); + URI value3 = put("foo.baz"); + URI value4 = put("foo.baz.bazz"); + URI value5 = put("foo2"); + + assertContentEquals(ImmutableSet.of(value1, value5), lookup.get(pattern("f*"), true)); + assertContentEquals(ImmutableSet.of(value2, value3), lookup.get(pattern("foo.*"), true)); + assertContentEquals(ImmutableSet.of(value2, value3), lookup.get(pattern("foo.ba*"), true)); + assertContentEquals(ImmutableSet.of(value2), lookup.get(pattern("foo.bar*"), true)); + } + + @Test + public void testNestedPatternMatchesWithRecursiveWildcard() { + URI value1 = put("foo"); + URI value2 = put("foo.bar"); + URI value3 = put("foo.bar.baz"); + URI value4 = put("foo.bar.baz.quux"); + URI value5 = put("foo.foo"); + URI value6 = put("foo2"); + + assertContentEquals(ImmutableSet.of(value2, value3, value4, value5), lookup.get(pattern("foo.**"), true)); + assertContentEquals(ImmutableSet.of(value2, value3, value4), lookup.get(pattern("foo.b**"), true)); + } + + @Test + public void testUnmatchedNestedPattern() { + URI value1 = put("foo"); + URI value2 = put("foo.bar"); + URI value3 = put("foo.bar.baz"); + URI value4 = put("foo.bar.baz.quux"); + URI value5 = put("foo.foo"); + URI value6 = put("foo2"); + + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("e*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("g*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foa.*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("fon.b*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.c*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.baq.b*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.bar.a*"), true)); + assertContentEquals(ImmutableSet.of(), lookup.get(pattern("foo.bar.bazz*"), true)); + } + + @Test + public void testOutOfOrderInsertion() { + QualifiedName name1 = name("foo.bar"); + Collection value1 = Collections.singletonList(uri(name1)); + lookup.putAll(name1, value1); + assertContentEquals(value1, lookup.get(name1)); + + QualifiedName name2 = name("foo"); + Collection value2 = Collections.singletonList(uri(name2)); + lookup.putAll(name2, value2); + assertContentEquals(value2, lookup.get(name2)); + } + + @Test + public void testLoadStore() throws IOException, ClassNotFoundException { + List nameList = List.of(name("foo"), name("foo.bar"), name("bar")); + for (QualifiedName qn : nameList) { + lookup.put(qn, uri(qn)); + } + ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutStream = new ObjectOutputStream(byteOutStream); + objectOutStream.writeObject(lookup); + byte[] data = byteOutStream.toByteArray(); + + ObjectInputStream inStream = new ObjectInputStream(new ByteArrayInputStream(data)); + Object readBack = inStream.readObject(); + + assertEquals(readBack.getClass(), lookup.getClass()); + + @SuppressWarnings("unchecked") + QualifiedNameLookup readBackLookup = (QualifiedNameLookup) readBack; + + for (QualifiedName qn : nameList) { + assertEquals(lookup.get(qn), readBackLookup.get(qn)); + } + } + + @Test + public void testGetMappings() { + final QualifiedName a = name("A"); + final QualifiedName b = name("B"); + final QualifiedName c = name("A.C"); + final QualifiedName d = name("A.D"); + final QualifiedName e = name("B.E"); + final QualifiedName f = name("B.F"); + final QualifiedName g = name("A.C.G"); + final QualifiedName h = name("A.C.H"); + final QualifiedName i = name("A.D.I"); + final QualifiedName j = name("A.D.J"); + + List nameList = List.of(a, b, c, d, e, f, g, h, i, j); + for (QualifiedName qn : nameList) { + lookup.put(qn, uri(qn)); + } + + URI value = URI.createURI("scheme:/host"); + + lookup.put(c, value); + lookup.put(g, value); + lookup.put(h, value); + lookup.put(f, value); + lookup.put(b, value); + + Collection result = lookup.getMappings(value); + Collection expected = List.of(c, g, h, f, b); + + assertContentEquals(expected, result); + + URI noSuchValue = URI.createURI("scheme:/anotherHost"); + expected = lookup.getMappings(noSuchValue); + assertEquals(expected.size(), 0); + } + + private QualifiedName name(final String str) { + return QualifiedNames.safeQualifiedName(str); + } + + private QualifiedNamePattern pattern(final String str) { + return pattern(name(str)); + } + + private QualifiedNamePattern pattern(final QualifiedName name) { + return QualifiedNamePattern.create(name); + } + + public URI uri(final QualifiedName name) { + return URI.createURI("scheme:/" + name); + } + + private URI put(final String name) { + QualifiedName qname = name(name); + URI value = uri(qname); + lookup.put(qname, value); + return value; + } + + public void assertContentEquals(final Collection expected, final Collection actual) { + assertEquals(ImmutableSet.copyOf(expected), ImmutableSet.copyOf(actual)); + } +} diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractXtextTestsInjectorProvider.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractXtextTestsInjectorProvider.java index b804cd5ad3..09ee5d0a70 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractXtextTestsInjectorProvider.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractXtextTestsInjectorProvider.java @@ -1,36 +1,36 @@ -/******************************************************************************* - * 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 Evolution AG - initial API and implementation - *******************************************************************************/ - -package com.avaloq.tools.ddk.xtext.resource; - -import org.eclipse.xtext.XtextRuntimeModule; -import org.eclipse.xtext.resource.IFragmentProvider; -import org.eclipse.xtext.testing.IInjectorProvider; -import org.eclipse.xtext.util.Modules2; - -import com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProviderTest.TestSelectorFragmentProvider; -import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Injector; - - -public class AbstractXtextTestsInjectorProvider implements IInjectorProvider { - - @Override - public Injector getInjector() { - return Guice.createInjector(Modules2.mixin(new XtextRuntimeModule(), new AbstractModule() { - @Override - protected void configure() { - bind(IFragmentProvider.class).to(TestSelectorFragmentProvider.class); - } - })); - } -} +/******************************************************************************* + * 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 Evolution AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.xtext.resource; + +import org.eclipse.xtext.XtextRuntimeModule; +import org.eclipse.xtext.resource.IFragmentProvider; +import org.eclipse.xtext.testing.IInjectorProvider; +import org.eclipse.xtext.util.Modules2; + +import com.avaloq.tools.ddk.xtext.resource.AbstractSelectorFragmentProviderTest.TestSelectorFragmentProvider; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; + + +public class AbstractXtextTestsInjectorProvider implements IInjectorProvider { + + @Override + public Injector getInjector() { + return Guice.createInjector(Modules2.mixin(new XtextRuntimeModule(), new AbstractModule() { + @Override + protected void configure() { + bind(IFragmentProvider.class).to(TestSelectorFragmentProvider.class); + } + })); + } +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/layered/DefaultXtextTargetPlatform.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/layered/DefaultXtextTargetPlatform.java index f63490367b..612eca5b06 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/layered/DefaultXtextTargetPlatform.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/layered/DefaultXtextTargetPlatform.java @@ -1,84 +1,84 @@ -/******************************************************************************* - * Copyright (c) 2016 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.layered; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; - -import com.avaloq.tools.ddk.xtext.build.IBinaryModelStore; -import com.avaloq.tools.ddk.xtext.resource.IDerivedObjectAssociationsStore; -import com.avaloq.tools.ddk.xtext.resource.extensions.DelegatingResourceDescriptionsData; -import com.avaloq.tools.ddk.xtext.resource.extensions.IResourceDescriptionsData; - - -/** - * Default implementation implements the legacy mechanism through an empty collection. - */ -public class DefaultXtextTargetPlatform implements IXtextTargetPlatform { - private volatile IResourceDescriptionsData index; // NOPMD: volatile - - @Override - public IResourceDescriptionsData getIResourceDescriptionsData() { - IResourceDescriptionsData localRef = index; - if (localRef == null) { - synchronized (this) { - localRef = index; - if (localRef == null) { - localRef = new DelegatingResourceDescriptionsData(new ResourceDescriptionsData(Collections.emptyList())); - index = localRef; - } - } - } - return localRef; - } - - @Override - public void open(final boolean clean, final IProgressMonitor monitor) throws IOException { - // nothing to do - } - - @Override - public void close(final IProgressMonitor monitor) { - // nothing to do - } - - @Override - public IIssueStore getIssueStore() { - // No issue store needed; Eclipse stores markers by itself. - return null; - } - - @Override - public IDerivedObjectAssociationsStore getAssociationsStore() { - // Association store for default platform is not implemented yet - return null; - } - - @Override - public Map getMetadata(final Collection keys, final IProgressMonitor monitor) { - return Collections.emptyMap(); - } - - @Override - public void setMetadata(final Map options) { - // nothing to do - } - - @Override - public IBinaryModelStore getBinaryModelStore() { - return null; - } +/******************************************************************************* + * Copyright (c) 2016 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.layered; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; + +import com.avaloq.tools.ddk.xtext.build.IBinaryModelStore; +import com.avaloq.tools.ddk.xtext.resource.IDerivedObjectAssociationsStore; +import com.avaloq.tools.ddk.xtext.resource.extensions.DelegatingResourceDescriptionsData; +import com.avaloq.tools.ddk.xtext.resource.extensions.IResourceDescriptionsData; + + +/** + * Default implementation implements the legacy mechanism through an empty collection. + */ +public class DefaultXtextTargetPlatform implements IXtextTargetPlatform { + private volatile IResourceDescriptionsData index; // NOPMD: volatile + + @Override + public IResourceDescriptionsData getIResourceDescriptionsData() { + IResourceDescriptionsData localRef = index; + if (localRef == null) { + synchronized (this) { + localRef = index; + if (localRef == null) { + localRef = new DelegatingResourceDescriptionsData(new ResourceDescriptionsData(Collections.emptyList())); + index = localRef; + } + } + } + return localRef; + } + + @Override + public void open(final boolean clean, final IProgressMonitor monitor) throws IOException { + // nothing to do + } + + @Override + public void close(final IProgressMonitor monitor) { + // nothing to do + } + + @Override + public IIssueStore getIssueStore() { + // No issue store needed; Eclipse stores markers by itself. + return null; + } + + @Override + public IDerivedObjectAssociationsStore getAssociationsStore() { + // Association store for default platform is not implemented yet + return null; + } + + @Override + public Map getMetadata(final Collection keys, final IProgressMonitor monitor) { + return Collections.emptyMap(); + } + + @Override + public void setMetadata(final Map options) { + // nothing to do + } + + @Override + public IBinaryModelStore getBinaryModelStore() { + return null; + } } \ No newline at end of file diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/DerivedObjectAssociations.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/DerivedObjectAssociations.java index 5f2a50e251..6086091fc8 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/DerivedObjectAssociations.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/DerivedObjectAssociations.java @@ -1,83 +1,83 @@ -/******************************************************************************* - * Copyright (c) 2018 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.resource; - -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - - -/** - * Derived objects associated with resource description. - *

- * Instances of this class are attached to resource descriptions or resource description delta. - *

- *

- * Main purpose of the associations is to manage life cycle of the derived resources. - * Associations enable generators to identify what derived objects (sources) should be dropped. - *

- *

- * There can be many producers of derived objects, where generators executed by builder participants - * are the most common kind. Objects produced by generators may be other sources, but could also be some - * entities specific to the business application of a DSL (i.e. database objects). Derived objects - * could also be valid Xtext resources visible to the builder. Therefore the framework in not trying - * to interpret the information about the derived objects, but stores it grouped by the producer (generator id). - *

- *

- * For simplicity we will refer to all producers of derived objects as to generators. - *

- */ -public class DerivedObjectAssociations { - - private final Map> derivedObjects = Maps.newHashMap(); - - /** - * Adds the association for a derived object created the given generator. - * - * @param generatorType - * an identifier of the generator that created the object, must not be {@code null} - * @param derivedObjectUri - * the URI of the derived object (freely defined by the generator), must not be {@code null} - */ - public void add(final String generatorType, final String derivedObjectUri) { - derivedObjects.computeIfAbsent(generatorType, s -> Sets.newHashSet()).add(derivedObjectUri); - } - - /** - * Returns identifiers of all generators that created derived objects for the given resource. - * - * @return identifiers of generators that created derived objects, never {@code null} - */ - public Set getGeneratorTypes() { - return derivedObjects.keySet(); - } - - /** - * Returns set of derived object URIs for the given generator. - * - * @param generatorType - * the identifier of the generator, must not be {@code null} - * @return set of URIs of derived objects, never {@code null} - */ - public Set getDerivedObjects(final String generatorType) { - return derivedObjects.getOrDefault(generatorType, Collections.emptySet()); - } - - @Override - public String toString() { - return derivedObjects.toString(); - } - -} +/******************************************************************************* + * Copyright (c) 2018 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.resource; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + + +/** + * Derived objects associated with resource description. + *

+ * Instances of this class are attached to resource descriptions or resource description delta. + *

+ *

+ * Main purpose of the associations is to manage life cycle of the derived resources. + * Associations enable generators to identify what derived objects (sources) should be dropped. + *

+ *

+ * There can be many producers of derived objects, where generators executed by builder participants + * are the most common kind. Objects produced by generators may be other sources, but could also be some + * entities specific to the business application of a DSL (i.e. database objects). Derived objects + * could also be valid Xtext resources visible to the builder. Therefore the framework in not trying + * to interpret the information about the derived objects, but stores it grouped by the producer (generator id). + *

+ *

+ * For simplicity we will refer to all producers of derived objects as to generators. + *

+ */ +public class DerivedObjectAssociations { + + private final Map> derivedObjects = Maps.newHashMap(); + + /** + * Adds the association for a derived object created the given generator. + * + * @param generatorType + * an identifier of the generator that created the object, must not be {@code null} + * @param derivedObjectUri + * the URI of the derived object (freely defined by the generator), must not be {@code null} + */ + public void add(final String generatorType, final String derivedObjectUri) { + derivedObjects.computeIfAbsent(generatorType, s -> Sets.newHashSet()).add(derivedObjectUri); + } + + /** + * Returns identifiers of all generators that created derived objects for the given resource. + * + * @return identifiers of generators that created derived objects, never {@code null} + */ + public Set getGeneratorTypes() { + return derivedObjects.keySet(); + } + + /** + * Returns set of derived object URIs for the given generator. + * + * @param generatorType + * the identifier of the generator, must not be {@code null} + * @return set of URIs of derived objects, never {@code null} + */ + public Set getDerivedObjects(final String generatorType) { + return derivedObjects.getOrDefault(generatorType, Collections.emptySet()); + } + + @Override + public String toString() { + return derivedObjects.toString(); + } + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/FixedCopiedResourceDescription.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/FixedCopiedResourceDescription.java index 8da46eaddc..200e85debd 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/FixedCopiedResourceDescription.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/FixedCopiedResourceDescription.java @@ -1,123 +1,123 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.InternalEObject; -import org.eclipse.emf.ecore.util.EcoreUtil; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.resource.EObjectDescription; -import org.eclipse.xtext.resource.IEObjectDescription; -import org.eclipse.xtext.resource.IReferenceDescription; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.impl.AbstractResourceDescription; -import org.eclipse.xtext.resource.impl.EObjectDescriptionLookUp; - -import com.avaloq.tools.ddk.xtext.resource.extensions.IResourceDescription2; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Maps; - - -/** - * Fix a contract-breaking default implementation ({@link CopiedResourceDescription}) in Xtext 2.0.1. Further use LinkedHashMap instead of HashMap to preserve - * order of user data entries. - * Also the {@link IDetachableDescription} is respected in order to reuse descriptions and computed data where possible. - * This descriptor, like {@link CopiedResourceDescription} should be local to a single build run and never survive that single build cycle. - * Within the build loop, imported names and reference descriptions should never be necessary, thus the original implementation logs an error - * if getImportedNames or getReferenceDescriptions are ever called. - * The ASMD builder calls though getReferenceDescriptions when exporting descriptors to the database, so at the time being we cannot guard this method as we - * would like to. - */ -public class FixedCopiedResourceDescription extends AbstractResourceDescription implements IResourceDescription2 { - - private static final Logger LOG = LogManager.getLogger(FixedCopiedResourceDescription.class); - - private final URI uri; - private final List exported; - - @SuppressWarnings("unchecked") - public FixedCopiedResourceDescription(final IResourceDescription original) { - this.uri = original.getURI(); - this.exported = ImmutableList.copyOf(Iterables.transform(original.getExportedObjects(), from -> { - if (from.getEObjectOrProxy().eIsProxy()) { - return from; - } else if (from instanceof IDetachableDescription) { - return ((IDetachableDescription) from).detach(); - } - InternalEObject result = (InternalEObject) EcoreUtil.create(from.getEClass()); - result.eSetProxyURI(from.getEObjectURI()); - String[] userDataKeys = from.getUserDataKeys(); - ImmutableMap.Builder userData = ImmutableMap.builderWithExpectedSize(userDataKeys.length); - for (final String key : userDataKeys) { - userData.put(key, from.getUserData(key)); - } - return EObjectDescription.create(from.getName(), result, userData.build()); - })); - } - - @Override - public URI getURI() { - return uri; - } - - @Override - protected List computeExportedObjects() { - return exported; - } - - @Override - protected EObjectDescriptionLookUp getLookUp() { - if (lookup == null) { - lookup = new PatternAwareEObjectDescriptionLookUp(computeExportedObjects()); - } - return lookup; - } - - @Override - public Iterable getImportedNames() { - IllegalStateException exception = new IllegalStateException("getImportedNames " + getURI()); //$NON-NLS-1$ - LOG.warn(exception, exception); - return Collections.emptyList(); - } - - @Override - public Iterable getReferenceDescriptions() { - return Collections.emptyList(); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(getClass().getName()); - result.append('@'); - result.append(Integer.toHexString(hashCode())); - - result.append(" (URI: "); //$NON-NLS-1$ - result.append(uri); - result.append(')'); - - return result.toString(); - } - - @Override - public Map> getImportedNamesTypes() { - return Maps.newHashMap(); - } -} +/******************************************************************************* + * Copyright (c) 2016 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.resource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.EObjectDescription; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IReferenceDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.impl.AbstractResourceDescription; +import org.eclipse.xtext.resource.impl.EObjectDescriptionLookUp; + +import com.avaloq.tools.ddk.xtext.resource.extensions.IResourceDescription2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + + +/** + * Fix a contract-breaking default implementation ({@link CopiedResourceDescription}) in Xtext 2.0.1. Further use LinkedHashMap instead of HashMap to preserve + * order of user data entries. + * Also the {@link IDetachableDescription} is respected in order to reuse descriptions and computed data where possible. + * This descriptor, like {@link CopiedResourceDescription} should be local to a single build run and never survive that single build cycle. + * Within the build loop, imported names and reference descriptions should never be necessary, thus the original implementation logs an error + * if getImportedNames or getReferenceDescriptions are ever called. + * The ASMD builder calls though getReferenceDescriptions when exporting descriptors to the database, so at the time being we cannot guard this method as we + * would like to. + */ +public class FixedCopiedResourceDescription extends AbstractResourceDescription implements IResourceDescription2 { + + private static final Logger LOG = LogManager.getLogger(FixedCopiedResourceDescription.class); + + private final URI uri; + private final List exported; + + @SuppressWarnings("unchecked") + public FixedCopiedResourceDescription(final IResourceDescription original) { + this.uri = original.getURI(); + this.exported = ImmutableList.copyOf(Iterables.transform(original.getExportedObjects(), from -> { + if (from.getEObjectOrProxy().eIsProxy()) { + return from; + } else if (from instanceof IDetachableDescription) { + return ((IDetachableDescription) from).detach(); + } + InternalEObject result = (InternalEObject) EcoreUtil.create(from.getEClass()); + result.eSetProxyURI(from.getEObjectURI()); + String[] userDataKeys = from.getUserDataKeys(); + ImmutableMap.Builder userData = ImmutableMap.builderWithExpectedSize(userDataKeys.length); + for (final String key : userDataKeys) { + userData.put(key, from.getUserData(key)); + } + return EObjectDescription.create(from.getName(), result, userData.build()); + })); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + protected List computeExportedObjects() { + return exported; + } + + @Override + protected EObjectDescriptionLookUp getLookUp() { + if (lookup == null) { + lookup = new PatternAwareEObjectDescriptionLookUp(computeExportedObjects()); + } + return lookup; + } + + @Override + public Iterable getImportedNames() { + IllegalStateException exception = new IllegalStateException("getImportedNames " + getURI()); //$NON-NLS-1$ + LOG.warn(exception, exception); + return Collections.emptyList(); + } + + @Override + public Iterable getReferenceDescriptions() { + return Collections.emptyList(); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(getClass().getName()); + result.append('@'); + result.append(Integer.toHexString(hashCode())); + + result.append(" (URI: "); //$NON-NLS-1$ + result.append(uri); + result.append(')'); + + return result.toString(); + } + + @Override + public Map> getImportedNamesTypes() { + return Maps.newHashMap(); + } +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsAccess.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsAccess.java index a830a8f1df..734b4f696e 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsAccess.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsAccess.java @@ -1,48 +1,48 @@ -/******************************************************************************* - * Copyright (c) 2018 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.resource; - -import java.util.Set; - -import org.eclipse.emf.common.util.URI; - - -/** - * Store API for associations between resources processed by the builder and the objects (i.e. sources) - * derived by various providers (i.e. by builder participants) for those resources. - *

- * Read more about associations in {@see DerivedObjectAssociations}. - *

- */ -public interface IDerivedObjectAssociationsAccess { - /** - * Update associated derived artifacts for the given generator and resource. - * - * @param resourceUri - * URI identifying the resource processed by the builder, the URI must be known to the Xtext index - * @param generatorType - * the name of the generator that processed the resource - * @param derivedObjectUris - * URIs identifying the generated objects - */ - void registerAssociations(URI resourceUri, String generatorType, Set derivedObjectUris); - - /** - * Returns associations for objects derived for the given resource identified by a resource URI. - * - * @param resourceUri - * the resource URI, must not be {@code null} - * @return the associations, never {@code null} for existing sources, or {@code null} if the passed URI is not known to the builder - */ - DerivedObjectAssociations getAssociations(URI resourceUri); - -} +/******************************************************************************* + * Copyright (c) 2018 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.resource; + +import java.util.Set; + +import org.eclipse.emf.common.util.URI; + + +/** + * Store API for associations between resources processed by the builder and the objects (i.e. sources) + * derived by various providers (i.e. by builder participants) for those resources. + *

+ * Read more about associations in {@see DerivedObjectAssociations}. + *

+ */ +public interface IDerivedObjectAssociationsAccess { + /** + * Update associated derived artifacts for the given generator and resource. + * + * @param resourceUri + * URI identifying the resource processed by the builder, the URI must be known to the Xtext index + * @param generatorType + * the name of the generator that processed the resource + * @param derivedObjectUris + * URIs identifying the generated objects + */ + void registerAssociations(URI resourceUri, String generatorType, Set derivedObjectUris); + + /** + * Returns associations for objects derived for the given resource identified by a resource URI. + * + * @param resourceUri + * the resource URI, must not be {@code null} + * @return the associations, never {@code null} for existing sources, or {@code null} if the passed URI is not known to the builder + */ + DerivedObjectAssociations getAssociations(URI resourceUri); + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsStore.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsStore.java index da5cadeedf..172dac17db 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsStore.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/IDerivedObjectAssociationsStore.java @@ -1,35 +1,35 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource; - -import java.util.Collection; -import java.util.function.BiConsumer; - -import org.eclipse.emf.common.util.URI; - - -/** - * Store API for associations used as an extension for platform. - */ -public interface IDerivedObjectAssociationsStore extends IDerivedObjectAssociationsAccess { - - /** - * Iterates through a {@link Collection} of {@link URI}s, calling the {@link BiConsumer} {@link derivedObjectAssociations} on each URI. - * - * @param allURIs - * A collection of the URIs for which we wish to retrieve associations - * @param derivedObjectAssociations - * The consumer function that receives URI associations as input - */ - void forEach(Collection allURIs, BiConsumer derivedObjectAssociations); - -} +/******************************************************************************* + * Copyright (c) 2016 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.resource; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import org.eclipse.emf.common.util.URI; + + +/** + * Store API for associations used as an extension for platform. + */ +public interface IDerivedObjectAssociationsStore extends IDerivedObjectAssociationsAccess { + + /** + * Iterates through a {@link Collection} of {@link URI}s, calling the {@link BiConsumer} {@link derivedObjectAssociations} on each URI. + * + * @param allURIs + * A collection of the URIs for which we wish to retrieve associations + * @param derivedObjectAssociations + * The consumer function that receives URI associations as input + */ + void forEach(Collection allURIs, BiConsumer derivedObjectAssociations); + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceCache.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceCache.java index da3fcb3961..3e9598e718 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceCache.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceCache.java @@ -1,147 +1,147 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Field; -import java.util.Map; - -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.xtext.util.OnChangeEvictingCache; -import org.eclipse.xtext.util.OnChangeEvictingCache.CacheAdapter; - -import com.avaloq.tools.ddk.caching.CacheManager; -import com.avaloq.tools.ddk.caching.CacheStatistics; -import com.avaloq.tools.ddk.caching.ICache; -import com.google.common.collect.ArrayListMultimap; - - -/** - * A cache whose lifecycle is associated to that of a resource. - * - * @param - * the key type - * @param - * the value type - */ -public class ResourceCache implements ICache, OnChangeEvictingCache.Listener { - - private final OnChangeEvictingCache.CacheAdapter backend; - private final Resource resource; - - /** - * Creates a new cache or retrieves an existing one associated with the given resource. - * - * @param - * the key type - * @param - * the value type - * @param name - * the name of the cache, must not be {@code null} - * @param resource - * the resource to associate the cache with, must not be {@code null} - * @return an existing or a new resource cache instance, never {@code null} - */ - @SuppressWarnings("unchecked") - public static ResourceCache getOrCreateResourceCache(final String name, final Resource resource) { - CacheManager cacheManager = CacheManager.getInstance(); - if (!cacheManager.isMonitoringEnabled()) { - return new ResourceCache(resource, false); - } - - // Encode the resource object hash into the multimap key, so that caches with the same name coming from different resources - // are much less likely to collide (since the hash is not guaranteed to be unique per object, collisions are still possible) - final String key = name + CacheManager.KEY_SEPARATOR + Integer.toHexString(resource.hashCode()); - ArrayListMultimap>> caches = cacheManager.getCaches(); - synchronized (caches) { - for (WeakReference> matchReference : caches.get(key)) { - ICache match = matchReference.get(); - if ((match instanceof ResourceCache) && ((ResourceCache) match).handles(resource)) { - return (ResourceCache) match; - } - } - ResourceCache cache = new ResourceCache(resource, true); - caches.put(key, new WeakReference>(cache)); - return cache; - } - } - - public ResourceCache(final Resource resource, final boolean addAsListener) { - this.resource = resource; - backend = new OnChangeEvictingCache().getOrCreate(resource); - if (addAsListener) { - backend.addCacheListener(this); - } - } - - /** - * Returns a value from the cache. - * - * @param key - * the key, must not be {@code null} - * @return the value, or {@code null} if not found - */ - public V get(final K key) { - return backend.get(key); - } - - /** - * Stores a value in the cache. - * - * @param key - * the key, must not be {@code null} - * @param value - * the value, may be {@code null} - */ - public void set(final K key, final V value) { - backend.set(key, value); - } - - @Override - @SuppressWarnings("unchecked") - public CacheStatistics getStatistics() { - try { - Field hitsField = backend.getClass().getDeclaredField("hits"); //$NON-NLS-1$ - hitsField.setAccessible(true); - int hits = hitsField.getInt(backend); - - Field missesField = backend.getClass().getDeclaredField("misses"); //$NON-NLS-1$ - missesField.setAccessible(true); - int misses = missesField.getInt(backend); - - Field valuesField = backend.getClass().getDeclaredField("values"); //$NON-NLS-1$ - valuesField.setAccessible(true); - Map values = (Map) valuesField.get(backend); - - return new CacheStatistics(values.size(), hits, misses); - // CHECKSTYLE:OFF If getting statistics fails, we should just return an empty result - } catch (Exception e) { - // CHECKSTYLE:ON - return new CacheStatistics(); - } - } - - @Override - public void onEvict(final CacheAdapter cache) { - // This is needed to make the backend hold a reference to this cache, thus making their lifecycles match - } - - /** - * Checks whether this cache is attached to the given resource. - * - * @param newResource - * the resource to check against, must not me {@code null} - * @return true, if the cache is attached to the resource - */ - public boolean handles(final Resource newResource) { - return resource.equals(newResource); - } -} +/******************************************************************************* + * Copyright (c) 2016 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.resource; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.Map; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.util.OnChangeEvictingCache; +import org.eclipse.xtext.util.OnChangeEvictingCache.CacheAdapter; + +import com.avaloq.tools.ddk.caching.CacheManager; +import com.avaloq.tools.ddk.caching.CacheStatistics; +import com.avaloq.tools.ddk.caching.ICache; +import com.google.common.collect.ArrayListMultimap; + + +/** + * A cache whose lifecycle is associated to that of a resource. + * + * @param + * the key type + * @param + * the value type + */ +public class ResourceCache implements ICache, OnChangeEvictingCache.Listener { + + private final OnChangeEvictingCache.CacheAdapter backend; + private final Resource resource; + + /** + * Creates a new cache or retrieves an existing one associated with the given resource. + * + * @param + * the key type + * @param + * the value type + * @param name + * the name of the cache, must not be {@code null} + * @param resource + * the resource to associate the cache with, must not be {@code null} + * @return an existing or a new resource cache instance, never {@code null} + */ + @SuppressWarnings("unchecked") + public static ResourceCache getOrCreateResourceCache(final String name, final Resource resource) { + CacheManager cacheManager = CacheManager.getInstance(); + if (!cacheManager.isMonitoringEnabled()) { + return new ResourceCache(resource, false); + } + + // Encode the resource object hash into the multimap key, so that caches with the same name coming from different resources + // are much less likely to collide (since the hash is not guaranteed to be unique per object, collisions are still possible) + final String key = name + CacheManager.KEY_SEPARATOR + Integer.toHexString(resource.hashCode()); + ArrayListMultimap>> caches = cacheManager.getCaches(); + synchronized (caches) { + for (WeakReference> matchReference : caches.get(key)) { + ICache match = matchReference.get(); + if ((match instanceof ResourceCache) && ((ResourceCache) match).handles(resource)) { + return (ResourceCache) match; + } + } + ResourceCache cache = new ResourceCache(resource, true); + caches.put(key, new WeakReference>(cache)); + return cache; + } + } + + public ResourceCache(final Resource resource, final boolean addAsListener) { + this.resource = resource; + backend = new OnChangeEvictingCache().getOrCreate(resource); + if (addAsListener) { + backend.addCacheListener(this); + } + } + + /** + * Returns a value from the cache. + * + * @param key + * the key, must not be {@code null} + * @return the value, or {@code null} if not found + */ + public V get(final K key) { + return backend.get(key); + } + + /** + * Stores a value in the cache. + * + * @param key + * the key, must not be {@code null} + * @param value + * the value, may be {@code null} + */ + public void set(final K key, final V value) { + backend.set(key, value); + } + + @Override + @SuppressWarnings("unchecked") + public CacheStatistics getStatistics() { + try { + Field hitsField = backend.getClass().getDeclaredField("hits"); //$NON-NLS-1$ + hitsField.setAccessible(true); + int hits = hitsField.getInt(backend); + + Field missesField = backend.getClass().getDeclaredField("misses"); //$NON-NLS-1$ + missesField.setAccessible(true); + int misses = missesField.getInt(backend); + + Field valuesField = backend.getClass().getDeclaredField("values"); //$NON-NLS-1$ + valuesField.setAccessible(true); + Map values = (Map) valuesField.get(backend); + + return new CacheStatistics(values.size(), hits, misses); + // CHECKSTYLE:OFF If getting statistics fails, we should just return an empty result + } catch (Exception e) { + // CHECKSTYLE:ON + return new CacheStatistics(); + } + } + + @Override + public void onEvict(final CacheAdapter cache) { + // This is needed to make the backend hold a reference to this cache, thus making their lifecycles match + } + + /** + * Checks whether this cache is attached to the given resource. + * + * @param newResource + * the resource to check against, must not me {@code null} + * @return true, if the cache is attached to the resource + */ + public boolean handles(final Resource newResource) { + return resource.equals(newResource); + } +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceSetOptions.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceSetOptions.java index 4a1874841e..d0d5c62011 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceSetOptions.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/ResourceSetOptions.java @@ -1,64 +1,64 @@ -/******************************************************************************* - * Copyright (c) 2016 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 Evolution AG - initial API and implementation - *******************************************************************************/ - -package com.avaloq.tools.ddk.xtext.resource; - -import org.eclipse.emf.ecore.resource.ResourceSet; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; - - -/** - * A class to set and get DDK specific options over an EMF ResourceSet, similar to {@link org.eclipse.xtext.resource.ResourceSetContext}. - */ -@SuppressWarnings("nls") -public final class ResourceSetOptions { - - private static final String INSTALL_DERIVED_STATE = "com.avaloq.tools.ddk.xtext.resource.ResourceSetOptions.installDerivedState"; - private static final String SKIP_MODEL = "com.avaloq.tools.ddk.xtext.resource.ResourceSetOptions.skipModel"; - - private ResourceSetOptions() { - // utility class - } - - /** - * If the derived state should be installed. - * - * @param resourceSet - * the resource set - * @return true, If the derived state should be installed - */ - public static boolean installDerivedState(final @NonNull ResourceSet resourceSet) { - Object object = resourceSet.getLoadOptions().get(INSTALL_DERIVED_STATE); - return object == null || (boolean) object; - } - - /** - * Sets the install derived state property. - * - * @param resourceSet - * the resource set - * @param installDerivedState - * {@code true} if the derived state should be installed - */ - public static void setInstallDerivedState(final @NonNull ResourceSet resourceSet, final @Nullable Boolean installDerivedState) { - resourceSet.getLoadOptions().put(INSTALL_DERIVED_STATE, installDerivedState); - } - - public static boolean skipModel(final @NonNull ResourceSet resourceSet) { - Object object = resourceSet.getLoadOptions().get(SKIP_MODEL); - return object != null && (boolean) object; // default is false - } - - public static void setSkipModel(final @NonNull ResourceSet resourceSet, final @Nullable Boolean skipModel) { - resourceSet.getLoadOptions().put(SKIP_MODEL, skipModel); - } -} +/******************************************************************************* + * Copyright (c) 2016 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 Evolution AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.xtext.resource; + +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + + +/** + * A class to set and get DDK specific options over an EMF ResourceSet, similar to {@link org.eclipse.xtext.resource.ResourceSetContext}. + */ +@SuppressWarnings("nls") +public final class ResourceSetOptions { + + private static final String INSTALL_DERIVED_STATE = "com.avaloq.tools.ddk.xtext.resource.ResourceSetOptions.installDerivedState"; + private static final String SKIP_MODEL = "com.avaloq.tools.ddk.xtext.resource.ResourceSetOptions.skipModel"; + + private ResourceSetOptions() { + // utility class + } + + /** + * If the derived state should be installed. + * + * @param resourceSet + * the resource set + * @return true, If the derived state should be installed + */ + public static boolean installDerivedState(final @NonNull ResourceSet resourceSet) { + Object object = resourceSet.getLoadOptions().get(INSTALL_DERIVED_STATE); + return object == null || (boolean) object; + } + + /** + * Sets the install derived state property. + * + * @param resourceSet + * the resource set + * @param installDerivedState + * {@code true} if the derived state should be installed + */ + public static void setInstallDerivedState(final @NonNull ResourceSet resourceSet, final @Nullable Boolean installDerivedState) { + resourceSet.getLoadOptions().put(INSTALL_DERIVED_STATE, installDerivedState); + } + + public static boolean skipModel(final @NonNull ResourceSet resourceSet) { + Object object = resourceSet.getLoadOptions().get(SKIP_MODEL); + return object != null && (boolean) object; // default is false + } + + public static void setSkipModel(final @NonNull ResourceSet resourceSet, final @Nullable Boolean skipModel) { + resourceSet.getLoadOptions().put(SKIP_MODEL, skipModel); + } +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/AbstractResourceDescriptionsData.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/AbstractResourceDescriptionsData.java index 4cadd966a2..b61fa55f4a 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/AbstractResourceDescriptionsData.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/AbstractResourceDescriptionsData.java @@ -1,39 +1,39 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource.extensions; - -import java.util.Collections; -import java.util.Map; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; - - -/** - * Unfortunately, {@link ResourceDescriptionsData} does not implement {@link org.eclipse.xtext.resource.IResourceDescriptions IResourceDescriptions}... - */ -public abstract class AbstractResourceDescriptionsData extends ResourceDescriptionsData implements IResourceDescriptionsData { - - public AbstractResourceDescriptionsData() { - this(Collections. emptyList()); - } - - protected AbstractResourceDescriptionsData(final Iterable resourceDescriptions) { - super(resourceDescriptions); - } - - protected AbstractResourceDescriptionsData(final Map resourceDescriptionMap, final Map lookupMap) { - super(resourceDescriptionMap, lookupMap); - } - -} +/******************************************************************************* + * Copyright (c) 2016 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.resource.extensions; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; + + +/** + * Unfortunately, {@link ResourceDescriptionsData} does not implement {@link org.eclipse.xtext.resource.IResourceDescriptions IResourceDescriptions}... + */ +public abstract class AbstractResourceDescriptionsData extends ResourceDescriptionsData implements IResourceDescriptionsData { + + public AbstractResourceDescriptionsData() { + this(Collections. emptyList()); + } + + protected AbstractResourceDescriptionsData(final Iterable resourceDescriptions) { + super(resourceDescriptions); + } + + protected AbstractResourceDescriptionsData(final Map resourceDescriptionMap, final Map lookupMap) { + super(resourceDescriptionMap, lookupMap); + } + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/DelegatingResourceDescriptionsData.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/DelegatingResourceDescriptionsData.java index a72f18ea39..1d1757eadd 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/DelegatingResourceDescriptionsData.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/DelegatingResourceDescriptionsData.java @@ -1,158 +1,158 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource.extensions; - -import java.util.Set; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.resource.IEObjectDescription; -import org.eclipse.xtext.resource.IReferenceDescription; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; - -import com.google.common.collect.Sets; - - -/** - * A {@link ResourceDescriptionsData} that delegates all operations to another {@link ResourceDescriptionsData}. - */ -public class DelegatingResourceDescriptionsData extends AbstractResourceDescriptionsData implements IResourceDescriptionsData { - - private final ResourceDescriptionsData delegate; - private final IResourceDescriptions2 wrapped; - - public DelegatingResourceDescriptionsData(final Iterable descriptions) { - this(new ResourceDescriptionsData(descriptions)); - } - - public DelegatingResourceDescriptionsData(final ResourceDescriptionsData resourceDescriptionsData) { - super(null, null); - delegate = resourceDescriptionsData; - wrapped = new ResourceDescriptions2(delegate); - } - - @Override - public ResourceDescriptionsData copy() { - return new DelegatingResourceDescriptionsData(delegate.copy()); - } - - @Override - public boolean isEmpty() { - return delegate.isEmpty(); - } - - @Override - public Iterable getAllResourceDescriptions() { - return delegate.getAllResourceDescriptions(); - } - - @Override - public IResourceDescription getResourceDescription(final URI uri) { - return delegate.getResourceDescription(uri); - } - - @Override - public void removeDescription(final URI uri) { - delegate.removeDescription(uri); - } - - @Override - public Iterable getExportedObjects() { - return delegate.getExportedObjects(); - } - - @Override - public Iterable getExportedObjects(final EClass type, final QualifiedName qualifiedName, final boolean ignoreCase) { - return delegate.getExportedObjects(type, qualifiedName, ignoreCase); - } - - @Override - public Iterable getExportedObjectsByObject(final EObject object) { - return delegate.getExportedObjectsByObject(object); - } - - @Override - public Iterable getExportedObjectsByType(final EClass type) { - return delegate.getExportedObjectsByType(type); - } - - @Override - protected Iterable getSelectables() { - return delegate.getAllResourceDescriptions(); // delegate.getSelectables is not visible here?! - } - - @Override - public Set getAllURIs() { - return delegate.getAllURIs(); - } - - @Override - public void addDescription(final URI uri, final IResourceDescription newDescription) { - delegate.addDescription(uri, newDescription); - } - - @Override - public Iterable findAllReferencingResources(final Set targetResources, final ReferenceMatchPolicy matchPolicy) { - return wrapped.findAllReferencingResources(targetResources, matchPolicy); - } - - @Override - public Iterable findExactReferencingResources(final Set targetObjects, final ReferenceMatchPolicy matchPolicy) { - return wrapped.findExactReferencingResources(targetObjects, matchPolicy); - } - - @Override - public Iterable findReferencesToObjects(final Set targetObjects) { - return wrapped.findReferencesToObjects(targetObjects); - } - - @Override - public void importData(final Iterable descriptions) { - for (IResourceDescription desc : descriptions) { - delegate.addDescription(desc.getURI(), desc); - } - } - - @Override - public void clear() { - for (URI uri : Sets.newHashSet(delegate.getAllURIs())) { - delegate.removeDescription(uri); - } - } - - @Override - public void beginChanges() { - // No-op. - } - - @Override - public void flushChanges() { - // No-op. - } - - @Override - public void commitChanges() { - // No-op. - } - - @Override - public void rollbackChanges() { - // No-op. - } - - protected ResourceDescriptionsData getDelegate() { - return delegate; - } - -} +/******************************************************************************* + * Copyright (c) 2016 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.resource.extensions; + +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IReferenceDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; + +import com.google.common.collect.Sets; + + +/** + * A {@link ResourceDescriptionsData} that delegates all operations to another {@link ResourceDescriptionsData}. + */ +public class DelegatingResourceDescriptionsData extends AbstractResourceDescriptionsData implements IResourceDescriptionsData { + + private final ResourceDescriptionsData delegate; + private final IResourceDescriptions2 wrapped; + + public DelegatingResourceDescriptionsData(final Iterable descriptions) { + this(new ResourceDescriptionsData(descriptions)); + } + + public DelegatingResourceDescriptionsData(final ResourceDescriptionsData resourceDescriptionsData) { + super(null, null); + delegate = resourceDescriptionsData; + wrapped = new ResourceDescriptions2(delegate); + } + + @Override + public ResourceDescriptionsData copy() { + return new DelegatingResourceDescriptionsData(delegate.copy()); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterable getAllResourceDescriptions() { + return delegate.getAllResourceDescriptions(); + } + + @Override + public IResourceDescription getResourceDescription(final URI uri) { + return delegate.getResourceDescription(uri); + } + + @Override + public void removeDescription(final URI uri) { + delegate.removeDescription(uri); + } + + @Override + public Iterable getExportedObjects() { + return delegate.getExportedObjects(); + } + + @Override + public Iterable getExportedObjects(final EClass type, final QualifiedName qualifiedName, final boolean ignoreCase) { + return delegate.getExportedObjects(type, qualifiedName, ignoreCase); + } + + @Override + public Iterable getExportedObjectsByObject(final EObject object) { + return delegate.getExportedObjectsByObject(object); + } + + @Override + public Iterable getExportedObjectsByType(final EClass type) { + return delegate.getExportedObjectsByType(type); + } + + @Override + protected Iterable getSelectables() { + return delegate.getAllResourceDescriptions(); // delegate.getSelectables is not visible here?! + } + + @Override + public Set getAllURIs() { + return delegate.getAllURIs(); + } + + @Override + public void addDescription(final URI uri, final IResourceDescription newDescription) { + delegate.addDescription(uri, newDescription); + } + + @Override + public Iterable findAllReferencingResources(final Set targetResources, final ReferenceMatchPolicy matchPolicy) { + return wrapped.findAllReferencingResources(targetResources, matchPolicy); + } + + @Override + public Iterable findExactReferencingResources(final Set targetObjects, final ReferenceMatchPolicy matchPolicy) { + return wrapped.findExactReferencingResources(targetObjects, matchPolicy); + } + + @Override + public Iterable findReferencesToObjects(final Set targetObjects) { + return wrapped.findReferencesToObjects(targetObjects); + } + + @Override + public void importData(final Iterable descriptions) { + for (IResourceDescription desc : descriptions) { + delegate.addDescription(desc.getURI(), desc); + } + } + + @Override + public void clear() { + for (URI uri : Sets.newHashSet(delegate.getAllURIs())) { + delegate.removeDescription(uri); + } + } + + @Override + public void beginChanges() { + // No-op. + } + + @Override + public void flushChanges() { + // No-op. + } + + @Override + public void commitChanges() { + // No-op. + } + + @Override + public void rollbackChanges() { + // No-op. + } + + protected ResourceDescriptionsData getDelegate() { + return delegate; + } + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/IResourceDescriptionsData.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/IResourceDescriptionsData.java index 128206627a..a0116e7f75 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/IResourceDescriptionsData.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/IResourceDescriptionsData.java @@ -1,83 +1,83 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource.extensions; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; - - -/** - * Marker interface; should be implemented by {@link org.eclipse.xtext.builder.builderState.ResourceDescriptionsData ResourceDescriptionsData}. - */ -public interface IResourceDescriptionsData extends IResourceDescriptions2 { - - /** - * Add a new resource description. - * - * @param uri - * of the resource - * @param newDescription - * the description - */ - void addDescription(URI uri, IResourceDescription newDescription); - - /** - * Remove a resource description. - * - * @param uri - * of the resource whose description shall be removed. - */ - void removeDescription(URI uri); - - /** - * Create a copy of this index. - * - * @return the copied index. - */ - ResourceDescriptionsData copy(); - - /** - * Include all the given resource descriptions into this index, i.e. into the internal resource-description data representation by the one supplied as - * {@link IResourceDescription}s, usually from an externally serialized - * representation. - * - * @param descriptions - * The {@link IResourceDescription}s to add. - */ - void importData(Iterable descriptions); - - /** - * Remove all resource descriptions from this index. - */ - void clear(); - - /** - * Indicate that the index will be changed. Subsequent changes are guaranteed to be permanent only after the next call to {@link commitChanges}. - */ - void beginChanges(); - - /** - * Flushes the currently buffered changes without committing them to make them available to queries within this transaction. The implementation is also - * permitted to make the buffered changes available to queries before this method is called. - */ - void flushChanges(); - - /** - * Make all pending changes in this index permanent. - */ - void commitChanges(); - - /** - * Rollback all pending changes. - */ - void rollbackChanges(); -} +/******************************************************************************* + * Copyright (c) 2016 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.resource.extensions; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; + + +/** + * Marker interface; should be implemented by {@link org.eclipse.xtext.builder.builderState.ResourceDescriptionsData ResourceDescriptionsData}. + */ +public interface IResourceDescriptionsData extends IResourceDescriptions2 { + + /** + * Add a new resource description. + * + * @param uri + * of the resource + * @param newDescription + * the description + */ + void addDescription(URI uri, IResourceDescription newDescription); + + /** + * Remove a resource description. + * + * @param uri + * of the resource whose description shall be removed. + */ + void removeDescription(URI uri); + + /** + * Create a copy of this index. + * + * @return the copied index. + */ + ResourceDescriptionsData copy(); + + /** + * Include all the given resource descriptions into this index, i.e. into the internal resource-description data representation by the one supplied as + * {@link IResourceDescription}s, usually from an externally serialized + * representation. + * + * @param descriptions + * The {@link IResourceDescription}s to add. + */ + void importData(Iterable descriptions); + + /** + * Remove all resource descriptions from this index. + */ + void clear(); + + /** + * Indicate that the index will be changed. Subsequent changes are guaranteed to be permanent only after the next call to {@link commitChanges}. + */ + void beginChanges(); + + /** + * Flushes the currently buffered changes without committing them to make them available to queries within this transaction. The implementation is also + * permitted to make the buffered changes available to queries before this method is called. + */ + void flushChanges(); + + /** + * Make all pending changes in this index permanent. + */ + void commitChanges(); + + /** + * Rollback all pending changes. + */ + void rollbackChanges(); +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/ResourceDescriptions2.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/ResourceDescriptions2.java index 6459102664..4c5f7a4b93 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/ResourceDescriptions2.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/extensions/ResourceDescriptions2.java @@ -1,101 +1,101 @@ -/******************************************************************************* - * Copyright (c) 2016 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.resource.extensions; - -import java.util.Set; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.resource.IEObjectDescription; -import org.eclipse.xtext.resource.IReferenceDescription; -import org.eclipse.xtext.resource.IResourceDescription; -import org.eclipse.xtext.resource.IResourceDescriptions; -import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; - - -/** - * Wrapper to convert old-style {@link IResourceDescriptions} into new-style {@link IResourceDescriptions2}. The new operations are implemented as simple - * filtered iterations over {@link IResourceDescriptions#getAllResourceDescriptions getAllResourceDescriptions}. - */ -public class ResourceDescriptions2 implements IResourceDescriptions2 { - - private final IResourceDescriptions delegate; - - public ResourceDescriptions2(final IResourceDescriptions data) { - delegate = data; - } - - public ResourceDescriptions2(final ResourceDescriptionsData data) { - delegate = data; - } - - @Override - public Iterable getAllResourceDescriptions() { - return delegate.getAllResourceDescriptions(); - } - - @Override - public IResourceDescription getResourceDescription(final URI normalizedURI) { - return delegate.getResourceDescription(normalizedURI); - } - - @Override - public boolean isEmpty() { - return delegate.isEmpty(); - } - - @Override - public Iterable getExportedObjects() { - return delegate.getExportedObjects(); - } - - @Override - public Iterable getExportedObjects(final EClass type, final QualifiedName name, final boolean ignoreCase) { - return delegate.getExportedObjects(type, name, ignoreCase); - } - - @Override - public Iterable getExportedObjectsByType(final EClass type) { - return delegate.getExportedObjectsByType(type); - } - - @Override - public Iterable getExportedObjectsByObject(final EObject object) { - return delegate.getExportedObjectsByObject(object); - } - - @Override - public Set getAllURIs() { - return ResourceDescriptionsUtil.getAllURIs(delegate); - } - - @Override - public Iterable findAllReferencingResources(final Set targetResources, final ReferenceMatchPolicy matchPolicy) { - return ResourceDescriptionsUtil.findReferencesToResources(delegate, targetResources, matchPolicy); - } - - @Override - public Iterable findExactReferencingResources(final Set targetObjects, final ReferenceMatchPolicy matchPolicy) { - return ResourceDescriptionsUtil.findExactReferencingResources(delegate, targetObjects, matchPolicy); - } - - @Override - public Iterable findReferencesToObjects(final Set targetObjects) { - return ResourceDescriptionsUtil.findReferencesToObjects(delegate, targetObjects); - } - - public Iterable findExactReferencingResources(final Set targetObjects, final boolean matchImportedNames) { - return null; - } - -} +/******************************************************************************* + * Copyright (c) 2016 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.resource.extensions; + +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.IReferenceDescription; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceDescriptions; +import org.eclipse.xtext.resource.impl.ResourceDescriptionsData; + + +/** + * Wrapper to convert old-style {@link IResourceDescriptions} into new-style {@link IResourceDescriptions2}. The new operations are implemented as simple + * filtered iterations over {@link IResourceDescriptions#getAllResourceDescriptions getAllResourceDescriptions}. + */ +public class ResourceDescriptions2 implements IResourceDescriptions2 { + + private final IResourceDescriptions delegate; + + public ResourceDescriptions2(final IResourceDescriptions data) { + delegate = data; + } + + public ResourceDescriptions2(final ResourceDescriptionsData data) { + delegate = data; + } + + @Override + public Iterable getAllResourceDescriptions() { + return delegate.getAllResourceDescriptions(); + } + + @Override + public IResourceDescription getResourceDescription(final URI normalizedURI) { + return delegate.getResourceDescription(normalizedURI); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterable getExportedObjects() { + return delegate.getExportedObjects(); + } + + @Override + public Iterable getExportedObjects(final EClass type, final QualifiedName name, final boolean ignoreCase) { + return delegate.getExportedObjects(type, name, ignoreCase); + } + + @Override + public Iterable getExportedObjectsByType(final EClass type) { + return delegate.getExportedObjectsByType(type); + } + + @Override + public Iterable getExportedObjectsByObject(final EObject object) { + return delegate.getExportedObjectsByObject(object); + } + + @Override + public Set getAllURIs() { + return ResourceDescriptionsUtil.getAllURIs(delegate); + } + + @Override + public Iterable findAllReferencingResources(final Set targetResources, final ReferenceMatchPolicy matchPolicy) { + return ResourceDescriptionsUtil.findReferencesToResources(delegate, targetResources, matchPolicy); + } + + @Override + public Iterable findExactReferencingResources(final Set targetObjects, final ReferenceMatchPolicy matchPolicy) { + return ResourceDescriptionsUtil.findExactReferencingResources(delegate, targetObjects, matchPolicy); + } + + @Override + public Iterable findReferencesToObjects(final Set targetObjects) { + return ResourceDescriptionsUtil.findReferencesToObjects(delegate, targetObjects); + } + + public Iterable findExactReferencingResources(final Set targetObjects, final boolean matchImportedNames) { + return null; + } + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/persistence/NonLockingBufferInputStream.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/persistence/NonLockingBufferInputStream.java index 0c2c0876b8..8a991216f7 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/persistence/NonLockingBufferInputStream.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/persistence/NonLockingBufferInputStream.java @@ -1,288 +1,288 @@ -/******************************************************************************* - * Copyright (c) 2024 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.resource.persistence; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.Objects; - - -/** - * Like {@code BufferedInputStream} without supporting concurrency. - */ -@SuppressWarnings("nls") -public class NonLockingBufferInputStream extends FilterInputStream { - - private static final int DEFAULT_BUFFER_SIZE = 8192; - private static final byte[] EMPTY = new byte[0]; - private final int initialSize; - - private byte[] buf; - private int count; - private int pos; - private int markpos = -1; - private int marklimit; - - private byte[] getBufIfOpen(final boolean allocateIfEmpty) throws IOException { - if (allocateIfEmpty && buf == EMPTY) { - buf = new byte[initialSize]; - } - return buf; - } - - private byte[] getBufIfOpen() throws IOException { - return getBufIfOpen(true); - } - - private void ensureOpen() throws IOException { - if (buf == null) { - throw new IOException("Stream closed"); - } - } - - /** - * Creates a {@code NonLockingBufferInputStream}. - * - * @param in - * the underlying input stream. - */ - public NonLockingBufferInputStream(final InputStream in) { - this(in, DEFAULT_BUFFER_SIZE); - } - - /** - * Creates a {@code NonLockingBufferInputStream} - * with the specified buffer size. - * - * @param in - * the underlying input stream. - * @param size - * the buffer size. - * @throws IllegalArgumentException - * if {@code size <= 0}. - */ - public NonLockingBufferInputStream(final InputStream in, final int size) { - super(in); - if (size <= 0) { - throw new IllegalArgumentException("Buffer size <= 0"); - } - initialSize = size; - buf = new byte[size]; - } - - private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; - - private static int newLength(final int oldLength, final int minGrowth, final int prefGrowth) { - int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow - if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { - return prefLength; - } else { - return hugeLength(oldLength, minGrowth); - } - } - - private static int hugeLength(final int oldLength, final int minGrowth) { - int minLength = oldLength + minGrowth; - if (minLength < 0) { // overflow - throw new OutOfMemoryError("Required array length " + oldLength + " + " + minGrowth + " is too large"); - } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) { - return SOFT_MAX_ARRAY_LENGTH; - } else { - return minLength; - } - } - - private void fill() throws IOException { - byte[] buffer = getBufIfOpen(); - if (markpos == -1) { - pos = 0; /* no mark: throw away the buffer */ - } else if (pos >= buffer.length) { /* no room left in buffer */ - if (markpos > 0) { /* can throw away early part of the buffer */ - int sz = pos - markpos; - System.arraycopy(buffer, markpos, buffer, 0, sz); - pos = sz; - markpos = 0; - } else if (buffer.length >= marklimit) { - markpos = -1; /* buffer got too big, invalidate mark */ - pos = 0; /* drop buffer contents */ - } else { /* grow buffer */ - int nsz = newLength(pos, 1, pos); - if (nsz > marklimit) { - nsz = marklimit; - } - byte[] nbuf = new byte[nsz]; - System.arraycopy(buffer, 0, nbuf, 0, pos); - buffer = nbuf; - } - } - count = pos; - int n = in.read(buffer, pos, buffer.length - pos); - if (n > 0) { - count = n + pos; - } - } - - @Override - @SuppressWarnings("checkstyle:MagicNumberCheck") - public int read() throws IOException { - if (pos >= count) { - fill(); - if (pos >= count) { - return -1; - } - } - return getBufIfOpen()[pos++] & 0xff; - } - - private int read1(final byte[] b, final int off, final int len) throws IOException { - int avail = count - pos; - if (avail <= 0) { - /* - * If the requested length is at least as large as the buffer, and - * if there is no mark/reset activity, do not bother to copy the - * bytes into the local buffer. In this way buffered streams will - * cascade harmlessly. - */ - int size = Math.max(getBufIfOpen(false).length, initialSize); - if (len >= size && markpos == -1) { - return in.read(b, off, len); - } - fill(); - avail = count - pos; - if (avail <= 0) { - return -1; - } - } - int cnt = (avail < len) ? avail : len; - System.arraycopy(getBufIfOpen(), pos, b, off, cnt); - pos += cnt; - return cnt; - } - - @Override - public int read(final byte[] b, final int off, final int len) throws IOException { - ensureOpen(); - if ((off | len | (off + len) | (b.length - (off + len))) < 0) { - throw new IndexOutOfBoundsException(); - } else if (len == 0) { - return 0; - } - - int n = 0; - for (;;) { - int nread = read1(b, off + n, len - n); - if (nread <= 0) { - return (n == 0) ? nread : n; - } - n += nread; - if (n >= len) { - return n; - } - // if not closed but no bytes available, return - InputStream input = in; - if (input != null && input.available() <= 0) { - return n; - } - } - } - - @Override - public long skip(final long n) throws IOException { - ensureOpen(); - if (n <= 0) { - return 0; - } - long avail = count - pos; - - if (avail <= 0) { - // If no mark position set then don't keep in buffer - if (markpos == -1) { - return in.skip(n); - } - - // Fill in buffer to save bytes for reset - fill(); - avail = count - pos; - if (avail <= 0) { - return 0; - } - } - - long skipped = (avail < n) ? avail : n; - pos += (int) skipped; - return skipped; - } - - @Override - public int available() throws IOException { - int n = count - pos; - int avail = in.available(); - return n > (Integer.MAX_VALUE - avail) ? Integer.MAX_VALUE : n + avail; - } - - @Override - public void mark(final int readlimit) { - marklimit = readlimit; - markpos = pos; - } - - @Override - public void reset() throws IOException { - ensureOpen(); - if (markpos < 0) { - throw new IOException("Resetting to invalid mark"); - } - pos = markpos; - } - - @Override - public boolean markSupported() { - return true; - } - - @Override - public void close() throws IOException { - while (buf != null) { - buf = null; - InputStream input = in; - in = null; - if (input != null) { - input.close(); - } - return; - } - } - - @Override - public long transferTo(final OutputStream out) throws IOException { - Objects.requireNonNull(out, "out"); - if (markpos == -1) { - int avail = count - pos; - if (avail > 0) { - // Prevent poisoning and leaking of buf - byte[] buffer = Arrays.copyOfRange(getBufIfOpen(), pos, count); - out.write(buffer); - pos = count; - } - try { - return Math.addExact(avail, in.transferTo(out)); - } catch (ArithmeticException ignore) { - return Long.MAX_VALUE; - } - } else { - return super.transferTo(out); - } - } - -} +/******************************************************************************* + * Copyright (c) 2024 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.resource.persistence; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Objects; + + +/** + * Like {@code BufferedInputStream} without supporting concurrency. + */ +@SuppressWarnings("nls") +public class NonLockingBufferInputStream extends FilterInputStream { + + private static final int DEFAULT_BUFFER_SIZE = 8192; + private static final byte[] EMPTY = new byte[0]; + private final int initialSize; + + private byte[] buf; + private int count; + private int pos; + private int markpos = -1; + private int marklimit; + + private byte[] getBufIfOpen(final boolean allocateIfEmpty) throws IOException { + if (allocateIfEmpty && buf == EMPTY) { + buf = new byte[initialSize]; + } + return buf; + } + + private byte[] getBufIfOpen() throws IOException { + return getBufIfOpen(true); + } + + private void ensureOpen() throws IOException { + if (buf == null) { + throw new IOException("Stream closed"); + } + } + + /** + * Creates a {@code NonLockingBufferInputStream}. + * + * @param in + * the underlying input stream. + */ + public NonLockingBufferInputStream(final InputStream in) { + this(in, DEFAULT_BUFFER_SIZE); + } + + /** + * Creates a {@code NonLockingBufferInputStream} + * with the specified buffer size. + * + * @param in + * the underlying input stream. + * @param size + * the buffer size. + * @throws IllegalArgumentException + * if {@code size <= 0}. + */ + public NonLockingBufferInputStream(final InputStream in, final int size) { + super(in); + if (size <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + initialSize = size; + buf = new byte[size]; + } + + private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; + + private static int newLength(final int oldLength, final int minGrowth, final int prefGrowth) { + int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow + if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { + return prefLength; + } else { + return hugeLength(oldLength, minGrowth); + } + } + + private static int hugeLength(final int oldLength, final int minGrowth) { + int minLength = oldLength + minGrowth; + if (minLength < 0) { // overflow + throw new OutOfMemoryError("Required array length " + oldLength + " + " + minGrowth + " is too large"); + } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) { + return SOFT_MAX_ARRAY_LENGTH; + } else { + return minLength; + } + } + + private void fill() throws IOException { + byte[] buffer = getBufIfOpen(); + if (markpos == -1) { + pos = 0; /* no mark: throw away the buffer */ + } else if (pos >= buffer.length) { /* no room left in buffer */ + if (markpos > 0) { /* can throw away early part of the buffer */ + int sz = pos - markpos; + System.arraycopy(buffer, markpos, buffer, 0, sz); + pos = sz; + markpos = 0; + } else if (buffer.length >= marklimit) { + markpos = -1; /* buffer got too big, invalidate mark */ + pos = 0; /* drop buffer contents */ + } else { /* grow buffer */ + int nsz = newLength(pos, 1, pos); + if (nsz > marklimit) { + nsz = marklimit; + } + byte[] nbuf = new byte[nsz]; + System.arraycopy(buffer, 0, nbuf, 0, pos); + buffer = nbuf; + } + } + count = pos; + int n = in.read(buffer, pos, buffer.length - pos); + if (n > 0) { + count = n + pos; + } + } + + @Override + @SuppressWarnings("checkstyle:MagicNumberCheck") + public int read() throws IOException { + if (pos >= count) { + fill(); + if (pos >= count) { + return -1; + } + } + return getBufIfOpen()[pos++] & 0xff; + } + + private int read1(final byte[] b, final int off, final int len) throws IOException { + int avail = count - pos; + if (avail <= 0) { + /* + * If the requested length is at least as large as the buffer, and + * if there is no mark/reset activity, do not bother to copy the + * bytes into the local buffer. In this way buffered streams will + * cascade harmlessly. + */ + int size = Math.max(getBufIfOpen(false).length, initialSize); + if (len >= size && markpos == -1) { + return in.read(b, off, len); + } + fill(); + avail = count - pos; + if (avail <= 0) { + return -1; + } + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(getBufIfOpen(), pos, b, off, cnt); + pos += cnt; + return cnt; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + ensureOpen(); + if ((off | len | (off + len) | (b.length - (off + len))) < 0) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int n = 0; + for (;;) { + int nread = read1(b, off + n, len - n); + if (nread <= 0) { + return (n == 0) ? nread : n; + } + n += nread; + if (n >= len) { + return n; + } + // if not closed but no bytes available, return + InputStream input = in; + if (input != null && input.available() <= 0) { + return n; + } + } + } + + @Override + public long skip(final long n) throws IOException { + ensureOpen(); + if (n <= 0) { + return 0; + } + long avail = count - pos; + + if (avail <= 0) { + // If no mark position set then don't keep in buffer + if (markpos == -1) { + return in.skip(n); + } + + // Fill in buffer to save bytes for reset + fill(); + avail = count - pos; + if (avail <= 0) { + return 0; + } + } + + long skipped = (avail < n) ? avail : n; + pos += (int) skipped; + return skipped; + } + + @Override + public int available() throws IOException { + int n = count - pos; + int avail = in.available(); + return n > (Integer.MAX_VALUE - avail) ? Integer.MAX_VALUE : n + avail; + } + + @Override + public void mark(final int readlimit) { + marklimit = readlimit; + markpos = pos; + } + + @Override + public void reset() throws IOException { + ensureOpen(); + if (markpos < 0) { + throw new IOException("Resetting to invalid mark"); + } + pos = markpos; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void close() throws IOException { + while (buf != null) { + buf = null; + InputStream input = in; + in = null; + if (input != null) { + input.close(); + } + return; + } + } + + @Override + public long transferTo(final OutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + if (markpos == -1) { + int avail = count - pos; + if (avail > 0) { + // Prevent poisoning and leaking of buf + byte[] buffer = Arrays.copyOfRange(getBufIfOpen(), pos, count); + out.write(buffer); + pos = count; + } + try { + return Math.addExact(avail, in.transferTo(out)); + } catch (ArithmeticException ignore) { + return Long.MAX_VALUE; + } + } else { + return super.transferTo(out); + } + } + +} diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/tracing/ResourceIndexAccessSummaryEvent.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/tracing/ResourceIndexAccessSummaryEvent.java index 3749e20787..3e4fe5f2f9 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/tracing/ResourceIndexAccessSummaryEvent.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/tracing/ResourceIndexAccessSummaryEvent.java @@ -1,31 +1,31 @@ -/******************************************************************************* - * Copyright (c) 2016 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.tracing; - -/** - * Event which contains index access statistics for a given resource. This event will typically have a {@link ResourceProcessingEvent} as its parent, but it - * could also be a more specific event like a {@link ResourceValidationEvent}. - */ -public class ResourceIndexAccessSummaryEvent extends ResourceEvent { - - /** - * Creates a new instance of {@link ResourceIndexAccessSummaryEvent}. - * - * @param trigger - * event trigger - * @param data - * event data, where the first data object is expected to be the resource's {@link org.eclipse.emf.common.util.URI} this event pertains to - */ - public ResourceIndexAccessSummaryEvent(final Trigger trigger, final Object... data) { - super(trigger, data); - } - -} +/******************************************************************************* + * Copyright (c) 2016 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.tracing; + +/** + * Event which contains index access statistics for a given resource. This event will typically have a {@link ResourceProcessingEvent} as its parent, but it + * could also be a more specific event like a {@link ResourceValidationEvent}. + */ +public class ResourceIndexAccessSummaryEvent extends ResourceEvent { + + /** + * Creates a new instance of {@link ResourceIndexAccessSummaryEvent}. + * + * @param trigger + * event trigger + * @param data + * event data, where the first data object is expected to be the resource's {@link org.eclipse.emf.common.util.URI} this event pertains to + */ + public ResourceIndexAccessSummaryEvent(final Trigger trigger, final Object... data) { + super(trigger, data); + } + +} diff --git a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheConfiguration.java b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheConfiguration.java index d16b02d40d..e42b90bdb1 100644 --- a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheConfiguration.java +++ b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheConfiguration.java @@ -1,143 +1,143 @@ -/******************************************************************************* - * Copyright (c) 2016 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.caching; - -/** - * Allows the behaviour of a cache to be configured. - */ -public class CacheConfiguration { - static final int UNSET_INT = -1; - - private boolean arraySize; - private boolean softValues; - private boolean weakKeys; - private boolean weakValues; - private boolean statistics; - - private long cacheSize = UNSET_INT; - private int initialCapacity = UNSET_INT; - private int concurrencyLevel = UNSET_INT; - - /** - * All values stored in the cache are wrapped as {@link java.lang.ref.SoftReference SoftReference}, allowing them to be garbage collected as necessary. - * - * @return the cache configuration - */ - public CacheConfiguration useSoftValues() { - softValues = true; - return this; - } - - /** - * All values stored in the cache are wrapped as {@link java.lang.ref.SoftReference WeakReference}, allowing them to be garbage collected as necessary. - * - * @return the cache configuration - */ - public CacheConfiguration useWeakValues() { - weakValues = true; - return this; - } - - /** - * All keys stored in the cache are wrapped as {@link java.lang.ref.SoftReference WeakReference}, allowing them to be garbage collected as necessary. - * - * @return the cache configuration - */ - public CacheConfiguration useWeakKeys() { - weakKeys = true; - return this; - } - - /** - * If enabled, the cache will treat the values as arrays and count their total entries when determining the maximum number of allowed entries. - * - * @return the cache configuration - */ - public CacheConfiguration useArraySize() { - arraySize = true; - return this; - } - - /** - * Enables the collection of statistics for the cache, with a possible performance penalty. - * - * @return the cache configuration - */ - public CacheConfiguration enableStatistics() { - statistics = true; - return this; - } - - /** - * Sets the maximum number of entries that can be stored in the cache, before old entries get evicted. - * - * @param size - * the maximum size - * @return the cache configuration - */ - public CacheConfiguration setMaximumSize(final long size) { - cacheSize = size; - return this; - } - - public boolean isSoftValuesEnabled() { - return softValues; - } - - public boolean isWeakValuesEnabled() { - return weakValues; - } - - public boolean isWeakKeysEnabled() { - return weakKeys; - } - - public boolean isStatisticsEnabled() { - return statistics; - } - - public long getMaximumSize() { - return cacheSize; - } - - public boolean isArraySizeEnabled() { - return arraySize; - } - - /** - * Sets the initial size for the cache. - * - * @param capacity - * the initial capacity - * @return the cache configuration - */ - public CacheConfiguration setInitialCapacity(final int capacity) { - initialCapacity = capacity; - return this; - } - - public int getInitialCapacity() { - return initialCapacity; - } - - public int getConcurrencyLevel() { - return concurrencyLevel; - } - - /** - * Setsthe concurrency level for the cache. Guava concurrent maps creates as many segments - * as the specified concurrency level, each segment having its own lock. - * If not set, a reasonable default is deternimed by the library according to the number of CPUs. - */ - public void setConcurrencyLevel(final int concurrencyLevel) { - this.concurrencyLevel = concurrencyLevel; - } -} +/******************************************************************************* + * Copyright (c) 2016 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.caching; + +/** + * Allows the behaviour of a cache to be configured. + */ +public class CacheConfiguration { + static final int UNSET_INT = -1; + + private boolean arraySize; + private boolean softValues; + private boolean weakKeys; + private boolean weakValues; + private boolean statistics; + + private long cacheSize = UNSET_INT; + private int initialCapacity = UNSET_INT; + private int concurrencyLevel = UNSET_INT; + + /** + * All values stored in the cache are wrapped as {@link java.lang.ref.SoftReference SoftReference}, allowing them to be garbage collected as necessary. + * + * @return the cache configuration + */ + public CacheConfiguration useSoftValues() { + softValues = true; + return this; + } + + /** + * All values stored in the cache are wrapped as {@link java.lang.ref.SoftReference WeakReference}, allowing them to be garbage collected as necessary. + * + * @return the cache configuration + */ + public CacheConfiguration useWeakValues() { + weakValues = true; + return this; + } + + /** + * All keys stored in the cache are wrapped as {@link java.lang.ref.SoftReference WeakReference}, allowing them to be garbage collected as necessary. + * + * @return the cache configuration + */ + public CacheConfiguration useWeakKeys() { + weakKeys = true; + return this; + } + + /** + * If enabled, the cache will treat the values as arrays and count their total entries when determining the maximum number of allowed entries. + * + * @return the cache configuration + */ + public CacheConfiguration useArraySize() { + arraySize = true; + return this; + } + + /** + * Enables the collection of statistics for the cache, with a possible performance penalty. + * + * @return the cache configuration + */ + public CacheConfiguration enableStatistics() { + statistics = true; + return this; + } + + /** + * Sets the maximum number of entries that can be stored in the cache, before old entries get evicted. + * + * @param size + * the maximum size + * @return the cache configuration + */ + public CacheConfiguration setMaximumSize(final long size) { + cacheSize = size; + return this; + } + + public boolean isSoftValuesEnabled() { + return softValues; + } + + public boolean isWeakValuesEnabled() { + return weakValues; + } + + public boolean isWeakKeysEnabled() { + return weakKeys; + } + + public boolean isStatisticsEnabled() { + return statistics; + } + + public long getMaximumSize() { + return cacheSize; + } + + public boolean isArraySizeEnabled() { + return arraySize; + } + + /** + * Sets the initial size for the cache. + * + * @param capacity + * the initial capacity + * @return the cache configuration + */ + public CacheConfiguration setInitialCapacity(final int capacity) { + initialCapacity = capacity; + return this; + } + + public int getInitialCapacity() { + return initialCapacity; + } + + public int getConcurrencyLevel() { + return concurrencyLevel; + } + + /** + * Setsthe concurrency level for the cache. Guava concurrent maps creates as many segments + * as the specified concurrency level, each segment having its own lock. + * If not set, a reasonable default is deternimed by the library according to the number of CPUs. + */ + public void setConcurrencyLevel(final int concurrencyLevel) { + this.concurrencyLevel = concurrencyLevel; + } +} diff --git a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheManager.java b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheManager.java index 736bf0fd15..6b199d348a 100644 --- a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheManager.java +++ b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheManager.java @@ -1,257 +1,257 @@ -/******************************************************************************* - * Copyright (c) 2016 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.caching; - -import java.lang.ref.WeakReference; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeSet; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import com.google.common.cache.CacheLoader; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Maps; -import com.google.common.util.concurrent.ThreadFactoryBuilder; - - -/** - * Instantiates and keeps track of Caches, allowing statistics to be collected and reported. - */ -public class CacheManager { - private static final Logger LOGGER = LogManager.getLogger(CacheManager.class); - - private static final class SingletonHolder { - private static CacheManager instance = new CacheManager(); - - public static CacheManager get() { - return instance; - } - } - - private static final int CLEANUP_DELAY = 5; - private static final int REPORT_DELAY = 10; - - private static final int PERCENT = 100; - - public static final String KEY_SEPARATOR = "§"; //$NON-NLS-1$ - - private final ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); - private final ArrayListMultimap>> caches = ArrayListMultimap.create(); - private final boolean monitoringEnabled; - - protected CacheManager() { - monitoringEnabled = Boolean.getBoolean("com.avaloq.tools.ddk.caching.EnableMonitor"); //$NON-NLS-1$ - if (isMonitoringEnabled()) { - threadPool.scheduleWithFixedDelay(referenceCleaner, CLEANUP_DELAY, CLEANUP_DELAY, TimeUnit.SECONDS); - threadPool.scheduleWithFixedDelay(reportPrinter, REPORT_DELAY, REPORT_DELAY, TimeUnit.SECONDS); - } - } - - public static CacheManager getInstance() { - return SingletonHolder.get(); - } - - /** - * Creates a new cache with the default configuration. - * - * @param - * the key type - * @param - * the value type - * @param name - * the name of the cache, must not be {@code null} - * @return a new Cache instance, never {@code null} - */ - @SuppressWarnings("PMD.LooseCoupling") - public MapCache createMapCache(final String name) { - return createMapCache(name, new CacheConfiguration()); - } - - /** - * Creates a new cache with the given configuration. - * - * @param - * the key type - * @param - * the value type - * @param name - * the name of the cache, must not be {@code null} - * @param configuration - * the configuration of the cache instance to be created, must not be {@code null} - * @return a new cache instance, never {@code null} - */ - @SuppressWarnings("PMD.LooseCoupling") - public MapCache createMapCache(final String name, final CacheConfiguration configuration) { - if (isMonitoringEnabled()) { - configuration.enableStatistics(); - } - - MapCache cache = new MapCache(name, configuration); - if (isMonitoringEnabled()) { - synchronized (getCaches()) { - getCaches().put(name, new WeakReference>(cache)); - } - } - return cache; - } - - /** - * Creates a new cache with the given configuration. - * - * @param - * the key type - * @param - * the value type - * @param name - * the name of the cache, must not be {@code null} - * @param configuration - * the configuration of the cache instance to be created, must not be {@code null} - * @param loader - * the cache loader used to obtain new values - * @return a new cache instance, never {@code null} - */ - @SuppressWarnings("PMD.LooseCoupling") - public MapCache createMapCache(final String name, final CacheConfiguration configuration, final CacheLoader loader) { - if (isMonitoringEnabled()) { - configuration.enableStatistics(); - } - - MapCache cache = new MapCache(name, configuration, loader); - if (isMonitoringEnabled()) { - synchronized (getCaches()) { - getCaches().put(name, new WeakReference>(cache)); - } - } - return cache; - } - - /** - * Returns the current statistics for the cache with the given name. - * - * @param name - * name of the cache, must not be {@code null} - * @return statistics, never {@code null} - */ - public CacheStatistics getStatistics(final String name) { - CacheStatistics result = new MultiCacheStatistics(); - if (isMonitoringEnabled()) { - synchronized (getCaches()) { - for (WeakReference> reference : getCaches().get(name)) { - ICache value = reference.get(); - if (value != null) { - result.aggregate(value.getStatistics()); - } - } - } - } - return result; - } - - public boolean isMonitoringEnabled() { - return monitoringEnabled; - } - - public ArrayListMultimap>> getCaches() { - return caches; - } - - // Task to print the a cache usage report - @SuppressWarnings("nls") - private final Runnable reportPrinter = new Runnable() { - @Override - public void run() { - Map allStatistics = Maps.newLinkedHashMap(); - synchronized (getCaches()) { - for (String key : new TreeSet(getCaches().keySet())) { - String name = getCacheNameFromKey(key); - if (!allStatistics.containsKey(name)) { - allStatistics.put(name, new MultiCacheStatistics()); - } - CacheStatistics statistics = allStatistics.get(name); - for (WeakReference> reference : getCaches().get(key)) { - ICache value = reference.get(); - if (value != null) { - statistics.aggregate(value.getStatistics()); - } - } - } - } - printStatistics(allStatistics); - } - - /** - * Extract the cache name from a (possibly composite) key. - * - * @param key - * the key - * @return the cache name - */ - private String getCacheNameFromKey(final String key) { - int endIndex = key.indexOf(KEY_SEPARATOR); - if (endIndex == -1) { - return key; - } - return key.substring(0, endIndex); - } - - private void printStatistics(final Map allStatistics) { - StringBuilder builder = new StringBuilder("Active caches:\n"); - builder.append(String.format("%70s | %6s | %9s | %9s | %9s | %4s\n", "name", "caches", "items", "hit", "miss", "rate")); - - for (Entry entry : allStatistics.entrySet()) { - final MultiCacheStatistics statistics = entry.getValue(); - builder.append(String.format("%70s | %,6d | %,9d | %,9d | %,9d | %3.0f%%\n", entry.getKey(), statistics.getCacheCounter(), statistics.getEntries(), statistics.getHits(), statistics.getMisses(), statistics.getRatio() - * PERCENT)); - } - - builder.append('\n'); - LOGGER.info(builder.toString()); - } - }; - - // Task to clean the weak references that have been garbage collected - private final Runnable referenceCleaner = new Runnable() { - @Override - public void run() { - synchronized (getCaches()) { - for (Iterator>> iterator = getCaches().values().iterator(); iterator.hasNext();) { - WeakReference> reference = iterator.next(); - if (reference.get() == null) { - iterator.remove(); - } - } - } - } - }; - - /** - * {@link CacheStatistics} representing the statistics coming from multiple caches. - */ - private final class MultiCacheStatistics extends CacheStatistics { - private int counter; - - @Override - void aggregate(final CacheStatistics statistics) { - counter++; - super.aggregate(statistics); - } - - public int getCacheCounter() { - return counter; - } - } -} +/******************************************************************************* + * Copyright (c) 2016 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.caching; + +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeSet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.cache.CacheLoader; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + + +/** + * Instantiates and keeps track of Caches, allowing statistics to be collected and reported. + */ +public class CacheManager { + private static final Logger LOGGER = LogManager.getLogger(CacheManager.class); + + private static final class SingletonHolder { + private static CacheManager instance = new CacheManager(); + + public static CacheManager get() { + return instance; + } + } + + private static final int CLEANUP_DELAY = 5; + private static final int REPORT_DELAY = 10; + + private static final int PERCENT = 100; + + public static final String KEY_SEPARATOR = "§"; //$NON-NLS-1$ + + private final ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); + private final ArrayListMultimap>> caches = ArrayListMultimap.create(); + private final boolean monitoringEnabled; + + protected CacheManager() { + monitoringEnabled = Boolean.getBoolean("com.avaloq.tools.ddk.caching.EnableMonitor"); //$NON-NLS-1$ + if (isMonitoringEnabled()) { + threadPool.scheduleWithFixedDelay(referenceCleaner, CLEANUP_DELAY, CLEANUP_DELAY, TimeUnit.SECONDS); + threadPool.scheduleWithFixedDelay(reportPrinter, REPORT_DELAY, REPORT_DELAY, TimeUnit.SECONDS); + } + } + + public static CacheManager getInstance() { + return SingletonHolder.get(); + } + + /** + * Creates a new cache with the default configuration. + * + * @param + * the key type + * @param + * the value type + * @param name + * the name of the cache, must not be {@code null} + * @return a new Cache instance, never {@code null} + */ + @SuppressWarnings("PMD.LooseCoupling") + public MapCache createMapCache(final String name) { + return createMapCache(name, new CacheConfiguration()); + } + + /** + * Creates a new cache with the given configuration. + * + * @param + * the key type + * @param + * the value type + * @param name + * the name of the cache, must not be {@code null} + * @param configuration + * the configuration of the cache instance to be created, must not be {@code null} + * @return a new cache instance, never {@code null} + */ + @SuppressWarnings("PMD.LooseCoupling") + public MapCache createMapCache(final String name, final CacheConfiguration configuration) { + if (isMonitoringEnabled()) { + configuration.enableStatistics(); + } + + MapCache cache = new MapCache(name, configuration); + if (isMonitoringEnabled()) { + synchronized (getCaches()) { + getCaches().put(name, new WeakReference>(cache)); + } + } + return cache; + } + + /** + * Creates a new cache with the given configuration. + * + * @param + * the key type + * @param + * the value type + * @param name + * the name of the cache, must not be {@code null} + * @param configuration + * the configuration of the cache instance to be created, must not be {@code null} + * @param loader + * the cache loader used to obtain new values + * @return a new cache instance, never {@code null} + */ + @SuppressWarnings("PMD.LooseCoupling") + public MapCache createMapCache(final String name, final CacheConfiguration configuration, final CacheLoader loader) { + if (isMonitoringEnabled()) { + configuration.enableStatistics(); + } + + MapCache cache = new MapCache(name, configuration, loader); + if (isMonitoringEnabled()) { + synchronized (getCaches()) { + getCaches().put(name, new WeakReference>(cache)); + } + } + return cache; + } + + /** + * Returns the current statistics for the cache with the given name. + * + * @param name + * name of the cache, must not be {@code null} + * @return statistics, never {@code null} + */ + public CacheStatistics getStatistics(final String name) { + CacheStatistics result = new MultiCacheStatistics(); + if (isMonitoringEnabled()) { + synchronized (getCaches()) { + for (WeakReference> reference : getCaches().get(name)) { + ICache value = reference.get(); + if (value != null) { + result.aggregate(value.getStatistics()); + } + } + } + } + return result; + } + + public boolean isMonitoringEnabled() { + return monitoringEnabled; + } + + public ArrayListMultimap>> getCaches() { + return caches; + } + + // Task to print the a cache usage report + @SuppressWarnings("nls") + private final Runnable reportPrinter = new Runnable() { + @Override + public void run() { + Map allStatistics = Maps.newLinkedHashMap(); + synchronized (getCaches()) { + for (String key : new TreeSet(getCaches().keySet())) { + String name = getCacheNameFromKey(key); + if (!allStatistics.containsKey(name)) { + allStatistics.put(name, new MultiCacheStatistics()); + } + CacheStatistics statistics = allStatistics.get(name); + for (WeakReference> reference : getCaches().get(key)) { + ICache value = reference.get(); + if (value != null) { + statistics.aggregate(value.getStatistics()); + } + } + } + } + printStatistics(allStatistics); + } + + /** + * Extract the cache name from a (possibly composite) key. + * + * @param key + * the key + * @return the cache name + */ + private String getCacheNameFromKey(final String key) { + int endIndex = key.indexOf(KEY_SEPARATOR); + if (endIndex == -1) { + return key; + } + return key.substring(0, endIndex); + } + + private void printStatistics(final Map allStatistics) { + StringBuilder builder = new StringBuilder("Active caches:\n"); + builder.append(String.format("%70s | %6s | %9s | %9s | %9s | %4s\n", "name", "caches", "items", "hit", "miss", "rate")); + + for (Entry entry : allStatistics.entrySet()) { + final MultiCacheStatistics statistics = entry.getValue(); + builder.append(String.format("%70s | %,6d | %,9d | %,9d | %,9d | %3.0f%%\n", entry.getKey(), statistics.getCacheCounter(), statistics.getEntries(), statistics.getHits(), statistics.getMisses(), statistics.getRatio() + * PERCENT)); + } + + builder.append('\n'); + LOGGER.info(builder.toString()); + } + }; + + // Task to clean the weak references that have been garbage collected + private final Runnable referenceCleaner = new Runnable() { + @Override + public void run() { + synchronized (getCaches()) { + for (Iterator>> iterator = getCaches().values().iterator(); iterator.hasNext();) { + WeakReference> reference = iterator.next(); + if (reference.get() == null) { + iterator.remove(); + } + } + } + } + }; + + /** + * {@link CacheStatistics} representing the statistics coming from multiple caches. + */ + private final class MultiCacheStatistics extends CacheStatistics { + private int counter; + + @Override + void aggregate(final CacheStatistics statistics) { + counter++; + super.aggregate(statistics); + } + + public int getCacheCounter() { + return counter; + } + } +} diff --git a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheStatistics.java b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheStatistics.java index 001e2c0028..9a81fc6f8a 100644 --- a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheStatistics.java +++ b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/CacheStatistics.java @@ -1,67 +1,67 @@ -/******************************************************************************* - * Copyright (c) 2016 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.caching; - -/** - * Represents useful statistics of a cache. - */ -public class CacheStatistics { - private long entries; - private long hits; - private long misses; - - public CacheStatistics() { - // Start with empty statistics - } - - public CacheStatistics(final long entries, final long hits, final long misses) { - this.entries = entries; - this.hits = hits; - this.misses = misses; - } - - public long getEntries() { - return entries; - } - - public long getHits() { - return hits; - } - - public long getMisses() { - return misses; - } - - /** - * Returns the hit ratio of the cache. - * - * @return the hit ratio, or zero if no hits or misses were reported. - */ - public double getRatio() { - double total = misses + hits; - if (total == 0) { - return 0; - } - return hits / total; - } - - /** - * Aggregates the given statistics into the current instance. - * - * @param statistics - * the other statistics, must not be {@code null} - */ - void aggregate(final CacheStatistics statistics) { - this.entries += statistics.entries; - this.hits += statistics.hits; - this.misses += statistics.misses; - } -} +/******************************************************************************* + * Copyright (c) 2016 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.caching; + +/** + * Represents useful statistics of a cache. + */ +public class CacheStatistics { + private long entries; + private long hits; + private long misses; + + public CacheStatistics() { + // Start with empty statistics + } + + public CacheStatistics(final long entries, final long hits, final long misses) { + this.entries = entries; + this.hits = hits; + this.misses = misses; + } + + public long getEntries() { + return entries; + } + + public long getHits() { + return hits; + } + + public long getMisses() { + return misses; + } + + /** + * Returns the hit ratio of the cache. + * + * @return the hit ratio, or zero if no hits or misses were reported. + */ + public double getRatio() { + double total = misses + hits; + if (total == 0) { + return 0; + } + return hits / total; + } + + /** + * Aggregates the given statistics into the current instance. + * + * @param statistics + * the other statistics, must not be {@code null} + */ + void aggregate(final CacheStatistics statistics) { + this.entries += statistics.entries; + this.hits += statistics.hits; + this.misses += statistics.misses; + } +} diff --git a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/ICache.java b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/ICache.java index a0425f5fbb..a62d255818 100644 --- a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/ICache.java +++ b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/ICache.java @@ -1,32 +1,32 @@ -/******************************************************************************* - * Copyright (c) 2016 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.caching; - -/** - * A basic cache capable of providing usage statistics. - * - * @param - * the key type - * @param - * the value type - */ -public interface ICache { - - /** - * Returns the statistics of this cache's usage. - *

- * Note: may not return any useful results if statistics were not enabled in the {@link CacheConfiguration} when the cache was created. - *

- * - * @return the statistics - */ - CacheStatistics getStatistics(); -} +/******************************************************************************* + * Copyright (c) 2016 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.caching; + +/** + * A basic cache capable of providing usage statistics. + * + * @param + * the key type + * @param + * the value type + */ +public interface ICache { + + /** + * Returns the statistics of this cache's usage. + *

+ * Note: may not return any useful results if statistics were not enabled in the {@link CacheConfiguration} when the cache was created. + *

+ * + * @return the statistics + */ + CacheStatistics getStatistics(); +} diff --git a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/MapCache.java b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/MapCache.java index 6802734cdd..b8ef508175 100644 --- a/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/MapCache.java +++ b/com.avaloq.tools.ddk/src/com/avaloq/tools/ddk/caching/MapCache.java @@ -1,218 +1,218 @@ -/******************************************************************************* - * Copyright (c) 2016 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.caching; - -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.CacheStats; -import com.google.common.cache.Weigher; - - -/** - * Represents a simple map-like cache. - *

- * Note: new instances should be created through {@link CacheManager} - *

- * - * @param - * the key type - * @param - * the value type - */ -public class MapCache implements ICache, Map { - - private final Cache backend; - private final String name; - - MapCache(final String name, final CacheConfiguration config) { - this.name = name; - backend = configure(config).build(); - } - - MapCache(final String name, final CacheConfiguration config, final CacheLoader loader) { - this.name = name; - backend = configure(config).build(loader); - } - - private CacheBuilder configure(final CacheConfiguration config) { - CacheBuilder cacheBuilder = CacheBuilder.newBuilder(); - if (config.isStatisticsEnabled()) { - cacheBuilder.recordStats(); - } - if (config.isSoftValuesEnabled()) { - cacheBuilder.softValues(); - } else if (config.isWeakValuesEnabled()) { - cacheBuilder.weakValues(); - } - - if (config.isWeakKeysEnabled()) { - cacheBuilder.weakKeys(); - } - - if (config.getInitialCapacity() >= 0) { - cacheBuilder.initialCapacity(config.getInitialCapacity()); - } - if (config.getMaximumSize() >= 0) { - if (config.isArraySizeEnabled()) { - cacheBuilder.maximumWeight(config.getMaximumSize()); - cacheBuilder.weigher(new Weigher() { - @Override - public int weigh(final K key, final V value) { - if (value instanceof byte[]) { - return ((byte[]) value).length; - } - throw new IllegalStateException("Using array size is only supported for byte arrays"); //$NON-NLS-1$ - } - }); - } else { - cacheBuilder.maximumSize(config.getMaximumSize()); - } - } - if (config.getConcurrencyLevel() >= 0) { - cacheBuilder.concurrencyLevel(config.getConcurrencyLevel()); - } - return cacheBuilder; - } - - public String getName() { - return name; - } - - @Override - public CacheStatistics getStatistics() { - CacheStats stats = backend.stats(); - return new CacheStatistics(backend.size(), stats.hitCount(), stats.missCount()); - } - - @Override - public void clear() { - backend.invalidateAll(); - } - - @Override - public V putIfAbsent(final K key, final V value) { - return backend.asMap().putIfAbsent(key, value); - } - - @Override - public Set keySet() { - return backend.asMap().keySet(); - } - - @Override - public Collection values() { - return backend.asMap().values(); - } - - @Override - public boolean isEmpty() { - return backend.size() == 0; - } - - @Override - public int size() { - return (int) backend.size(); - } - - @Override - public boolean containsKey(final Object key) { - return backend.getIfPresent(key) != null; - } - - @Override - public boolean containsValue(final Object value) { - return backend.asMap().containsValue(value); - } - - @Override - public V get(final Object key) { - return backend.getIfPresent(key); - } - - @Override - public V put(final K key, final V value) { - return backend.asMap().put(key, value); - } - - @Override - public V remove(final Object key) { - return backend.asMap().remove(key); - } - - @Override - public void putAll(final Map m) { - backend.putAll(m); - } - - @Override - public Set> entrySet() { - return backend.asMap().entrySet(); - } - - @Override - public V computeIfAbsent(final K key, final Function mappingFunction) { - return backend.asMap().computeIfAbsent(key, mappingFunction); - } - - @Override - public V getOrDefault(final Object key, final V defaultValue) { - return backend.asMap().getOrDefault(key, defaultValue); - } - - @Override - public void forEach(final BiConsumer action) { - backend.asMap().forEach(action); - } - - @Override - public void replaceAll(final BiFunction function) { - backend.asMap().replaceAll(function); - } - - @Override - public boolean remove(final Object key, final Object value) { - return backend.asMap().remove(key, value); - } - - @Override - public boolean replace(final K key, final V oldValue, final V newValue) { - return backend.asMap().replace(key, oldValue, newValue); - } - - @Override - public V replace(final K key, final V value) { - return backend.asMap().replace(key, value); - } - - @Override - public V computeIfPresent(final K key, final BiFunction remappingFunction) { - return backend.asMap().computeIfPresent(key, remappingFunction); - } - - @Override - public V compute(final K key, final BiFunction remappingFunction) { - return backend.asMap().compute(key, remappingFunction); - } - - @Override - public V merge(final K key, final V value, final BiFunction remappingFunction) { - return backend.asMap().merge(key, value, remappingFunction); - } - -} +/******************************************************************************* + * Copyright (c) 2016 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.caching; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.CacheStats; +import com.google.common.cache.Weigher; + + +/** + * Represents a simple map-like cache. + *

+ * Note: new instances should be created through {@link CacheManager} + *

+ * + * @param + * the key type + * @param + * the value type + */ +public class MapCache implements ICache, Map { + + private final Cache backend; + private final String name; + + MapCache(final String name, final CacheConfiguration config) { + this.name = name; + backend = configure(config).build(); + } + + MapCache(final String name, final CacheConfiguration config, final CacheLoader loader) { + this.name = name; + backend = configure(config).build(loader); + } + + private CacheBuilder configure(final CacheConfiguration config) { + CacheBuilder cacheBuilder = CacheBuilder.newBuilder(); + if (config.isStatisticsEnabled()) { + cacheBuilder.recordStats(); + } + if (config.isSoftValuesEnabled()) { + cacheBuilder.softValues(); + } else if (config.isWeakValuesEnabled()) { + cacheBuilder.weakValues(); + } + + if (config.isWeakKeysEnabled()) { + cacheBuilder.weakKeys(); + } + + if (config.getInitialCapacity() >= 0) { + cacheBuilder.initialCapacity(config.getInitialCapacity()); + } + if (config.getMaximumSize() >= 0) { + if (config.isArraySizeEnabled()) { + cacheBuilder.maximumWeight(config.getMaximumSize()); + cacheBuilder.weigher(new Weigher() { + @Override + public int weigh(final K key, final V value) { + if (value instanceof byte[]) { + return ((byte[]) value).length; + } + throw new IllegalStateException("Using array size is only supported for byte arrays"); //$NON-NLS-1$ + } + }); + } else { + cacheBuilder.maximumSize(config.getMaximumSize()); + } + } + if (config.getConcurrencyLevel() >= 0) { + cacheBuilder.concurrencyLevel(config.getConcurrencyLevel()); + } + return cacheBuilder; + } + + public String getName() { + return name; + } + + @Override + public CacheStatistics getStatistics() { + CacheStats stats = backend.stats(); + return new CacheStatistics(backend.size(), stats.hitCount(), stats.missCount()); + } + + @Override + public void clear() { + backend.invalidateAll(); + } + + @Override + public V putIfAbsent(final K key, final V value) { + return backend.asMap().putIfAbsent(key, value); + } + + @Override + public Set keySet() { + return backend.asMap().keySet(); + } + + @Override + public Collection values() { + return backend.asMap().values(); + } + + @Override + public boolean isEmpty() { + return backend.size() == 0; + } + + @Override + public int size() { + return (int) backend.size(); + } + + @Override + public boolean containsKey(final Object key) { + return backend.getIfPresent(key) != null; + } + + @Override + public boolean containsValue(final Object value) { + return backend.asMap().containsValue(value); + } + + @Override + public V get(final Object key) { + return backend.getIfPresent(key); + } + + @Override + public V put(final K key, final V value) { + return backend.asMap().put(key, value); + } + + @Override + public V remove(final Object key) { + return backend.asMap().remove(key); + } + + @Override + public void putAll(final Map m) { + backend.putAll(m); + } + + @Override + public Set> entrySet() { + return backend.asMap().entrySet(); + } + + @Override + public V computeIfAbsent(final K key, final Function mappingFunction) { + return backend.asMap().computeIfAbsent(key, mappingFunction); + } + + @Override + public V getOrDefault(final Object key, final V defaultValue) { + return backend.asMap().getOrDefault(key, defaultValue); + } + + @Override + public void forEach(final BiConsumer action) { + backend.asMap().forEach(action); + } + + @Override + public void replaceAll(final BiFunction function) { + backend.asMap().replaceAll(function); + } + + @Override + public boolean remove(final Object key, final Object value) { + return backend.asMap().remove(key, value); + } + + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + return backend.asMap().replace(key, oldValue, newValue); + } + + @Override + public V replace(final K key, final V value) { + return backend.asMap().replace(key, value); + } + + @Override + public V computeIfPresent(final K key, final BiFunction remappingFunction) { + return backend.asMap().computeIfPresent(key, remappingFunction); + } + + @Override + public V compute(final K key, final BiFunction remappingFunction) { + return backend.asMap().compute(key, remappingFunction); + } + + @Override + public V merge(final K key, final V value, final BiFunction remappingFunction) { + return backend.asMap().merge(key, value, remappingFunction); + } + +}