You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/09/10 05:44:55 UTC

[isis] branch master updated: ISIS-2774: fixes _OrphanedSupportingMethodValidator

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new d5c8148  ISIS-2774: fixes _OrphanedSupportingMethodValidator
d5c8148 is described below

commit d5c8148ff406fa38be94e53bf4784ed9164d80be
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Sep 10 07:44:46 2021 +0200

    ISIS-2774: fixes _OrphanedSupportingMethodValidator
---
 .../java/org/apache/isis/applib/Identifier.java    |   9 ++
 .../isis/applib/services/metamodel/BeanSort.java   |   7 +-
 .../progmodel/ProgrammingModelConstants.java       |  14 ++-
 ...tionEnforcesMetamodelContributionValidator.java |  58 +++++-----
 .../methods/OrphanedSupportingMethodValidator.java | 127 ---------------------
 .../_OrphanedSupportingMethodValidator.java        |  75 ++++++++++++
 .../dflt/ProgrammingModelFacetsJava11.java         |   2 -
 .../DomainModelTest_usingGoodDomain.java           |  38 ++++--
 ...anceInterface.java => ProperFullyAbstract.java} |  47 ++------
 .../testdomain/model/good/ProperFullyImpl.java     |  74 ++++++++++++
 .../good/ProperMemberInheritanceAbstract.java      |   4 +-
 .../good/ProperMemberInheritanceInterface.java     |   3 +-
 .../interaction/DomainObjectTesterFactory.java     |  93 +++++++++++++--
 .../applib/fixturescripts/FixtureScripts.java      |   3 +-
 14 files changed, 333 insertions(+), 221 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/Identifier.java b/api/applib/src/main/java/org/apache/isis/applib/Identifier.java
index cb5a6b5..f0d09e3 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/Identifier.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/Identifier.java
@@ -19,6 +19,7 @@
 package org.apache.isis.applib;
 
 import java.io.Serializable;
+import java.lang.reflect.Method;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
@@ -30,6 +31,7 @@ import org.apache.isis.applib.services.i18n.TranslationContext;
 import org.apache.isis.applib.services.i18n.TranslationService;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Strings;
+import org.apache.isis.commons.internal.reflection._Reflect;
 
 import lombok.Getter;
 import lombok.NonNull;
@@ -77,6 +79,13 @@ implements
                 Type.PROPERTY_OR_COLLECTION);
     }
 
