diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/core/LinkedSpecificationItem.java b/api/src/main/java/org/itsallcode/openfasttrace/api/core/LinkedSpecificationItem.java index f2345902..fc2b961e 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/core/LinkedSpecificationItem.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/core/LinkedSpecificationItem.java @@ -17,6 +17,8 @@ public class LinkedSpecificationItem private final Set coveredArtifactTypes = new HashSet<>(); private final Set coveredArtifactTypesFromApprovedItems = new HashSet<>(); private final Set overCoveredArtifactTypes = new HashSet<>(); + private boolean isRefined = false; + private boolean isRefinedApproved = false; /** * Create a new instance of class {@link LinkedSpecificationItem}. @@ -142,6 +144,10 @@ public void addLinkToItemWithStatus(final LinkedSpecificationItem item, final Li cacheApprovedCoveredArtifactType(item); coveredArtifactTypes.add(item.getArtifactType()); addMyItemIdToCoveringItem(item); + if (item.getArtifactType() != null && item.getArtifactType().equals(this.getArtifactType())) + { + isRefined = true; + } break; case COVERED_UNWANTED: cacheOverCoveredArtifactType(item); @@ -171,6 +177,10 @@ private void cacheApprovedCoveredArtifactType(final LinkedSpecificationItem cove if (coveringItem.isApproved()) { coveredArtifactTypesFromApprovedItems.add(coveringItem.getArtifactType()); + if (coveringItem.getArtifactType() != null && coveringItem.getArtifactType().equals(this.getArtifactType())) + { + isRefinedApproved = true; + } } } @@ -300,6 +310,26 @@ public List getUncoveredApprovedArtifactTypes() return uncovered; } + /** + * Check if this item is covered by an item of the same artifactType. + * + * @return true if this item is covered by an item of the same artifactType. + */ + public boolean isRefined() + { + return isRefined; + } + + /** + * Check if this item is covered by an item of the same artifactType and both items are approved. + * + * @return if this item is approved and is covered by an item of the same artifactType that is also approved. + */ + public boolean isRefinedApproved() + { + return isRefinedApproved; + } + /** * Check if the item is covered shallow (i.e. if for all needed artifact * types coverage exists without recursive search). @@ -447,12 +477,12 @@ private boolean hasBadLinks() private boolean areAllArtifactTypesCovered() { - return this.getCoveredArtifactTypes().containsAll(this.getNeedsArtifactTypes()); + return isRefined || this.getCoveredArtifactTypes().containsAll(this.getNeedsArtifactTypes()); } private boolean areAllCoveredArtifactTypesApproved() { - return this.getCoveredApprovedArtifactTypes().containsAll(this.getNeedsArtifactTypes()); + return isRefinedApproved || this.getCoveredApprovedArtifactTypes().containsAll(this.getNeedsArtifactTypes()); } private boolean isApproved() diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/Linker.java b/core/src/main/java/org/itsallcode/openfasttrace/core/Linker.java index 0fc738c4..bcbfac6d 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/Linker.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/Linker.java @@ -1,7 +1,6 @@ package org.itsallcode.openfasttrace.core; import java.util.HashMap; - import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -83,7 +82,8 @@ private void linkMatchingRevision(final LinkedSpecificationItem covering, final LinkedSpecificationItem covered) { final String coveringArtifactType = covering.getArtifactType(); - if (covered.getItem().getNeedsArtifactTypes().contains(coveringArtifactType)) + if (covered.getItem().getNeedsArtifactTypes().contains(coveringArtifactType) || + covered.getArtifactType().equals(covering.getArtifactType())) { if (covered.hasDuplicates()) { diff --git a/core/src/test/java/org/itsallcode/openfasttrace/core/TestLinker.java b/core/src/test/java/org/itsallcode/openfasttrace/core/TestLinker.java index e2f169a5..c75090de 100644 --- a/core/src/test/java/org/itsallcode/openfasttrace/core/TestLinker.java +++ b/core/src/test/java/org/itsallcode/openfasttrace/core/TestLinker.java @@ -1,19 +1,17 @@ package org.itsallcode.openfasttrace.core; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; import static org.itsallcode.openfasttrace.api.core.LinkStatus.*; import static org.itsallcode.openfasttrace.api.core.SpecificationItemId.createId; import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item; -import static org.itsallcode.openfasttrace.testutil.core.SampleArtifactTypes.IMPL; -import static org.itsallcode.openfasttrace.testutil.core.SampleArtifactTypes.REQ; -import static org.itsallcode.openfasttrace.testutil.core.SampleArtifactTypes.UTEST; +import static org.itsallcode.openfasttrace.testutil.core.SampleArtifactTypes.*; import static org.junit.jupiter.api.Assertions.fail; import java.util.*; import java.util.stream.Collectors; +import org.itsallcode.openfasttrace.api.core.DeepCoverageStatus; import org.itsallcode.openfasttrace.api.core.LinkStatus; import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItem; @@ -198,6 +196,16 @@ private void assertItemHasLinksWithStatus(final LinkedSpecificationItem itemUnde } } + private static void assertItemRefined( final LinkedSpecificationItem item, final boolean approved ) + { + if(approved) { + assertThat( item.getId() + " is refined by approved item", item.isRefinedApproved(), is(true)); + } else { + assertThat( item.getId() + " is refined by an item", item.isRefined(), is(true)); + } + } + + private String createDebugInfoFromExpectedStatuses( final Map expectedStatuses) { @@ -295,7 +303,7 @@ void testCoverageForDifferentArtifactTypes() .addCoveredId(REQ, "to-be-covered", 42) // .build(); final SpecificationItem unwanted = item() // - .id(REQ, "unwanted", 1) // + .id(ITEST, "unwanted", 1) // .addCoveredId(REQ, "to-be-covered", 42) // .build(); final List linkedItems = linkItems(covering, covered, unwanted); @@ -316,4 +324,35 @@ void testCoverageForDifferentArtifactTypes() } } + @Test + void testRefiningItemCoverItem() + { + final SpecificationItem covered = item() // + .id(REQ, "to-be-covered", 42) // + .addNeedsArtifactType(IMPL) // + .addNeedsArtifactType(UTEST) // + .build(); + final SpecificationItem coveringReq = item() // + .id(REQ, "covering", 1) // + .addCoveredId(REQ, "to-be-covered", 42) // + .build(); + final SpecificationItem coveringImpl = item() // + .id(IMPL, "covering", 1) // + .addCoveredId(REQ, "to-be-covered", 42) // + .build(); + final List linkedItems = linkItems(coveringReq, covered, coveringImpl); + final Optional linkedCovered = findLinkedItem(covered,linkedItems); + if (linkedCovered.isPresent()) + { + assertItemRefined(linkedCovered.get(), false); + assertItemRefined(linkedCovered.get(), true); + assertThat(linkedCovered.get().isCoveredShallow(),is(true)); + assertThat(linkedCovered.get().isCoveredShallowWithApprovedItems(),is(true)); + assertThat(linkedCovered.get().getDeepCoverageStatus(),equalTo(DeepCoverageStatus.COVERED)); + assertThat(linkedCovered.get().getDeepCoverageStatusOnlyAcceptApprovedItems(),equalTo(DeepCoverageStatus.COVERED)); + } else { + fail("Covered item " + covered.getId() + " not found in linked items"); + } + } + }