+    /** for reporting orphaned methods */
+    public static Identifier methodIdentifier(
+            final LogicalType typeIdentifier,
+            final Method method) {
+        return actionIdentifier(typeIdentifier, _Reflect.methodToShortString(method), method.getParameterTypes());
+    }
+
     public static Identifier actionIdentifier(
             final LogicalType typeIdentifier,
             final String actionName,
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/metamodel/BeanSort.java b/api/applib/src/main/java/org/apache/isis/applib/services/metamodel/BeanSort.java
index b40bcf9..46a98be 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/metamodel/BeanSort.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/metamodel/BeanSort.java
@@ -89,8 +89,13 @@ public enum BeanSort {
         return this == MANAGED_BEAN_CONTRIBUTING;
     }
 
+    public boolean isManagedBeanNotContributing() {
+        return this == MANAGED_BEAN_NOT_CONTRIBUTING;
+    }
+
     public boolean isManagedBean() {
-        return this == MANAGED_BEAN_CONTRIBUTING || this == MANAGED_BEAN_NOT_CONTRIBUTING;
+        return this == MANAGED_BEAN_CONTRIBUTING
+                || this == MANAGED_BEAN_NOT_CONTRIBUTING;
     }
 
     public boolean isMixin() {
diff --git a/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java b/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
index f0e9182..6bc3d23 100644
--- a/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/progmodel/ProgrammingModelConstants.java
@@ -315,10 +315,14 @@ public final class ProgrammingModelConstants {
     public static enum Validation {
         CONFLICTING_TITLE_STRATEGIES(
                 "${type} has title() method with @Title annotation, which is not allowed; "
-                + "consider either removing the @Title annotation or renaming the method");
+                + "consider either removing the @Title annotation or renaming the method"),
+        ORPHANED_METHOD("${type}#${member}: is public, but orphaned (was not picked up by the framework); "
+                + "reporting orphans, because the class is setup for member introspection, "
+                + "without enforcing annotations")
+        ;
         private final String template;
-        public String getMessage(final Identifier identifier) {
-            return processMessageTemplate(template, identifier);
+        public String getMessage(final Identifier featureIdentifier) {
+            return processMessageTemplate(template, featureIdentifier);
         }
     }
 
@@ -344,7 +348,9 @@ public final class ProgrammingModelConstants {
     private static String processMessageTemplate(
             final String template,
             final Identifier identifier) {
-        return template.replace("${type}", identifier.getLogicalType().getClassName());
+        return template
+                .replace("${type}", identifier.getLogicalType().getClassName())
+                .replace("${member}", identifier.getMemberLogicalName());
     }
 
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
index 0f2dca0..02a60e5 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
@@ -22,6 +22,7 @@ import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Optional;
 import java.util.TreeSet;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -65,32 +66,33 @@ extends MetaModelVisitingValidatorAbstract {
     @Override
     public void validate(final ObjectSpecification spec) {
 
-        if(spec.isManagedBean()
-                || spec.isAbstract()) {
+        if(!(spec instanceof ObjectSpecificationAbstract)
+                || spec.isAbstract()
+                || spec.getBeanSort().isManagedBeanNotContributing()
+                || spec.isValue()) {
             return;
         }
 
         final Class<?> type = spec.getCorrespondingClass();
 
+        // methods picked up by the framework
         // assuming 'weak' equality, treating overwritten and overriding methods as same
-        val recognizedMemberMethods = new TreeSet<Method>(_Reflect::methodWeakCompare);
+        val memberMethods = new TreeSet<Method>(_Reflect::methodWeakCompare);
+        val supportMethods = new TreeSet<Method>(_Reflect::methodWeakCompare);
 
         spec
         .streamAnyActions(MixedIn.EXCLUDED)
         .map(ObjectMemberAbstract.class::cast)
         .map(ObjectMemberAbstract::getFacetedMethod)
         .map(FacetedMethod::getMethod)
-        .forEach(recognizedMemberMethods::add);
+        .forEach(memberMethods::add);
 
         spec
         .streamAssociations(MixedIn.EXCLUDED)
         .map(ObjectMemberAbstract.class::cast)
         .map(ObjectMemberAbstract::getFacetedMethod)
         .map(FacetedMethod::getMethod)
-        .forEach(recognizedMemberMethods::add);
-
-        // support methods known to the meta-model
-        val recognizedSupportMethods = new TreeSet<Method>(_Reflect::methodWeakCompare);
+        .forEach(memberMethods::add);
 
         spec
         .streamFacetHolders()
@@ -101,10 +103,9 @@ extends MetaModelVisitingValidatorAbstract {
         .map(ImperativeFacet.class::cast)
         .map(ImperativeFacet::getMethods)
         .flatMap(Can::stream)
-        .forEach(recognizedSupportMethods::add);
+        .forEach(supportMethods::add);
 
-        // methods intended to be included with the meta-model
-        val notRecognizedMethods = _Sets.<Method>newHashSet();
+        val methodsIntendedToBeIncludedButNotPickedUp = _Sets.<Method>newHashSet();
 
         classCache
         // methods intended to be included with the meta-model but missing
@@ -113,31 +114,34 @@ extends MetaModelVisitingValidatorAbstract {
                 "domain-include",
                 method->_Annotations.synthesizeInherited(method, Domain.Include.class).isPresent())
         // filter away those that are recognized
-        .filter(intendedMethod->!recognizedSupportMethods.contains(intendedMethod))
-        .filter(intendedMethod->!recognizedMemberMethods.contains(intendedMethod))
-        .forEach(notRecognizedMethods::add);
+        .filter(Predicate.not(memberMethods::contains))
+        .filter(Predicate.not(supportMethods::contains))
+        .forEach(methodsIntendedToBeIncludedButNotPickedUp::add);
 
         // find reasons about why these are not recognized
-        notRecognizedMethods.forEach(notRecognizedMethod->{
-            final List<String> unmetContraints =
-                    unmetContraints((ObjectSpecificationAbstract) spec, notRecognizedMethod);
+        methodsIntendedToBeIncludedButNotPickedUp
+        .forEach(notPickedUpMethod->{
+            val unmetContraints =
+                    unmetContraints((ObjectSpecificationAbstract) spec, notPickedUpMethod)
+                    .stream()
+                    .collect(Collectors.joining("; "));
 
             //FIXME[ISIS-2774] - update message to a more generic one
-            String messageFormat = "%s#%s: has annotation @%s, is assumed to support "
-                    + "a property, collection or action. Unmet constraint(s): %s";
-            ValidationFailure.raiseFormatted(
-                    spec,
-                    messageFormat,
+            ValidationFailure.raiseFormatted(spec,
+                    "%s#%s: has annotation @%s, is assumed to support "
+                            + "a property, collection or action. Unmet constraint(s): %s",
                     spec.getFeatureIdentifier().getClassName(),
-                    _Reflect.methodToShortString(notRecognizedMethod),
+                    _Reflect.methodToShortString(notPickedUpMethod),
                     "Domain.Include",
-                    unmetContraints.stream()
-                    .collect(Collectors.joining("; ")));
+                    unmetContraints);
         });
 
+        _OrphanedSupportingMethodValidator.validate((ObjectSpecificationAbstract)spec,
+                supportMethods, memberMethods, methodsIntendedToBeIncludedButNotPickedUp);
+
     }
 
-    // -- VALIDATION LOGIC
+    // -- HELPER - VALIDATION LOGIC
 
     private List<String> unmetContraints(
             final ObjectSpecificationAbstract spec,
@@ -157,4 +161,6 @@ extends MetaModelVisitingValidatorAbstract {
 
     }
 
+
+
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/OrphanedSupportingMethodValidator.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/OrphanedSupportingMethodValidator.java
deleted file mode 100644
index 0dab07d..0000000
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/OrphanedSupportingMethodValidator.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.methods;
-
-import java.lang.reflect.Method;
-import java.util.HashSet;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.inject.Inject;
-
-import org.apache.isis.commons.collections.Can;
-import org.apache.isis.commons.internal.collections._Lists;
-import org.apache.isis.commons.internal.collections._Sets;
-import org.apache.isis.commons.internal.reflection._Reflect;
-import org.apache.isis.core.metamodel.context.MetaModelContext;
-import org.apache.isis.core.metamodel.facetapi.FacetHolder;
-import org.apache.isis.core.metamodel.facets.ImperativeFacet;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
-import org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
-import org.apache.isis.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
-import org.apache.isis.core.metamodel.specloader.validator.ValidationFailure;
-
-import lombok.NonNull;
-import lombok.val;
-
-/**
- *
- * @since 2.0
- *
- */
-public class OrphanedSupportingMethodValidator
-extends MetaModelVisitingValidatorAbstract {
-
-    @Inject
-    public OrphanedSupportingMethodValidator(final MetaModelContext mmc) {
-        super(mmc);
-    }
-
-    @Override
-    public void validate(final @NonNull ObjectSpecification spec) {
-
-        if(!(spec instanceof ObjectSpecificationAbstract)) {
-            return; // continue
-        }
-
-        if(spec.isAbstract()) {
-            return; // continue - we don't care about abstract types
-        }
-
-        val potentialOrphans = ((ObjectSpecificationAbstract) spec).getPotentialOrphans();
-        if(potentialOrphans.isEmpty()) {
-            return; // continue
-        }
-
-        // methods known to the meta-model
-        val recognizedMethods = spec.streamFacetHolders()
-                .flatMap(FacetHolder::streamFacets)
-                .filter(ImperativeFacet.class::isInstance)
-                .map(ImperativeFacet.class::cast)
-                .map(ImperativeFacet::getMethods)
-                .flatMap(Can::stream)
-                .collect(Collectors.toCollection(HashSet::new));
-
-        // methods intended to be included with the meta-model but missing
-        val notRecognizedMethods =
-                _Sets.minus(potentialOrphans, recognizedMethods);
-
-        // find reasons why these are not recognized
-        notRecognizedMethods.forEach(notRecognizedMethod->{
-
-            val unmetContraints = unmetContraints(spec, notRecognizedMethod);
-
-            //FIXME[ISIS-2774] - update message to a more generic one ?
-            // why are we duplicating DomainIncludeAnnotationEnforcesMetamodelContributionValidator here?
-            val messageFormat = "%s#%s: is assumed to support "
-                    + "a property, collection or action. Unmet constraint(s): %s";
-
-            ValidationFailure.raise(
-                    spec,
-                    String.format(
-                            messageFormat,
-                            spec.getFeatureIdentifier().getClassName(),
-                            _Reflect.methodToShortString(notRecognizedMethod),
-                            unmetContraints.stream()
-                            .collect(Collectors.joining("; "))));
-        });
-
-        potentialOrphans.clear(); // no longer needed
-
-    }
-
-    // -- VALIDATION LOGIC
-
-    private List<String> unmetContraints(
-            final ObjectSpecification spec,
-            final Method method) {
-
-        val unmetContraints = _Lists.<String>newArrayList();
-
-        unmetContraints.add("unsupported method signature or "
-                + "orphaned (not associated with a member)");
-        return unmetContraints;
-
-    }
-
-
-
-
-
-}
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/_OrphanedSupportingMethodValidator.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/_OrphanedSupportingMethodValidator.java
new file mode 100644
index 0000000..ab4f1b3
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/methods/_OrphanedSupportingMethodValidator.java
@@ -0,0 +1,75 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.metamodel.methods;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.apache.isis.applib.Identifier;
+import org.apache.isis.core.config.progmodel.ProgrammingModelConstants;
+import org.apache.isis.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
+import org.apache.isis.core.metamodel.specloader.validator.ValidationFailure;
+
+import lombok.NonNull;
+import lombok.val;
+
+class _OrphanedSupportingMethodValidator {
+
+    static void validate(
+            final @NonNull ObjectSpecificationAbstract spec,
+            final @NonNull Set<Method> supportMethods,
+            final @NonNull Set<Method> memberMethods,
+            final @NonNull Set<Method> alreadyReported) {
+
+        if(spec.isAbstract()
+                || spec.getBeanSort().isManagedBeanNotContributing()
+                || spec.isValue()
+                || spec.getIntrospectionPolicy()
+                    .getMemberAnnotationPolicy()
+                    .isMemberAnnotationsRequired()) {
+            return; // ignore
+        }
+
+        val potentialOrphans = spec.getPotentialOrphans();
+        if(potentialOrphans.isEmpty()) {
+            return; // nothing to do
+        }
+
+        // find reasons why these are not recognized
+        potentialOrphans.stream()
+        .filter(Predicate.not(alreadyReported::contains))
+        .filter(Predicate.not(memberMethods::contains))
+        .filter(Predicate.not(supportMethods::contains))
+        .forEach(orphanedMethod->{
+
+            val methodIdentifier = Identifier
+                    .methodIdentifier(spec.getFeatureIdentifier().getLogicalType(), orphanedMethod);
+
+            ValidationFailure.raise(
+                    spec,
+                    ProgrammingModelConstants.Validation.ORPHANED_METHOD
+                    .getMessage(methodIdentifier));
+        });
+
+        potentialOrphans.clear(); // no longer needed
+
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
index cd2839a..300d209 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
@@ -127,7 +127,6 @@ import org.apache.isis.core.metamodel.facets.value.url.URLValueFacetUsingSemanti
 import org.apache.isis.core.metamodel.facets.value.uuid.UUIDValueFacetUsingSemanticsProviderFactory;
 import org.apache.isis.core.metamodel.methods.DomainIncludeAnnotationEnforcesMetamodelContributionValidator;
 import org.apache.isis.core.metamodel.methods.MethodByClassMap;
-import org.apache.isis.core.metamodel.methods.OrphanedSupportingMethodValidator;
 import org.apache.isis.core.metamodel.postprocessors.DeriveMixinMembersPostProcessor;
 import org.apache.isis.core.metamodel.postprocessors.all.DeriveDescribedAsFromTypePostProcessor;
 import org.apache.isis.core.metamodel.postprocessors.all.i18n.SynthesizeObjectNamingPostProcessor;
@@ -378,7 +377,6 @@ extends ProgrammingModelAbstract {
         val mmc = getMetaModelContext();
 
         addValidator(new DomainIncludeAnnotationEnforcesMetamodelContributionValidator(mmc));
-        addValidator(new OrphanedSupportingMethodValidator(mmc));
         addValidator(new TitlesAndTranslationsValidator(mmc));  // should this instead be a post processor, alongside TranslationPostProcessor ?
         addValidator(new ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator(mmc));
         addValidator(new ActionOverloadingValidator(mmc));
diff --git a/regressiontests/stable-domainmodel/src/test/java/org/apache/isis/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java b/regressiontests/stable-domainmodel/src/test/java/org/apache/isis/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
index e8a0912..172888e 100644
--- a/regressiontests/stable-domainmodel/src/test/java/org/apache/isis/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
+++ b/regressiontests/stable-domainmodel/src/test/java/org/apache/isis/testdomain/domainmodel/DomainModelTest_usingGoodDomain.java
@@ -23,6 +23,7 @@ import java.util.stream.Stream;
 
 import javax.inject.Inject;
 
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -71,6 +72,7 @@ import org.apache.isis.testdomain.model.good.ElementTypeConcrete;
 import org.apache.isis.testdomain.model.good.ElementTypeInterface;
 import org.apache.isis.testdomain.model.good.ProperChoicesWhenChoicesFrom;
 import org.apache.isis.testdomain.model.good.ProperElementTypeVm;
+import org.apache.isis.testdomain.model.good.ProperFullyImpl;
 import org.apache.isis.testdomain.model.good.ProperInterface2;
 import org.apache.isis.testdomain.model.good.ProperMemberInheritanceInterface;
 import org.apache.isis.testdomain.model.good.ProperMemberInheritance_usingAbstract;
@@ -132,6 +134,14 @@ class DomainModelTest_usingGoodDomain {
         System.out.println("==============");
     }
 
+    private DomainObjectTesterFactory testerFactory;;
+
+    @BeforeEach
+    void setUp() {
+        testerFactory = new DomainObjectTesterFactory(serviceInjector);
+    }
+
+
     @Test
     void goodDomain_shouldPassValidation() {
         //debug();
@@ -205,9 +215,18 @@ class DomainModelTest_usingGoodDomain {
 
     }
 
+    @Test
+    void fullyAbstractObject_whenImplemented_shouldBeSupported() {
+        val tester = testerFactory.objectTester(ProperFullyImpl.class);
+        tester.assertTitle("title");
+        tester.assertIcon("icon");
+        tester.assertCssClass("css");
+        tester.assertLayout("layout");
+    }
+
     @ParameterizedTest
     @MethodSource("provideProperMemberInheritanceTypes")
-    void titleAndIconName_shouldBeInheritable(final Class<?> type) {
+    void titleAndIconName_shouldBeInheritable(final Class<?> type) throws Exception {
 
         val spec = specificationLoader.specForTypeElseFail(type);
 
@@ -217,9 +236,16 @@ class DomainModelTest_usingGoodDomain {
         val iconFacet = spec.getFacet(IconFacet.class);
         assertNotNull(iconFacet);
 
-        val properMemberInheritance = new ProperMemberInheritance_usingAbstract();
-        assertEquals(properMemberInheritance.title(), titleService.titleOf(properMemberInheritance));
-        assertEquals(properMemberInheritance.iconName(), titleService.iconNameOf(properMemberInheritance));
+        if(!spec.isAbstract()) {
+            val instance = type.getDeclaredConstructor().newInstance();
+            assertEquals("inherited title", titleService.titleOf(instance));
+            assertEquals("inherited icon", titleService.iconNameOf(instance));
+
+            val domainObject = ManagedObject.of(spec, instance);
+            assertEquals("inherited title", titleFacet.title(domainObject));
+            assertEquals("inherited icon", iconFacet.iconName(domainObject));
+        }
+
     }
 
     @ParameterizedTest
@@ -440,8 +466,6 @@ class DomainModelTest_usingGoodDomain {
     @Test
     void viewmodelWithEncapsulatedMembers() {
 
-        val testerFactory = new DomainObjectTesterFactory(serviceInjector);
-
         // OBJECT
 
         val objectSpec = specificationLoader.specForTypeElseFail(ViewModelWithEncapsulatedMembers.class);
@@ -509,8 +533,6 @@ class DomainModelTest_usingGoodDomain {
     @Test
     void viewmodelWithAnnotationOptional_usingPrivateSupport() {
 
-        val testerFactory = new DomainObjectTesterFactory(serviceInjector);
-
         // OBJECT
 
         val objectSpec = specificationLoader.specForTypeElseFail(ViewModelWithAnnotationOptionalUsingPrivateSupport.class);
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyAbstract.java
similarity index 62%
copy from regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java
copy to regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyAbstract.java
index 1680ed6..cdd8f34 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyAbstract.java
@@ -27,53 +27,26 @@ import org.apache.isis.applib.annotation.CollectionLayout;
 import org.apache.isis.applib.annotation.ObjectSupport;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
-import org.apache.isis.applib.annotation.Title;
 
-public interface ProperMemberInheritanceInterface {
+abstract class ProperFullyAbstract {
 
-    @Title
-    default String title() {
-        return "inherited title";
-    }
-
-    @ObjectSupport
-    default String iconName() {
-        return "inherited icon";
-    }
+    @ObjectSupport public abstract String title();
+    @ObjectSupport public abstract String iconName();
+    @ObjectSupport public abstract String cssClass();
+    @ObjectSupport public abstract String layout();
 
     @Action
     @ActionLayout(named = "foo", describedAs = "bar")
-    default void sampleAction() {
-    }
+    public abstract void sampleAction();
 
     @Property
     @PropertyLayout(named = "foo", describedAs = "bar")
-    default String getSampleProperty() {
-        return null;
-    }
+    public abstract String getSampleProperty();
+    public abstract void setSampleProperty(String sampleProperty);
 
     @Collection
     @CollectionLayout(named = "foo", describedAs = "bar")
-    default List<String> getSampleCollection() {
-        return null;
-    }
-
-    // -- OVERRIDING TESTS
-
-    @Action
-    @ActionLayout(named = "foo", describedAs = "bar")
-    default void sampleActionOverride() {
-    }
-
-    @Action
-    @ActionLayout(named = "foo", describedAs = "bar")
-    default void sampleActionOverrideWithParam(final String x) {
-    }
-
-    @Property
-    @PropertyLayout(named = "foo", describedAs = "bar")
-    default String getSamplePropertyOverride() {
-        return null;
-    }
+    public abstract List<String> getSampleCollection();
+    public abstract void setSampleCollection(List<String> sampleCollection);
 
 }
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyImpl.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyImpl.java
new file mode 100644
index 0000000..792adb9
--- /dev/null
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperFullyImpl.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.testdomain.model.good;
+
+import java.util.List;
+
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Nature;
+
+@DomainObject(nature = Nature.VIEW_MODEL)
+public class ProperFullyImpl
+extends ProperFullyAbstract {
+
+    @Override
+    public String title() {
+        return "title";
+    }
+
+    @Override
+    public String iconName() {
+        return "icon";
+    }
+
+    @Override
+    public String cssClass() {
+        return "css";
+    }
+
+    @Override
+    public String layout() {
+        return "layout";
+    }
+
+    @Override
+    public void sampleAction() {
+
+    }
+
+    @Override
+    public String getSampleProperty() {
+        return null;
+    }
+
+    @Override
+    public void setSampleProperty(final String sampleProperty) {
+    }
+
+    @Override
+    public List<String> getSampleCollection() {
+        return null;
+    }
+
+    @Override
+    public void setSampleCollection(final List<String> sampleCollection) {
+    }
+
+
+}
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceAbstract.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceAbstract.java
index 571239a..b59f77e 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceAbstract.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceAbstract.java
@@ -27,14 +27,14 @@ import org.apache.isis.applib.annotation.CollectionLayout;
 import org.apache.isis.applib.annotation.ObjectSupport;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
-import org.apache.isis.applib.annotation.Title;
 
 import lombok.Getter;
 import lombok.Setter;
 
 abstract class ProperMemberInheritanceAbstract {
 
-    @ObjectSupport public String title() {
+    @ObjectSupport
+    public String title() {
         return "inherited title";
     }
 
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java
index 1680ed6..f653ad7 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/good/ProperMemberInheritanceInterface.java
@@ -27,11 +27,10 @@ import org.apache.isis.applib.annotation.CollectionLayout;
 import org.apache.isis.applib.annotation.ObjectSupport;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
-import org.apache.isis.applib.annotation.Title;
 
 public interface ProperMemberInheritanceInterface {
 
-    @Title
+    @ObjectSupport
     default String title() {
         return "inherited title";
     }
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/util/interaction/DomainObjectTesterFactory.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/util/interaction/DomainObjectTesterFactory.java
index 4c26794..ccdda0f 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/util/interaction/DomainObjectTesterFactory.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/util/interaction/DomainObjectTesterFactory.java
@@ -34,6 +34,10 @@ import org.apache.isis.applib.services.factory.FactoryService;
 import org.apache.isis.applib.services.iactnlayer.InteractionService;
 import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.commons.collections.Can;
+import org.apache.isis.core.metamodel.facets.members.cssclass.CssClassFacet;
+import org.apache.isis.core.metamodel.facets.object.icon.IconFacet;
+import org.apache.isis.core.metamodel.facets.object.layout.LayoutFacet;
+import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
 import org.apache.isis.core.metamodel.interactions.managed.ActionInteraction;
 import org.apache.isis.core.metamodel.interactions.managed.CollectionInteraction;
 import org.apache.isis.core.metamodel.interactions.managed.ManagedAction;
@@ -57,6 +61,14 @@ public class DomainObjectTesterFactory {
 
     private final @NonNull ServiceInjector serviceInjector;
 
+    public <T> ObjectTester<T> objectTester(
+            final Class<T> domainObjectType) {
+        val tester = serviceInjector.injectServicesInto(
+                new ObjectTester<T>(domainObjectType));
+        tester.init();
+        return tester;
+    }
+
     public <T> ActionTester<T> actionTester(
             final Class<T> domainObjectType,
             final String actionName) {
@@ -84,6 +96,48 @@ public class DomainObjectTesterFactory {
         return tester;
     }
 
+    // -- OBJECT TESTER
+
+    public static class ObjectTester<T>
+    extends Tester<T> {
+
+        protected ObjectTester(final @NonNull Class<T> domainObjectType) {
+            super(domainObjectType);
+        }
+
+        public void assertTitle(final @Nullable String expectedResult) {
+            assertEquals(expectedResult,
+                    super.objectSpecification.getTitleService().titleOf(vm.getPojo()));
+            assertEquals(expectedResult,
+                    super.objectSpecification.lookupFacet(TitleFacet.class)
+                    .map(titleFacet->titleFacet.title(vm))
+                    .orElse(null));
+        }
+
+        public void assertIcon(final @Nullable String expectedResult) {
+            assertEquals(expectedResult,
+                    super.objectSpecification.getTitleService().iconNameOf(vm.getPojo()));
+            assertEquals(expectedResult,
+                    super.objectSpecification.lookupFacet(IconFacet.class)
+                    .map(iconFacet->iconFacet.iconName(vm))
+                    .orElse(null));
+        }
+
+        public void assertCssClass(final @Nullable String expectedResult) {
+            assertEquals(expectedResult,
+                    super.objectSpecification.lookupFacet(CssClassFacet.class)
+                    .map(cssClassFacet->cssClassFacet.cssClass(vm))
+                    .orElse(null));
+        }
+
+        public void assertLayout(final @Nullable String expectedResult) {
+            assertEquals(expectedResult,
+                    super.objectSpecification.lookupFacet(LayoutFacet.class)
+                    .map(layoutFacet->layoutFacet.layout(vm))
+                    .orElse(null));
+        }
+
+    }
 
     // -- ACTION TESTER
 
@@ -259,32 +313,26 @@ public class DomainObjectTesterFactory {
 
     // -- COMMON ABSTRACT MEMBER TESTER
 
-    private static abstract class MemberTester<T> {
+    private static abstract class MemberTester<T>
+    extends Tester<T>{
 
-        @Inject protected SpecificationLoader specificationLoader;
-        @Inject protected InteractionService interactionService;
-        @Inject protected FactoryService factoryService;
-
-        @Getter private final Class<T> domainObjectType;
         @Getter private final String memberName;
         private final String memberSort;
 
-        @Getter private ObjectSpecification objectSpecification;
         private Optional<? extends ManagedMember> managedMemberIfAny;
 
         protected MemberTester(
                 final @NonNull Class<T> domainObjectType,
                 final @NonNull String memberName,
                 final @NonNull String memberSort) {
-
-            this.domainObjectType = domainObjectType;
+            super(domainObjectType);
             this.memberName = memberName;
             this.memberSort = memberSort;
         }
 
+        @Override
         protected final MemberTester<T> init() {
-            this.objectSpecification = specificationLoader.specForTypeElseFail(domainObjectType);
-            val vm = ManagedObject.of(objectSpecification, factoryService.viewModel(domainObjectType));
+            super.init();
             this.managedMemberIfAny = startInteractionOn(vm);
             return this;
         }
@@ -399,5 +447,28 @@ public class DomainObjectTesterFactory {
 
     }
 
+    private static abstract class Tester<T> {
+
+        @Inject protected SpecificationLoader specificationLoader;
+        @Inject protected InteractionService interactionService;
+        @Inject protected FactoryService factoryService;
+
+        @Getter private final Class<T> domainObjectType;
+
+        @Getter private ObjectSpecification objectSpecification;
+        protected ManagedObject vm;
+
+        protected Tester(
+                final @NonNull Class<T> domainObjectType) {
+            this.domainObjectType = domainObjectType;
+        }
+
+        protected Tester<T> init() {
+            this.objectSpecification = specificationLoader.specForTypeElseFail(domainObjectType);
+            this.vm = ManagedObject.of(objectSpecification, factoryService.viewModel(domainObjectType));
+            return this;
+        }
+
+    }
 
 }
\ No newline at end of file
diff --git a/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/fixturescripts/FixtureScripts.java b/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/fixturescripts/FixtureScripts.java
index d8eaaaf..846f5b9 100644
--- a/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/fixturescripts/FixtureScripts.java
+++ b/testing/fixtures/applib/src/main/java/org/apache/isis/testing/fixtures/applib/fixturescripts/FixtureScripts.java
@@ -33,6 +33,7 @@ import javax.xml.bind.annotation.XmlRootElement;
 import org.apache.isis.applib.ViewModel;
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.Domain;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.DomainServiceLayout;
 import org.apache.isis.applib.annotation.MemberSupport;
@@ -351,7 +352,7 @@ public class FixtureScripts {
                 ? choices.iterator().next()
                 : null;
     }
-    @MemberSupport private String defaultFromFixtureScriptsSpecification() {
+    @Domain.Exclude private String defaultFromFixtureScriptsSpecification() {
         Class<? extends FixtureScript> defaultScript = specification.getRunScriptDefaultScriptClass();
         return defaultScript != null
                 ? findFixtureScriptNameFor(defaultScript)